Repository: usememos/memos Branch: main Commit: a7cabb7ce66f Files: 743 Total size: 4.0 MB Directory structure: gitextract_vixwurpk/ ├── .dockerignore ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows/ │ ├── backend-tests.yml │ ├── build-canary-image.yml │ ├── demo-deploy.yml │ ├── frontend-tests.yml │ ├── proto-linter.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── .golangci.yaml ├── AGENTS.md ├── CLAUDE.md ├── CODEOWNERS ├── LICENSE ├── README.md ├── SECURITY.md ├── go.mod ├── go.sum ├── internal/ │ ├── base/ │ │ ├── resource_name.go │ │ └── resource_name_test.go │ ├── profile/ │ │ └── profile.go │ ├── util/ │ │ ├── util.go │ │ └── util_test.go │ └── version/ │ ├── version.go │ └── version_test.go ├── plugin/ │ ├── cron/ │ │ ├── README.md │ │ ├── chain.go │ │ ├── chain_test.go │ │ ├── constantdelay.go │ │ ├── constantdelay_test.go │ │ ├── cron.go │ │ ├── cron_test.go │ │ ├── logger.go │ │ ├── option.go │ │ ├── option_test.go │ │ ├── parser.go │ │ ├── parser_test.go │ │ ├── spec.go │ │ └── spec_test.go │ ├── email/ │ │ ├── README.md │ │ ├── client.go │ │ ├── client_test.go │ │ ├── config.go │ │ ├── config_test.go │ │ ├── doc.go │ │ ├── email.go │ │ ├── email_test.go │ │ ├── message.go │ │ └── message_test.go │ ├── filter/ │ │ ├── MAINTENANCE.md │ │ ├── README.md │ │ ├── engine.go │ │ ├── helpers.go │ │ ├── ir.go │ │ ├── parser.go │ │ ├── render.go │ │ └── schema.go │ ├── httpgetter/ │ │ ├── html_meta.go │ │ ├── html_meta_test.go │ │ ├── http_getter.go │ │ ├── image.go │ │ └── util.go │ ├── idp/ │ │ ├── idp.go │ │ └── oauth2/ │ │ ├── oauth2.go │ │ └── oauth2_test.go │ ├── markdown/ │ │ ├── ast/ │ │ │ └── tag.go │ │ ├── extensions/ │ │ │ └── tag.go │ │ ├── markdown.go │ │ ├── markdown_test.go │ │ ├── parser/ │ │ │ ├── tag.go │ │ │ └── tag_test.go │ │ └── renderer/ │ │ ├── markdown_renderer.go │ │ └── markdown_renderer_test.go │ ├── scheduler/ │ │ ├── README.md │ │ ├── doc.go │ │ ├── example_test.go │ │ ├── integration_test.go │ │ ├── job.go │ │ ├── job_test.go │ │ ├── middleware.go │ │ ├── middleware_test.go │ │ ├── parser.go │ │ ├── parser_test.go │ │ ├── scheduler.go │ │ └── scheduler_test.go │ ├── storage/ │ │ └── s3/ │ │ └── s3.go │ └── webhook/ │ ├── validate.go │ ├── webhook.go │ └── webhook_test.go ├── proto/ │ ├── README.md │ ├── api/ │ │ └── v1/ │ │ ├── README.md │ │ ├── attachment_service.proto │ │ ├── auth_service.proto │ │ ├── common.proto │ │ ├── idp_service.proto │ │ ├── instance_service.proto │ │ ├── memo_service.proto │ │ ├── shortcut_service.proto │ │ └── user_service.proto │ ├── buf.gen.yaml │ ├── buf.yaml │ ├── gen/ │ │ ├── api/ │ │ │ └── v1/ │ │ │ ├── apiv1connect/ │ │ │ │ ├── attachment_service.connect.go │ │ │ │ ├── auth_service.connect.go │ │ │ │ ├── idp_service.connect.go │ │ │ │ ├── instance_service.connect.go │ │ │ │ ├── memo_service.connect.go │ │ │ │ ├── shortcut_service.connect.go │ │ │ │ └── user_service.connect.go │ │ │ ├── attachment_service.pb.go │ │ │ ├── attachment_service.pb.gw.go │ │ │ ├── attachment_service_grpc.pb.go │ │ │ ├── auth_service.pb.go │ │ │ ├── auth_service.pb.gw.go │ │ │ ├── auth_service_grpc.pb.go │ │ │ ├── common.pb.go │ │ │ ├── idp_service.pb.go │ │ │ ├── idp_service.pb.gw.go │ │ │ ├── idp_service_grpc.pb.go │ │ │ ├── instance_service.pb.go │ │ │ ├── instance_service.pb.gw.go │ │ │ ├── instance_service_grpc.pb.go │ │ │ ├── memo_service.pb.go │ │ │ ├── memo_service.pb.gw.go │ │ │ ├── memo_service_grpc.pb.go │ │ │ ├── shortcut_service.pb.go │ │ │ ├── shortcut_service.pb.gw.go │ │ │ ├── shortcut_service_grpc.pb.go │ │ │ ├── user_service.pb.go │ │ │ ├── user_service.pb.gw.go │ │ │ └── user_service_grpc.pb.go │ │ ├── openapi.yaml │ │ └── store/ │ │ ├── attachment.pb.go │ │ ├── idp.pb.go │ │ ├── inbox.pb.go │ │ ├── instance_setting.pb.go │ │ ├── memo.pb.go │ │ └── user_setting.pb.go │ └── store/ │ ├── attachment.proto │ ├── idp.proto │ ├── inbox.proto │ ├── instance_setting.proto │ ├── memo.proto │ └── user_setting.proto ├── scripts/ │ ├── Dockerfile │ ├── build.sh │ ├── compose.yaml │ ├── entrypoint.sh │ ├── entrypoint_test.sh │ └── install.sh ├── server/ │ ├── auth/ │ │ ├── authenticator.go │ │ ├── context.go │ │ ├── extract.go │ │ ├── token.go │ │ └── token_test.go │ ├── router/ │ │ ├── api/ │ │ │ └── v1/ │ │ │ ├── acl_config.go │ │ │ ├── acl_config_test.go │ │ │ ├── attachment_exif_test.go │ │ │ ├── attachment_service.go │ │ │ ├── auth_service.go │ │ │ ├── auth_service_client_info_test.go │ │ │ ├── common.go │ │ │ ├── connect_handler.go │ │ │ ├── connect_interceptors.go │ │ │ ├── connect_services.go │ │ │ ├── header_carrier.go │ │ │ ├── health_service.go │ │ │ ├── idp_service.go │ │ │ ├── instance_service.go │ │ │ ├── memo_attachment_service.go │ │ │ ├── memo_relation_service.go │ │ │ ├── memo_service.go │ │ │ ├── memo_service_converter.go │ │ │ ├── memo_service_filter.go │ │ │ ├── memo_share_service.go │ │ │ ├── reaction_service.go │ │ │ ├── resource_name.go │ │ │ ├── shortcut_service.go │ │ │ ├── sse_handler.go │ │ │ ├── sse_hub.go │ │ │ ├── sse_hub_test.go │ │ │ ├── test/ │ │ │ │ ├── attachment_service_test.go │ │ │ │ ├── auth_test.go │ │ │ │ ├── idp_service_test.go │ │ │ │ ├── instance_admin_cache_test.go │ │ │ │ ├── instance_service_test.go │ │ │ │ ├── memo_attachment_service_test.go │ │ │ │ ├── memo_relation_service_test.go │ │ │ │ ├── memo_service_test.go │ │ │ │ ├── memo_share_service_test.go │ │ │ │ ├── reaction_service_test.go │ │ │ │ ├── shortcut_service_test.go │ │ │ │ ├── sse_handler_test.go │ │ │ │ ├── test_helper.go │ │ │ │ ├── user_notification_test.go │ │ │ │ ├── user_service_registration_test.go │ │ │ │ └── user_service_stats_test.go │ │ │ ├── user_service.go │ │ │ ├── user_service_stats.go │ │ │ └── v1.go │ │ ├── fileserver/ │ │ │ ├── README.md │ │ │ ├── fileserver.go │ │ │ └── fileserver_test.go │ │ ├── frontend/ │ │ │ └── frontend.go │ │ ├── mcp/ │ │ │ ├── README.md │ │ │ ├── mcp.go │ │ │ ├── prompts.go │ │ │ ├── resources_memo.go │ │ │ ├── tools_attachment.go │ │ │ ├── tools_memo.go │ │ │ ├── tools_reaction.go │ │ │ ├── tools_relation.go │ │ │ └── tools_tag.go │ │ └── rss/ │ │ └── rss.go │ ├── runner/ │ │ ├── memopayload/ │ │ │ └── runner.go │ │ └── s3presign/ │ │ └── runner.go │ └── server.go ├── store/ │ ├── attachment.go │ ├── cache/ │ │ ├── cache.go │ │ └── cache_test.go │ ├── cache.go │ ├── common.go │ ├── db/ │ │ ├── db.go │ │ ├── mysql/ │ │ │ ├── attachment.go │ │ │ ├── common.go │ │ │ ├── idp.go │ │ │ ├── inbox.go │ │ │ ├── instance_setting.go │ │ │ ├── memo.go │ │ │ ├── memo_relation.go │ │ │ ├── memo_share.go │ │ │ ├── mysql.go │ │ │ ├── reaction.go │ │ │ ├── user.go │ │ │ └── user_setting.go │ │ ├── postgres/ │ │ │ ├── attachment.go │ │ │ ├── common.go │ │ │ ├── idp.go │ │ │ ├── inbox.go │ │ │ ├── instance_setting.go │ │ │ ├── memo.go │ │ │ ├── memo_relation.go │ │ │ ├── memo_share.go │ │ │ ├── postgres.go │ │ │ ├── reaction.go │ │ │ ├── user.go │ │ │ ├── user_setting.go │ │ │ └── user_setting_test.go │ │ └── sqlite/ │ │ ├── attachment.go │ │ ├── common.go │ │ ├── functions.go │ │ ├── idp.go │ │ ├── inbox.go │ │ ├── instance_setting.go │ │ ├── memo.go │ │ ├── memo_relation.go │ │ ├── memo_share.go │ │ ├── reaction.go │ │ ├── sqlite.go │ │ ├── user.go │ │ └── user_setting.go │ ├── driver.go │ ├── idp.go │ ├── inbox.go │ ├── instance_setting.go │ ├── memo.go │ ├── memo_relation.go │ ├── memo_share.go │ ├── migration/ │ │ ├── mysql/ │ │ │ ├── 0.17/ │ │ │ │ ├── 00__inbox.sql │ │ │ │ └── 01__delete_activity.sql │ │ │ ├── 0.18/ │ │ │ │ ├── 00__extend_text.sql │ │ │ │ ├── 01__webhook.sql │ │ │ │ └── 02__user_setting.sql │ │ │ ├── 0.19/ │ │ │ │ └── 00__add_resource_name.sql │ │ │ ├── 0.20/ │ │ │ │ └── 00__reaction.sql │ │ │ ├── 0.21/ │ │ │ │ ├── 00__user_description.sql │ │ │ │ └── 01__rename_uid.sql │ │ │ ├── 0.22/ │ │ │ │ ├── 00__resource_storage_type.sql │ │ │ │ ├── 01__memo_tags.sql │ │ │ │ ├── 02__memo_payload.sql │ │ │ │ └── 03__drop_tag.sql │ │ │ ├── 0.23/ │ │ │ │ └── 00__reactions.sql │ │ │ ├── 0.24/ │ │ │ │ ├── 00__memo.sql │ │ │ │ ├── 01__memo_pinned.sql │ │ │ │ └── 02__s3_reference_length.sql │ │ │ ├── 0.25/ │ │ │ │ └── 00__remove_webhook.sql │ │ │ ├── 0.26/ │ │ │ │ ├── 00__rename_resource_to_attachment.sql │ │ │ │ ├── 01__drop_memo_organizer.sql │ │ │ │ └── 02__migrate_host_to_admin.sql │ │ │ ├── 0.27/ │ │ │ │ ├── 00__migrate_storage_setting.sql │ │ │ │ ├── 01__add_idp_uid.sql │ │ │ │ ├── 02__migrate_inbox_message_payload.sql │ │ │ │ ├── 03__drop_activity.sql │ │ │ │ └── 04__memo_share.sql │ │ │ └── LATEST.sql │ │ ├── postgres/ │ │ │ ├── 0.19/ │ │ │ │ └── 00__add_resource_name.sql │ │ │ ├── 0.20/ │ │ │ │ └── 00__reaction.sql │ │ │ ├── 0.21/ │ │ │ │ ├── 00__user_description.sql │ │ │ │ └── 01__rename_uid.sql │ │ │ ├── 0.22/ │ │ │ │ ├── 00__resource_storage_type.sql │ │ │ │ ├── 01__memo_tags.sql │ │ │ │ ├── 02__memo_payload.sql │ │ │ │ └── 03__drop_tag.sql │ │ │ ├── 0.23/ │ │ │ │ └── 00__reactions.sql │ │ │ ├── 0.24/ │ │ │ │ ├── 00__memo.sql │ │ │ │ └── 01__memo_pinned.sql │ │ │ ├── 0.25/ │ │ │ │ └── 00__remove_webhook.sql │ │ │ ├── 0.26/ │ │ │ │ ├── 00__rename_resource_to_attachment.sql │ │ │ │ ├── 01__drop_memo_organizer.sql │ │ │ │ └── 02__migrate_host_to_admin.sql │ │ │ ├── 0.27/ │ │ │ │ ├── 00__migrate_storage_setting.sql │ │ │ │ ├── 01__add_idp_uid.sql │ │ │ │ ├── 02__migrate_inbox_message_payload.sql │ │ │ │ ├── 03__drop_activity.sql │ │ │ │ └── 04__memo_share.sql │ │ │ └── LATEST.sql │ │ └── sqlite/ │ │ ├── 0.10/ │ │ │ └── 00__activity.sql │ │ ├── 0.11/ │ │ │ ├── 00__user_avatar.sql │ │ │ ├── 01__idp.sql │ │ │ └── 02__storage.sql │ │ ├── 0.12/ │ │ │ ├── 00__user_setting.sql │ │ │ ├── 01__system_setting.sql │ │ │ ├── 03__resource_internal_path.sql │ │ │ └── 04__resource_public_id.sql │ │ ├── 0.13/ │ │ │ ├── 00__memo_relation.sql │ │ │ └── 01__remove_memo_organizer_id.sql │ │ ├── 0.14/ │ │ │ ├── 00__drop_resource_public_id.sql │ │ │ └── 01__create_indexes.sql │ │ ├── 0.15/ │ │ │ └── 00__drop_user_open_id.sql │ │ ├── 0.16/ │ │ │ ├── 00__add_memo_id_to_resource.sql │ │ │ └── 01__drop_shortcut_table.sql │ │ ├── 0.17/ │ │ │ ├── 00__inbox.sql │ │ │ └── 01__delete_activities.sql │ │ ├── 0.18/ │ │ │ ├── 00__webhook.sql │ │ │ └── 01__user_setting.sql │ │ ├── 0.19/ │ │ │ └── 00__add_resource_name.sql │ │ ├── 0.2/ │ │ │ ├── 00__user_role.sql │ │ │ └── 01__memo_visibility.sql │ │ ├── 0.20/ │ │ │ └── 00__reaction.sql │ │ ├── 0.21/ │ │ │ ├── 00__user_description.sql │ │ │ └── 01__rename_uid.sql │ │ ├── 0.22/ │ │ │ ├── 00__resource_storage_type.sql │ │ │ ├── 01__memo_tags.sql │ │ │ ├── 02__memo_payload.sql │ │ │ └── 03__drop_tag.sql │ │ ├── 0.23/ │ │ │ └── 00__reactions.sql │ │ ├── 0.24/ │ │ │ ├── 00__memo.sql │ │ │ └── 01__memo_pinned.sql │ │ ├── 0.25/ │ │ │ └── 00__remove_webhook.sql │ │ ├── 0.26/ │ │ │ ├── 00__rename_resource_to_attachment.sql │ │ │ ├── 01__drop_memo_organizer.sql │ │ │ ├── 02__drop_indexes.sql │ │ │ ├── 03__alter_user_role.sql │ │ │ └── 04__migrate_host_to_admin.sql │ │ ├── 0.27/ │ │ │ ├── 00__migrate_storage_setting.sql │ │ │ ├── 01__add_idp_uid.sql │ │ │ ├── 02__migrate_inbox_message_payload.sql │ │ │ ├── 03__drop_activity.sql │ │ │ └── 04__memo_share.sql │ │ ├── 0.3/ │ │ │ └── 00__memo_visibility_protected.sql │ │ ├── 0.4/ │ │ │ └── 00__user_setting.sql │ │ ├── 0.5/ │ │ │ ├── 00__regenerate_foreign_keys.sql │ │ │ ├── 01__memo_resource.sql │ │ │ ├── 02__system_setting.sql │ │ │ └── 03__resource_extermal_link.sql │ │ ├── 0.6/ │ │ │ └── 00__recreate_triggers.sql │ │ ├── 0.7/ │ │ │ ├── 00__remove_fk.sql │ │ │ └── 01__remove_triggers.sql │ │ ├── 0.8/ │ │ │ ├── 00__migration_history.sql │ │ │ └── 01__user_username.sql │ │ ├── 0.9/ │ │ │ └── 00__tag.sql │ │ └── LATEST.sql │ ├── migrator.go │ ├── reaction.go │ ├── seed/ │ │ ├── DEMO_DATA_GUIDE.md │ │ └── sqlite/ │ │ └── 01__dump.sql │ ├── store.go │ ├── test/ │ │ ├── README.md │ │ ├── attachment_filter_test.go │ │ ├── attachment_test.go │ │ ├── containers.go │ │ ├── filter_helpers_test.go │ │ ├── idp_test.go │ │ ├── inbox_test.go │ │ ├── instance_setting_test.go │ │ ├── main_test.go │ │ ├── memo_filter_test.go │ │ ├── memo_relation_test.go │ │ ├── memo_test.go │ │ ├── migrator_test.go │ │ ├── reaction_test.go │ │ ├── store.go │ │ ├── user_setting_test.go │ │ └── user_test.go │ ├── user.go │ └── user_setting.go └── web/ ├── .gitignore ├── biome.json ├── components.json ├── docs/ │ └── auth-architecture.md ├── index.html ├── package.json ├── public/ │ └── site.webmanifest ├── src/ │ ├── App.tsx │ ├── auth-state.ts │ ├── components/ │ │ ├── ActivityCalendar/ │ │ │ ├── CalendarCell.tsx │ │ │ ├── MonthCalendar.tsx │ │ │ ├── YearCalendar.tsx │ │ │ ├── constants.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── useCalendar.ts │ │ │ └── utils.ts │ │ ├── AttachmentIcon.tsx │ │ ├── AuthFooter.tsx │ │ ├── ChangeMemberPasswordDialog.tsx │ │ ├── ConfirmDialog/ │ │ │ ├── README.md │ │ │ └── index.tsx │ │ ├── CreateAccessTokenDialog.tsx │ │ ├── CreateIdentityProviderDialog.tsx │ │ ├── CreateShortcutDialog.tsx │ │ ├── CreateUserDialog.tsx │ │ ├── CreateWebhookDialog.tsx │ │ ├── DateTimeInput.tsx │ │ ├── Empty.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── Inbox/ │ │ │ └── MemoCommentMessage.tsx │ │ ├── LearnMore.tsx │ │ ├── LocaleSelect.tsx │ │ ├── MemoActionMenu/ │ │ │ ├── MemoActionMenu.tsx │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── MemoAttachment.tsx │ │ ├── MemoContent/ │ │ │ ├── CodeBlock.tsx │ │ │ ├── ConditionalComponent.tsx │ │ │ ├── MermaidBlock.tsx │ │ │ ├── Table.tsx │ │ │ ├── Tag.tsx │ │ │ ├── TaskListItem.tsx │ │ │ ├── constants.ts │ │ │ ├── hooks.ts │ │ │ ├── index.tsx │ │ │ ├── markdown/ │ │ │ │ ├── Blockquote.tsx │ │ │ │ ├── Heading.tsx │ │ │ │ ├── HorizontalRule.tsx │ │ │ │ ├── Image.tsx │ │ │ │ ├── InlineCode.tsx │ │ │ │ ├── Link.tsx │ │ │ │ ├── List.tsx │ │ │ │ ├── Paragraph.tsx │ │ │ │ ├── README.md │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── MemoDetailSidebar/ │ │ │ ├── MemoDetailSidebar.tsx │ │ │ ├── MemoDetailSidebarDrawer.tsx │ │ │ └── index.ts │ │ ├── MemoDisplaySettingMenu.tsx │ │ ├── MemoEditor/ │ │ │ ├── Editor/ │ │ │ │ ├── SlashCommands.tsx │ │ │ │ ├── SuggestionsPopup.tsx │ │ │ │ ├── TagSuggestions.tsx │ │ │ │ ├── commands.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── shortcuts.ts │ │ │ │ ├── useListCompletion.ts │ │ │ │ └── useSuggestions.ts │ │ │ ├── README.md │ │ │ ├── Toolbar/ │ │ │ │ ├── InsertMenu.tsx │ │ │ │ ├── VisibilitySelector.tsx │ │ │ │ └── index.ts │ │ │ ├── components/ │ │ │ │ ├── AttachmentList.tsx │ │ │ │ ├── EditorContent.tsx │ │ │ │ ├── EditorMetadata.tsx │ │ │ │ ├── EditorToolbar.tsx │ │ │ │ ├── FocusModeOverlay.tsx │ │ │ │ ├── LinkMemoDialog.tsx │ │ │ │ ├── LocationDialog.tsx │ │ │ │ ├── LocationDisplay.tsx │ │ │ │ ├── RelationList.tsx │ │ │ │ ├── TimestampPopover.tsx │ │ │ │ └── index.ts │ │ │ ├── constants.ts │ │ │ ├── hooks/ │ │ │ │ ├── index.ts │ │ │ │ ├── useAutoSave.ts │ │ │ │ ├── useBlobUrls.ts │ │ │ │ ├── useDragAndDrop.ts │ │ │ │ ├── useFileUpload.ts │ │ │ │ ├── useFocusMode.ts │ │ │ │ ├── useKeyboard.ts │ │ │ │ ├── useLinkMemo.ts │ │ │ │ ├── useLocation.ts │ │ │ │ └── useMemoInit.ts │ │ │ ├── index.tsx │ │ │ ├── services/ │ │ │ │ ├── cacheService.ts │ │ │ │ ├── errorService.ts │ │ │ │ ├── index.ts │ │ │ │ ├── memoService.ts │ │ │ │ ├── uploadService.ts │ │ │ │ └── validationService.ts │ │ │ ├── state/ │ │ │ │ ├── actions.ts │ │ │ │ ├── context.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── reducer.ts │ │ │ │ └── types.ts │ │ │ └── types/ │ │ │ ├── attachment.ts │ │ │ ├── components.ts │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ └── insert-menu.ts │ │ ├── MemoExplorer/ │ │ │ ├── MemoExplorer.tsx │ │ │ ├── MemoExplorerDrawer.tsx │ │ │ ├── ShortcutsSection.tsx │ │ │ ├── TagsSection.tsx │ │ │ └── index.ts │ │ ├── MemoFilters.tsx │ │ ├── MemoPreview/ │ │ │ ├── MemoPreview.tsx │ │ │ └── index.ts │ │ ├── MemoReactionListView/ │ │ │ ├── MemoReactionListView.tsx │ │ │ ├── ReactionSelector.tsx │ │ │ ├── ReactionView.tsx │ │ │ ├── hooks.ts │ │ │ └── index.ts │ │ ├── MemoRelationForceGraph/ │ │ │ ├── MemoRelationForceGraph.tsx │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── MemoResource.tsx │ │ ├── MemoSharePanel.tsx │ │ ├── MemoView/ │ │ │ ├── MemoView.tsx │ │ │ ├── MemoViewContext.tsx │ │ │ ├── components/ │ │ │ │ ├── MemoBody.tsx │ │ │ │ ├── MemoCommentListView.tsx │ │ │ │ ├── MemoHeader.tsx │ │ │ │ ├── MemoSnippetLink.tsx │ │ │ │ ├── index.ts │ │ │ │ └── metadata/ │ │ │ │ ├── AttachmentCard.tsx │ │ │ │ ├── AttachmentList.tsx │ │ │ │ ├── LocationDisplay.tsx │ │ │ │ ├── RelationCard.tsx │ │ │ │ ├── RelationList.tsx │ │ │ │ ├── SectionHeader.tsx │ │ │ │ └── index.ts │ │ │ ├── constants.ts │ │ │ ├── hooks/ │ │ │ │ ├── index.ts │ │ │ │ ├── useImagePreview.ts │ │ │ │ ├── useMemoActions.ts │ │ │ │ └── useMemoHandlers.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── MemosLogo.tsx │ │ ├── MobileHeader.tsx │ │ ├── Navigation.tsx │ │ ├── NavigationDrawer.tsx │ │ ├── PagedMemoList/ │ │ │ ├── PagedMemoList.tsx │ │ │ └── index.ts │ │ ├── PasswordSignInForm.tsx │ │ ├── PreviewImageDialog.tsx │ │ ├── RequiredBadge.tsx │ │ ├── SearchBar.tsx │ │ ├── Settings/ │ │ │ ├── AccessTokenSection.tsx │ │ │ ├── InstanceSection.tsx │ │ │ ├── MemberSection.tsx │ │ │ ├── MemoRelatedSettings.tsx │ │ │ ├── MyAccountSection.tsx │ │ │ ├── PreferencesSection.tsx │ │ │ ├── SSOSection.tsx │ │ │ ├── SectionMenuItem.tsx │ │ │ ├── SettingGroup.tsx │ │ │ ├── SettingRow.tsx │ │ │ ├── SettingSection.tsx │ │ │ ├── SettingTable.tsx │ │ │ ├── StorageSection.tsx │ │ │ └── WebhookSection.tsx │ │ ├── Skeleton.tsx │ │ ├── StatisticsView/ │ │ │ ├── MonthNavigator.tsx │ │ │ ├── StatisticsView.tsx │ │ │ └── index.ts │ │ ├── TagTree.tsx │ │ ├── ThemeSelect.tsx │ │ ├── UpdateAccountDialog.tsx │ │ ├── UpdateCustomizedProfileDialog.tsx │ │ ├── UserAvatar.tsx │ │ ├── UserMemoMap/ │ │ │ ├── UserMemoMap.tsx │ │ │ └── index.ts │ │ ├── UserMenu.tsx │ │ ├── VisibilityIcon.tsx │ │ ├── kit/ │ │ │ ├── OverflowTip.tsx │ │ │ └── SquareDiv.tsx │ │ ├── map/ │ │ │ ├── LocationPicker.tsx │ │ │ ├── index.ts │ │ │ ├── map-utils.tsx │ │ │ └── useReverseGeocoding.ts │ │ └── ui/ │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── switch.tsx │ │ ├── textarea.tsx │ │ ├── tooltip.tsx │ │ └── visually-hidden.tsx │ ├── connect.ts │ ├── contexts/ │ │ ├── AuthContext.tsx │ │ ├── InstanceContext.tsx │ │ ├── MemoFilterContext.tsx │ │ └── ViewContext.tsx │ ├── helpers/ │ │ ├── consts.ts │ │ ├── resource-names.ts │ │ └── utils.ts │ ├── hooks/ │ │ ├── index.ts │ │ ├── useAsyncEffect.ts │ │ ├── useAttachmentQueries.ts │ │ ├── useCurrentUser.ts │ │ ├── useDateFilterNavigation.ts │ │ ├── useDialog.ts │ │ ├── useFilteredMemoStats.ts │ │ ├── useInstanceQueries.ts │ │ ├── useLiveMemoRefresh.ts │ │ ├── useLoading.ts │ │ ├── useMediaQuery.ts │ │ ├── useMemoFilters.ts │ │ ├── useMemoQueries.ts │ │ ├── useMemoShareQueries.ts │ │ ├── useMemoSorting.ts │ │ ├── useNavigateTo.ts │ │ ├── useTokenRefreshOnFocus.ts │ │ ├── useUserLocale.ts │ │ ├── useUserQueries.ts │ │ └── useUserTheme.ts │ ├── i18n.ts │ ├── index.css │ ├── layouts/ │ │ ├── MainLayout.tsx │ │ └── RootLayout.tsx │ ├── lib/ │ │ ├── calendar-utils.ts │ │ ├── error.ts │ │ ├── query-client.ts │ │ └── utils.ts │ ├── locales/ │ │ ├── ar.json │ │ ├── ca.json │ │ ├── cs.json │ │ ├── de.json │ │ ├── en-GB.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fa.json │ │ ├── fr.json │ │ ├── gl.json │ │ ├── hi.json │ │ ├── hr.json │ │ ├── hu.json │ │ ├── id.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── ka-GE.json │ │ ├── ko.json │ │ ├── mr.json │ │ ├── nb.json │ │ ├── nl.json │ │ ├── pl.json │ │ ├── pt-BR.json │ │ ├── pt-PT.json │ │ ├── ru.json │ │ ├── sl.json │ │ ├── sv.json │ │ ├── th.json │ │ ├── tr.json │ │ ├── uk.json │ │ ├── vi.json │ │ ├── zh-Hans.json │ │ └── zh-Hant.json │ ├── main.tsx │ ├── pages/ │ │ ├── AdminSignIn.tsx │ │ ├── Archived.tsx │ │ ├── Attachments.tsx │ │ ├── AuthCallback.tsx │ │ ├── Explore.tsx │ │ ├── Home.tsx │ │ ├── Inboxes.tsx │ │ ├── MemoDetail.tsx │ │ ├── NotFound.tsx │ │ ├── PermissionDenied.tsx │ │ ├── Setting.tsx │ │ ├── SharedMemo.tsx │ │ ├── SignIn.tsx │ │ ├── SignUp.tsx │ │ └── UserProfile.tsx │ ├── router/ │ │ ├── index.tsx │ │ └── routes.ts │ ├── themes/ │ │ ├── COLOR_GUIDE.md │ │ ├── default-dark.css │ │ ├── default.css │ │ └── paper.css │ ├── types/ │ │ ├── common.d.ts │ │ ├── common.ts │ │ ├── i18n.d.ts │ │ ├── markdown.ts │ │ ├── modules/ │ │ │ └── setting.d.ts │ │ ├── proto/ │ │ │ ├── api/ │ │ │ │ └── v1/ │ │ │ │ ├── attachment_service_pb.ts │ │ │ │ ├── auth_service_pb.ts │ │ │ │ ├── common_pb.ts │ │ │ │ ├── idp_service_pb.ts │ │ │ │ ├── instance_service_pb.ts │ │ │ │ ├── memo_service_pb.ts │ │ │ │ ├── shortcut_service_pb.ts │ │ │ │ └── user_service_pb.ts │ │ │ └── google/ │ │ │ ├── api/ │ │ │ │ ├── annotations_pb.ts │ │ │ │ ├── client_pb.ts │ │ │ │ ├── field_behavior_pb.ts │ │ │ │ ├── http_pb.ts │ │ │ │ ├── launch_stage_pb.ts │ │ │ │ └── resource_pb.ts │ │ │ └── type/ │ │ │ └── color_pb.ts │ │ ├── statistics.ts │ │ └── view.d.ts │ └── utils/ │ ├── attachment.ts │ ├── auth-redirect.ts │ ├── format.ts │ ├── i18n.ts │ ├── markdown-list-detection.ts │ ├── markdown-manipulation.ts │ ├── memo.ts │ ├── oauth.ts │ ├── remark-plugins/ │ │ ├── remark-disable-setext.ts │ │ ├── remark-preserve-type.ts │ │ └── remark-tag.ts │ ├── theme.ts │ ├── user.ts │ └── uuid.ts ├── tsconfig.json └── vite.config.mts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ web/node_modules web/dist .git .github build/ tmp/ memos *.md .gitignore .golangci.yaml .dockerignore docs/ .DS_Store ================================================ FILE: .github/FUNDING.yml ================================================ github: usememos ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug Report description: Something isn't working as expected type: Bug body: - type: markdown attributes: value: | Thanks for reporting a bug! Please fill out the form below so we can reproduce and fix the issue. **Before submitting**, please search [existing issues](https://github.com/usememos/memos/issues) to avoid duplicates. - type: checkboxes id: pre-check attributes: label: Pre-submission Checklist options: - label: I have searched existing issues and confirmed this bug has not been reported required: true - label: I can reproduce this bug on the latest version or the [demo site](https://demo.usememos.com) required: true - label: This is a bug, not a question (use [Discussions](https://github.com/usememos/memos/discussions) for questions) required: true - type: input id: version attributes: label: Memos Version description: Find this in **Settings > System > About** or via the `--version` flag placeholder: "v0.25.2" validations: required: true - type: dropdown id: deployment attributes: label: Deployment Method options: - Docker - Pre-built binary - Built from source validations: required: true - type: dropdown id: database attributes: label: Database options: - SQLite - PostgreSQL - MySQL validations: required: true - type: input id: browser-os attributes: label: Browser & OS description: e.g. Chrome 120 on macOS 15, Firefox 130 on Ubuntu 24.04 placeholder: "Chrome 120 on macOS 15" validations: required: false - type: textarea id: bug-description attributes: label: Bug Description description: A clear and concise description of what the bug is placeholder: When I try to..., the application... validations: required: true - type: textarea id: reproduction-steps attributes: label: Steps to Reproduce description: Minimal steps to reliably reproduce the issue placeholder: | 1. Go to '...' 2. Click on '...' 3. See error validations: required: true - type: textarea id: expected-behavior attributes: label: Expected Behavior description: What did you expect to happen instead? placeholder: I expected... validations: required: true - type: textarea id: additional-context attributes: label: Screenshots, Logs & Additional Context description: Attach screenshots, browser console errors, or server logs if available placeholder: Drag and drop images here, or paste error logs... ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Questions & Support url: https://github.com/usememos/memos/discussions about: Ask questions or get help in GitHub Discussions — please don't open issues for questions - name: Documentation url: https://www.usememos.com/docs about: Check the documentation before opening an issue ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature Request description: Suggest a new feature or improvement type: Feature body: - type: markdown attributes: value: | Thanks for suggesting a feature! Please fill out the form below so we can understand your idea. **Before submitting**, please search [existing issues](https://github.com/usememos/memos/issues?q=label%3Aenhancement) to avoid duplicates. - type: checkboxes id: pre-check attributes: label: Pre-submission Checklist options: - label: I have searched existing issues and confirmed this feature has not been requested required: true - label: This is a feature request, not a bug report or question required: true - type: dropdown id: feature-type attributes: label: Feature Area options: - User Interface (UI) - User Experience (UX) - API / Backend - Integrations / Plugins - Security / Privacy - Performance - Other validations: required: true - type: textarea id: problem-statement attributes: label: Problem or Use Case description: What problem does this feature solve? Why do you need it? placeholder: | I often need to... but currently there's no way to... validations: required: true - type: textarea id: proposed-solution attributes: label: Proposed Solution description: Describe what you'd like to happen placeholder: | It would be great if Memos could... validations: required: true - type: textarea id: alternatives attributes: label: Alternatives Considered description: Have you considered any workarounds or alternative approaches? placeholder: | I've tried... but it doesn't work well because... - type: textarea id: additional-context attributes: label: Additional Context description: Mockups, screenshots, examples from other apps, or any other context placeholder: Drag and drop images here... - type: checkboxes id: contribution attributes: label: Contribution description: Would you be willing to help implement this feature? options: - label: I'm willing to submit a pull request for this feature ================================================ FILE: .github/workflows/backend-tests.yml ================================================ name: Backend Tests on: push: branches: [main] pull_request: branches: [main] paths: - "go.mod" - "go.sum" - "**.go" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: GO_VERSION: "1.26.1" jobs: static-checks: name: Static Checks runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache: true cache-dependency-path: go.sum - name: Verify go.mod is tidy run: | go mod tidy -go=${{ env.GO_VERSION }} git diff --exit-code - name: Run golangci-lint uses: golangci/golangci-lint-action@v9 with: version: v2.11.3 args: --timeout=3m tests: name: Tests (${{ matrix.test-group }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: test-group: [store, server, plugin, other] steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache: true cache-dependency-path: go.sum - name: Run tests run: | case "${{ matrix.test-group }}" in store) # Run store tests for all drivers (sqlite, mysql, postgres) go test -v -coverprofile=coverage.out -covermode=atomic ./store/... ;; server) go test -v -race -coverprofile=coverage.out -covermode=atomic ./server/... ;; plugin) go test -v -race -coverprofile=coverage.out -covermode=atomic ./plugin/... ;; other) go test -v -race -coverprofile=coverage.out -covermode=atomic \ ./cmd/... ./internal/... ./proto/... ;; esac env: DRIVER: ${{ matrix.test-group == 'store' && '' || 'sqlite' }} - name: Upload coverage if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: codecov/codecov-action@v5 with: files: ./coverage.out flags: ${{ matrix.test-group }} fail_ci_if_error: false ================================================ FILE: .github/workflows/build-canary-image.yml ================================================ name: Build Canary Image on: push: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true jobs: build-frontend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4.2.0 with: version: 10 - uses: actions/setup-node@v6 with: node-version: "24" cache: pnpm cache-dependency-path: "web/pnpm-lock.yaml" - name: Get pnpm store directory id: pnpm-cache shell: bash run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - name: Setup pnpm cache uses: actions/cache@v5 with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }} restore-keys: ${{ runner.os }}-pnpm-store- - run: pnpm install --frozen-lockfile working-directory: web - name: Run frontend build run: pnpm release working-directory: web - name: Upload frontend artifacts uses: actions/upload-artifact@v6 with: name: frontend-dist path: server/router/frontend/dist retention-days: 1 build-push: needs: build-frontend runs-on: ubuntu-latest permissions: contents: read packages: write strategy: fail-fast: false matrix: platform: - linux/amd64 - linux/arm64 steps: - uses: actions/checkout@v6 - name: Download frontend artifacts uses: actions/download-artifact@v7 with: name: frontend-dist path: server/router/frontend/dist - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Build and push by digest id: build uses: docker/build-push-action@v6 with: context: . file: ./scripts/Dockerfile platforms: ${{ matrix.platform }} cache-from: type=gha,scope=build-${{ matrix.platform }} cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }} outputs: type=image,name=neosmemo/memos,push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v6 with: name: digests-${{ strategy.job-index }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge: needs: build-push runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Download digests uses: actions/download-artifact@v7 with: pattern: digests-* merge-multiple: true path: /tmp/digests - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: | neosmemo/memos ghcr.io/usememos/memos flavor: | latest=false tags: | type=raw,value=canary - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Create manifest list and push working-directory: /tmp/digests run: | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf 'neosmemo/memos@sha256:%s ' *) env: DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} - name: Inspect images run: | docker buildx imagetools inspect neosmemo/memos:canary docker buildx imagetools inspect ghcr.io/usememos/memos:canary ================================================ FILE: .github/workflows/demo-deploy.yml ================================================ name: Demo Deploy on: workflow_dispatch: jobs: deploy-demo: runs-on: ubuntu-latest steps: - name: Trigger Render Deploy run: | curl -X POST "${{ secrets.RENDER_DEPLOY_HOOK }}" \ -H "Content-Type: application/json" \ -d '{"trigger": "github_action"}' - name: Deployment Status run: echo "Demo deployment triggered successfully on Render" ================================================ FILE: .github/workflows/frontend-tests.yml ================================================ name: Frontend Tests on: push: branches: [main] pull_request: branches: [main] paths: - "web/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: NODE_VERSION: "24" PNPM_VERSION: "10" jobs: lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup pnpm uses: pnpm/action-setup@v4.2.0 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm cache-dependency-path: web/pnpm-lock.yaml - name: Install dependencies working-directory: web run: pnpm install --frozen-lockfile - name: Run lint working-directory: web run: pnpm lint build: name: Build runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup pnpm uses: pnpm/action-setup@v4.2.0 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm cache-dependency-path: web/pnpm-lock.yaml - name: Install dependencies working-directory: web run: pnpm install --frozen-lockfile - name: Build frontend working-directory: web run: pnpm build ================================================ FILE: .github/workflows/proto-linter.yml ================================================ name: Proto Linter on: push: branches: [main] pull_request: branches: [main] paths: - "proto/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: lint: name: Lint Protos runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup buf uses: bufbuild/buf-setup-action@v1 with: github_token: ${{ github.token }} - name: Run buf lint uses: bufbuild/buf-lint-action@v1 with: input: proto - name: Check buf format run: | if [[ $(buf format -d) ]]; then echo "❌ Proto files are not formatted. Run 'buf format -w' to fix." exit 1 fi ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - "v*.*.*" workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: GO_VERSION: "1.26.1" NODE_VERSION: "24" PNPM_VERSION: "10" ARTIFACT_RETENTION_DAYS: 60 ARTIFACT_PREFIX: memos jobs: prepare: name: Extract Version runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} tag: ${{ steps.version.outputs.tag }} steps: - name: Extract version id: version env: REF_NAME: ${{ github.ref_name }} EVENT_NAME: ${{ github.event_name }} run: | if [ "$EVENT_NAME" = "workflow_dispatch" ]; then echo "tag=" >> "$GITHUB_OUTPUT" echo "version=manual-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" exit 0 fi echo "tag=${REF_NAME}" >> "$GITHUB_OUTPUT" echo "version=${REF_NAME#v}" >> "$GITHUB_OUTPUT" build-frontend: name: Build Frontend needs: prepare runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup pnpm uses: pnpm/action-setup@v4.2.0 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm cache-dependency-path: web/pnpm-lock.yaml - name: Get pnpm store directory id: pnpm-cache shell: bash run: echo "STORE_PATH=$(pnpm store path)" >> "$GITHUB_OUTPUT" - name: Setup pnpm cache uses: actions/cache@v4 with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }} restore-keys: ${{ runner.os }}-pnpm-store- - name: Install dependencies working-directory: web run: pnpm install --frozen-lockfile - name: Build frontend release assets working-directory: web run: pnpm release - name: Upload frontend artifacts uses: actions/upload-artifact@v4 with: name: frontend-dist path: server/router/frontend/dist retention-days: 1 build-binaries: name: Build ${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm && format('v{0}', matrix.goarm) || '' }} needs: [prepare, build-frontend] runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - goos: linux goarch: amd64 - goos: linux goarch: arm64 - goos: linux goarch: arm goarm: "7" - goos: darwin goarch: amd64 - goos: darwin goarch: arm64 - goos: windows goarch: amd64 steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Download frontend artifacts uses: actions/download-artifact@v4 with: name: frontend-dist path: server/router/frontend/dist - name: Build binary env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} GOARM: ${{ matrix.goarm }} CGO_ENABLED: "0" run: | output_name="memos" if [ "$GOOS" = "windows" ]; then output_name="memos.exe" fi mkdir -p build go build \ -trimpath \ -ldflags="-s -w -X github.com/usememos/memos/internal/version.Version=${{ needs.prepare.outputs.version }} -extldflags '-static'" \ -tags netgo,osusergo \ -o "build/${output_name}" \ ./cmd/memos - name: Package binary env: VERSION: ${{ needs.prepare.outputs.version }} GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} GOARM: ${{ matrix.goarm }} run: | cd build package_name="${ARTIFACT_PREFIX}_${VERSION}_${GOOS}_${GOARCH}" if [ -n "$GOARM" ]; then package_name="${package_name}v${GOARM}" fi if [ "$GOOS" = "windows" ]; then artifact_name="${package_name}.zip" zip -q "${artifact_name}" memos.exe else artifact_name="${package_name}.tar.gz" tar czf "${artifact_name}" memos fi echo "artifact_name=${artifact_name}" >> "$GITHUB_ENV" - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: ${{ env.artifact_name }} path: build/${{ env.artifact_name }} retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} checksums: name: Generate Checksums needs: [prepare, build-binaries] runs-on: ubuntu-latest steps: - name: Download binary artifacts uses: actions/download-artifact@v4 with: path: artifacts pattern: ${{ env.ARTIFACT_PREFIX }}_* merge-multiple: true - name: Generate checksums working-directory: artifacts run: sha256sum * > checksums.txt - name: Upload checksum artifact uses: actions/upload-artifact@v4 with: name: checksums path: artifacts/checksums.txt retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} release: name: Publish GitHub Release needs: [prepare, build-binaries, checksums] if: github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: write steps: - name: Download binary artifacts uses: actions/download-artifact@v4 with: path: artifacts pattern: ${{ env.ARTIFACT_PREFIX }}_* merge-multiple: true - name: Download checksum artifact uses: actions/download-artifact@v4 with: name: checksums path: artifacts - name: Publish release assets uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.prepare.outputs.tag }} name: ${{ needs.prepare.outputs.tag }} generate_release_notes: true files: artifacts/* build-push: name: Build Image ${{ matrix.platform }} needs: [prepare, build-frontend] if: github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: read packages: write strategy: fail-fast: false matrix: platform: - linux/amd64 - linux/arm/v7 - linux/arm64 steps: - name: Checkout code uses: actions/checkout@v6 - name: Download frontend artifacts uses: actions/download-artifact@v4 with: name: frontend-dist path: server/router/frontend/dist - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Build and push by digest id: build uses: docker/build-push-action@v6 with: context: . file: ./scripts/Dockerfile platforms: ${{ matrix.platform }} build-args: | VERSION=${{ needs.prepare.outputs.version }} COMMIT=${{ github.sha }} cache-from: type=gha,scope=release-${{ matrix.platform }} cache-to: type=gha,mode=max,scope=release-${{ matrix.platform }} outputs: type=image,name=neosmemo/memos,push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v4 with: name: digests-${{ strategy.job-index }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge-images: name: Publish Stable Image Tags needs: [prepare, build-push] if: github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Download digests uses: actions/download-artifact@v4 with: pattern: digests-* merge-multiple: true path: /tmp/digests - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Create manifest list and push working-directory: /tmp/digests run: | version="${{ needs.prepare.outputs.version }}" major_minor=$(echo "$version" | cut -d. -f1,2) docker buildx imagetools create \ -t "neosmemo/memos:${version}" \ -t "neosmemo/memos:${major_minor}" \ -t "neosmemo/memos:stable" \ -t "ghcr.io/usememos/memos:${version}" \ -t "ghcr.io/usememos/memos:${major_minor}" \ -t "ghcr.io/usememos/memos:stable" \ $(printf 'neosmemo/memos@sha256:%s ' *) - name: Inspect images run: | docker buildx imagetools inspect neosmemo/memos:${{ needs.prepare.outputs.version }} docker buildx imagetools inspect neosmemo/memos:stable ================================================ FILE: .github/workflows/stale.yml ================================================ name: Close Stale on: schedule: - cron: "0 */8 * * *" # Every 8 hours jobs: close-stale: name: Close Stale Issues and PRs runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - name: Mark and close stale issues and PRs uses: actions/stale@v10.1.1 with: # Issues: mark stale after 14 days of inactivity, close after 3 more days days-before-issue-stale: 14 days-before-issue-close: 3 # Pull requests: mark stale after 14 days of inactivity, close after 3 more days days-before-pr-stale: 14 days-before-pr-close: 3 ================================================ FILE: .gitignore ================================================ # temp folder tmp # Frontend asset web/dist # Build artifacts build/ bin/ memos # Plan/design documents docs/plans/ .DS_Store # Jetbrains .idea # Docker Compose Environment File .env dist # VSCode settings .vscode # Git worktrees .worktrees/ ================================================ FILE: .golangci.yaml ================================================ 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 ================================================ FILE: AGENTS.md ================================================ # AGENTS.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. Self-hosted note-taking tool. Go 1.26 backend (Echo v5, Connect RPC + gRPC-Gateway), React 18 + TypeScript 5.9 + Vite 7 frontend, Protocol Buffers API, SQLite/MySQL/PostgreSQL. ## Commands ```bash # Backend go run ./cmd/memos --port 8081 # Start dev server go test ./... # Run all tests go test -v ./store/... # Run store tests (all 3 DB drivers via TestContainers) go test -v -race ./server/... # Run server tests with race detection go test -v -run TestFoo ./pkg/... # Run a single test golangci-lint run # Lint (v2, config: .golangci.yaml) golangci-lint run --fix # Auto-fix lint issues (includes goimports) # Frontend (cd web) pnpm install # Install deps pnpm dev # Dev server (:3001, proxies API to :8081) pnpm lint # Type check + Biome lint pnpm lint:fix # Auto-fix lint issues pnpm format # Format code pnpm build # Production build pnpm release # Build to server/router/frontend/dist # Protocol Buffers (cd proto) buf generate # Regenerate Go + TypeScript + OpenAPI buf lint # Lint proto files buf format -w # Format proto files ``` ## Architecture ``` cmd/memos/main.go # Cobra CLI + Viper config, server init server/ ├── server.go # Echo v5 HTTP server, background runners ├── auth/ # JWT access (15min) + refresh (30d) tokens, PAT ├── router/ │ ├── api/v1/ # 8 gRPC services (Connect + Gateway) │ │ ├── acl_config.go # Public endpoints whitelist │ │ ├── sse_hub.go # Server-Sent Events (live updates) │ │ └── mcp/ # MCP server for AI assistants │ ├── frontend/ # SPA static file serving │ ├── fileserver/ # Native HTTP file server (thumbnails, range requests) │ └── rss/ # RSS feeds └── runner/ # Background: memo payload processing, S3 presign refresh store/ ├── driver.go # Database driver interface ├── store.go # Store wrapper + in-memory cache (TTL 10min, max 1000) ├── migrator.go # Migration logic (LATEST.sql for fresh, incremental for upgrades) └── db/{sqlite,mysql,postgres}/ # Driver implementations proto/ ├── api/v1/ # Service definitions ├── store/ # Internal storage messages └── gen/ # Generated Go, TypeScript, OpenAPI plugin/ # scheduler, cron, email, filter (CEL), webhook, # markdown (Goldmark), httpgetter, idp (OAuth2), storage/s3 web/src/ ├── connect.ts # Connect RPC client + auth interceptor + token refresh ├── auth-state.ts # Token storage (localStorage + BroadcastChannel cross-tab) ├── contexts/ # AuthContext, InstanceContext, ViewContext, MemoFilterContext ├── hooks/ # React Query hooks (useMemoQueries, useUserQueries, etc.) ├── lib/query-client.ts # React Query v5 (staleTime: 30s, gcTime: 5min) ├── router/index.tsx # Route definitions ├── components/ # UI components (Radix UI primitives, MemoEditor, Settings, etc.) ├── themes/ # CSS themes (default, dark, paper) — OKLch color tokens └── pages/ # Page components ``` ## Conventions ### Go - **Errors:** `errors.Wrap(err, "context")` from `github.com/pkg/errors`. Never `fmt.Errorf` (lint-enforced via forbidigo). - **gRPC errors:** `status.Errorf(codes.X, "message")` from service methods. - **Imports:** stdlib, then third-party, then local (`github.com/usememos/memos`). Enforced by goimports (runs as golangci-lint formatter). - **Comments:** All exported functions must have doc comments (godot enforced). ### Frontend - **Imports:** Use `@/` alias for absolute imports. - **Formatting:** Biome — 140 char lines, double quotes, always semicolons, 2-space indent. - **State:** Server data via React Query hooks (`hooks/`). Client state via React Context (`contexts/`). - **Styling:** Tailwind CSS v4 (`@tailwindcss/vite`), `cn()` utility (clsx + tailwind-merge), CVA for variants. ### Database & Proto - **DB changes:** Migration files for all 3 drivers + update `LATEST.sql`. - **Proto changes:** Run `buf generate`. Generated code: `proto/gen/` and `web/src/types/proto/`. - **Public endpoints:** Add to `server/router/api/v1/acl_config.go`. ## CI/CD - **backend-tests.yml:** Go 1.26.1, golangci-lint v2.4.0, tests parallelized by group (store, server, plugin, other) - **frontend-tests.yml:** Node 24, pnpm 10, lint + build - **proto-linter.yml:** buf lint + format check - **Docker:** Multi-stage (`scripts/Dockerfile`), Alpine 3.21, non-root user, port 5230, multi-arch (amd64/arm64/arm/v7) ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. See `AGENTS.md` for full architecture, workflows, conventions, and patterns. ================================================ FILE: CODEOWNERS ================================================ # These owners will be the default owners for everything in the repo. * @usememos/moderators ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: README.md ================================================

Featured Sponsor: Warp — The AI-powered terminal built for speed and collaboration

Warp sponsorship
# Memos Memos Open-source, self-hosted note-taking tool built for quick capture. Markdown-native, lightweight, and fully yours. [![Home](https://img.shields.io/badge/🏠-usememos.com-blue?style=flat-square)](https://usememos.com) [![Live Demo](https://img.shields.io/badge/✨-Try%20Demo-orange?style=flat-square)](https://demo.usememos.com/) [![Docs](https://img.shields.io/badge/📚-Documentation-green?style=flat-square)](https://usememos.com/docs) [![Discord](https://img.shields.io/badge/💬-Discord-5865f2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/tfPJa4UmAv) [![Docker Pulls](https://img.shields.io/docker/pulls/neosmemo/memos?style=flat-square&logo=docker)](https://hub.docker.com/r/neosmemo/memos) Memos Demo Screenshot ### 💎 Featured Sponsors [**Warp** — The AI-powered terminal built for speed and collaboration](https://go.warp.dev/memos) Warp - The AI-powered terminal built for speed and collaboration

[**TestMu AI** - The world’s first full-stack Agentic AI Quality Engineering platform](https://www.testmuai.com/?utm_medium=sponsor&utm_source=memos) TestMu AI

[**SSD Nodes** - Affordable VPS hosting for self-hosters](https://ssdnodes.com/?utm_source=memos&utm_medium=sponsor) SSD Nodes ## Features - **Instant Capture** — Timeline-first UI. Open, write, done — no folders to navigate. - **Total Data Ownership** — Self-hosted on your infrastructure. Notes stored in Markdown, always portable. Zero telemetry. - **Radical Simplicity** — Single Go binary, ~20MB Docker image. One command to deploy with SQLite, MySQL, or PostgreSQL. - **Open & Extensible** — MIT-licensed with full REST and gRPC APIs for integration. ## Quick Start ### Docker (Recommended) ```bash docker run -d \ --name memos \ -p 5230:5230 \ -v ~/.memos:/var/opt/memos \ neosmemo/memos:stable ``` Open `http://localhost:5230` and start writing! ### Native Binary ```bash curl -fsSL https://raw.githubusercontent.com/usememos/memos/main/scripts/install.sh | sh ``` ### Try the Live Demo Don't want to install yet? Try our [live demo](https://demo.usememos.com/) first! ### Other Installation Methods - **Docker Compose** - Recommended for production deployments - **Pre-built Binaries** - Available for Linux, macOS, and Windows - **Kubernetes** - Helm charts and manifests available - **Build from Source** - For development and customization See our [installation guide](https://usememos.com/docs/deploy) for detailed instructions. ## Contributing Contributions are welcome — bug reports, feature suggestions, pull requests, documentation, and translations. - [Report bugs](https://github.com/usememos/memos/issues/new?template=bug_report.md) - [Suggest features](https://github.com/usememos/memos/issues/new?template=feature_request.md) - [Submit pull requests](https://github.com/usememos/memos/pulls) - [Improve documentation](https://github.com/usememos/dotcom) - [Help with translations](https://github.com/usememos/memos/tree/main/web/src/locales) ## Sponsors Love Memos? [Sponsor us on GitHub](https://github.com/sponsors/usememos) to help keep the project growing! ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=usememos/memos&type=Date)](https://star-history.com/#usememos/memos&Date) ## License Memos is open-source software licensed under the [MIT License](LICENSE). See our [Privacy Policy](https://usememos.com/privacy) for details on data handling. --- **[Website](https://usememos.com)** • **[Documentation](https://usememos.com/docs)** • **[Demo](https://demo.usememos.com/)** • **[Discord](https://discord.gg/tfPJa4UmAv)** • **[X/Twitter](https://x.com/usememos)** Vercel OSS Program ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Project Status Memos is currently in beta (v0.x). While we take security seriously, we are not yet ready for formal CVE assignments or coordinated disclosure programs. ## Reporting Security Issues ### For All Security Concerns: Please report via **email only**: dev@usememos.com **DO NOT open public GitHub issues for security vulnerabilities.** Include in your report: - Description of the issue - Steps to reproduce - Affected versions - Your assessment of severity ### What to Expect: - We will acknowledge your report as soon as we can - Fixes will be included in regular releases without special security advisories - No CVEs will be assigned during the beta phase - Credit will be given in release notes if you wish ### For Non-Security Bugs: Use GitHub issues for functionality bugs, feature requests, and general questions. ## Philosophy As a beta project, we prioritize: 1. **Rapid iteration** over lengthy disclosure timelines 2. **Quick patches** over formal security processes 3. **Transparency** about our beta status We plan to implement formal vulnerability disclosure and CVE handling after reaching v1.0 stable. ## Self-Hosting Security Since Memos is self-hosted software: - Keep your instance updated to the latest release - Don't expose your instance directly to the internet without authentication - Use reverse proxies (nginx, Caddy) with rate limiting - Review the deployment documentation for security best practices Thank you for helping improve Memos! ================================================ FILE: go.mod ================================================ module github.com/usememos/memos go 1.26.1 require ( connectrpc.com/connect v1.19.1 github.com/aws/aws-sdk-go-v2 v1.41.4 github.com/aws/aws-sdk-go-v2/config v1.32.12 github.com/aws/aws-sdk-go-v2/credentials v1.19.12 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 github.com/docker/docker v28.5.2+incompatible github.com/go-sql-driver/mysql v1.9.3 github.com/google/cel-go v0.27.0 github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.2.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 github.com/joho/godotenv v1.5.1 github.com/labstack/echo/v5 v5.0.4 github.com/lib/pq v1.11.2 github.com/lithammer/shortuuid/v4 v4.2.0 github.com/mark3labs/mcp-go v0.45.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.41.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.41.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 github.com/yuin/goldmark v1.7.16 golang.org/x/crypto v0.49.0 golang.org/x/mod v0.34.0 golang.org/x/net v0.52.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 google.golang.org/genproto/googleapis/api v0.0.0-20260316172706-e463d84ca32d google.golang.org/grpc v1.79.2 modernc.org/sqlite v1.46.1 ) require ( cel.dev/expr v0.25.1 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/shirou/gopsutil/v4 v4.26.2 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/otel v1.41.0 // indirect go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/otel/trace v1.41.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/image v0.30.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect github.com/aws/smithy-go v1.24.2 // 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.3.1 github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0= github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 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/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 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-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= github.com/google/go-cmp v0.5.6/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/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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/v5 v5.0.4 h1:ll3I/O8BifjMztj9dD1vx/peZQv8cR2CTUdQK6QxGGc= github.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= 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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc= github.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= 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/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 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/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais= github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI= github.com/testcontainers/testcontainers-go/modules/mysql v0.41.0 h1:5rwejaJr5nIfw8NK99eKPX7O6k27lnSMklTj5DbYybM= github.com/testcontainers/testcontainers-go/modules/mysql v0.41.0/go.mod h1:iMO/aFWnbjYkqHw8VPsJB3rVTOD9hKDsUtV0PvzD0DA= github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 h1:AOtFXssrDlLm84A2sTTR/AhvJiYbrIuCO59d+Ro9Tb0= github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0/go.mod h1:k2a09UKhgSp6vNpliIY0QSgm4Hi7GXVTzWvWgUemu/8= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE= google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= google.golang.org/genproto/googleapis/api v0.0.0-20260316172706-e463d84ca32d h1:RdWlPmVySdTF0IBIZzvZJvSD0ZocPBNUsnE+uGBxj+4= google.golang.org/genproto/googleapis/api v0.0.0-20260316172706-e463d84ca32d/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.3.40/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/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= 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.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= 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= ================================================ FILE: internal/base/resource_name.go ================================================ package base import "regexp" var ( UIDMatcher = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,30}[a-zA-Z0-9])?$") ) ================================================ FILE: internal/base/resource_name_test.go ================================================ 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) } }) } } ================================================ FILE: internal/profile/profile.go ================================================ 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 { // Demo indicates if the server is in demo mode Demo bool // 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 checkDataDir(dataDir string) (string, error) { // Convert to absolute path if relative path is supplied. if !filepath.IsAbs(dataDir) { // Use current working directory, not the binary's directory // This ensures we use the actual working directory where the process runs absDir, err := filepath.Abs(dataDir) 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 { // Set default data directory if not specified if p.Data == "" { if runtime.GOOS == "windows" { p.Data = filepath.Join(os.Getenv("ProgramData"), "memos") } else { // On Linux/macOS, check if /var/opt/memos exists and is writable (Docker scenario) if info, err := os.Stat("/var/opt/memos"); err == nil && info.IsDir() { // Check if we can write to this directory testFile := filepath.Join("/var/opt/memos", ".write-test") if err := os.WriteFile(testFile, []byte("test"), 0600); err == nil { os.Remove(testFile) p.Data = "/var/opt/memos" } else { // /var/opt/memos exists but is not writable, use current directory slog.Warn("/var/opt/memos is not writable, using current directory") p.Data = "." } } else { // /var/opt/memos doesn't exist, use current directory (local development) p.Data = "." } } } // Create data directory if it doesn't exist 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 } } 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 == "" { mode := "prod" if p.Demo { mode = "demo" } dbFile := fmt.Sprintf("memos_%s.db", mode) p.DSN = filepath.Join(dataDir, dbFile) } return nil } ================================================ FILE: internal/util/util.go ================================================ package util //nolint:revive // util namespace is intentional for shared helpers 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 } ================================================ FILE: internal/util/util_test.go ================================================ package util //nolint:revive // util is an appropriate package name for utility functions 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) } } } ================================================ FILE: internal/version/version.go ================================================ 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.27.0" func GetCurrentVersion() string { return Version } // GetMinorVersion extracts the minor version (e.g., "0.25") from a full version string (e.g., "0.25.1"). // Returns the minor version string or empty string if the version format is invalid. // Version format should be "major.minor.patch" (e.g., "0.25.1"). func GetMinorVersion(version string) string { versionList := strings.Split(version, ".") if len(versionList) < 2 { return "" } // Return major.minor only (first two components) 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 } ================================================ FILE: internal/version/version_test.go ================================================ package version import ( "slices" "testing" "github.com/stretchr/testify/assert" "golang.org/x/mod/semver" ) 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 { slices.SortFunc(test.versionList, func(a, b string) int { return semver.Compare("v"+a, "v"+b) }) assert.Equal(t, test.versionList, test.want) } } ================================================ FILE: plugin/cron/README.md ================================================ Fork from https://github.com/robfig/cron ================================================ FILE: plugin/cron/chain.go ================================================ 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") } }) } } ================================================ FILE: plugin/cron/chain_test.go ================================================ //nolint:all package cron import ( "io" "log" "reflect" "sync" "testing" "time" ) func waitFor(t *testing.T, timeout time.Duration, fn func() bool) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { if fn() { return } time.Sleep(time.Millisecond) } t.Fatal("condition not met before timeout") } 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() waitFor(t, 100*time.Millisecond, func() bool { return j.Done() == 1 }) 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() }() waitFor(t, 100*time.Millisecond, func() bool { return j.Done() == 2 }) 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() }() waitFor(t, 100*time.Millisecond, func() bool { return j.Started() == 1 }) started, done := j.Started(), j.Done() if done != 0 { t.Error("expected first job started, but not finished, got", started, done) } waitFor(t, 200*time.Millisecond, func() bool { return j.Done() == 2 }) 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 // already 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) } }) } ================================================ FILE: plugin/cron/constantdelay.go ================================================ 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) } ================================================ FILE: plugin/cron/constantdelay_test.go ================================================ //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) } } } ================================================ FILE: plugin/cron/cron.go ================================================ package cron import ( "context" "slices" "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 } // 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. slices.SortFunc(c.entries, func(a, b *Entry) int { switch { case a.Next.IsZero() && b.Next.IsZero(): return 0 case a.Next.IsZero(): return 1 case b.Next.IsZero(): return -1 case a.Next.Before(b.Next): return -1 case b.Next.Before(a.Next): return 1 default: return 0 } }) 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.Go(func() { 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 } ================================================ FILE: plugin/cron/cron_test.go ================================================ //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()) } ================================================ FILE: plugin/cron/logger.go ================================================ 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 } ================================================ FILE: plugin/cron/option.go ================================================ 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 } } ================================================ FILE: plugin/cron/option_test.go ================================================ //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) } } ================================================ FILE: plugin/cron/parser.go ================================================ 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 parsed 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 default: // Continue to check @every prefix below } 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) } ================================================ FILE: plugin/cron/parser_test.go ================================================ //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, } } ================================================ FILE: plugin/cron/spec.go ================================================ 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 } ================================================ FILE: plugin/cron/spec_test.go ================================================ //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") } } ================================================ FILE: plugin/email/README.md ================================================ # Email Plugin SMTP email sending functionality for self-hosted Memos instances. ## Overview This plugin provides a simple, reliable email sending interface following industry-standard SMTP protocols. It's designed for self-hosted environments where instance administrators configure their own email service, similar to platforms like GitHub, GitLab, and Discourse. ## Features - Standard SMTP protocol support - TLS/STARTTLS and SSL/TLS encryption - HTML and plain text emails - Multiple recipients (To, Cc, Bcc) - Synchronous and asynchronous sending - Detailed error reporting with context - Works with all major email providers - Reply-To header support - RFC 5322 compliant message formatting ## Quick Start ### 1. Configure SMTP Settings ```go import "github.com/usememos/memos/plugin/email" config := &email.Config{ SMTPHost: "smtp.gmail.com", SMTPPort: 587, SMTPUsername: "your-email@gmail.com", SMTPPassword: "your-app-password", FromEmail: "noreply@yourdomain.com", FromName: "Memos", UseTLS: true, } ``` ### 2. Create and Send Email ```go message := &email.Message{ To: []string{"user@example.com"}, Subject: "Welcome to Memos!", Body: "Thanks for signing up.", IsHTML: false, } // Synchronous send (waits for result) err := email.Send(config, message) if err != nil { log.Printf("Failed to send email: %v", err) } // Asynchronous send (returns immediately) email.SendAsync(config, message) ``` ## Provider Configuration ### Gmail Requires an [App Password](https://support.google.com/accounts/answer/185833) (2FA must be enabled): ```go config := &email.Config{ SMTPHost: "smtp.gmail.com", SMTPPort: 587, SMTPUsername: "your-email@gmail.com", SMTPPassword: "your-16-char-app-password", FromEmail: "your-email@gmail.com", FromName: "Memos", UseTLS: true, } ``` **Alternative (SSL):** ```go config := &email.Config{ SMTPHost: "smtp.gmail.com", SMTPPort: 465, SMTPUsername: "your-email@gmail.com", SMTPPassword: "your-16-char-app-password", FromEmail: "your-email@gmail.com", FromName: "Memos", UseSSL: true, } ``` ### SendGrid ```go config := &email.Config{ SMTPHost: "smtp.sendgrid.net", SMTPPort: 587, SMTPUsername: "apikey", SMTPPassword: "your-sendgrid-api-key", FromEmail: "noreply@yourdomain.com", FromName: "Memos", UseTLS: true, } ``` ### AWS SES ```go config := &email.Config{ SMTPHost: "email-smtp.us-east-1.amazonaws.com", SMTPPort: 587, SMTPUsername: "your-smtp-username", SMTPPassword: "your-smtp-password", FromEmail: "verified@yourdomain.com", FromName: "Memos", UseTLS: true, } ``` **Note:** Replace `us-east-1` with your AWS region. Email must be verified in SES. ### Mailgun ```go config := &email.Config{ SMTPHost: "smtp.mailgun.org", SMTPPort: 587, SMTPUsername: "postmaster@yourdomain.com", SMTPPassword: "your-mailgun-smtp-password", FromEmail: "noreply@yourdomain.com", FromName: "Memos", UseTLS: true, } ``` ### Self-Hosted SMTP (Postfix, Exim, etc.) ```go config := &email.Config{ SMTPHost: "mail.yourdomain.com", SMTPPort: 587, SMTPUsername: "username", SMTPPassword: "password", FromEmail: "noreply@yourdomain.com", FromName: "Memos", UseTLS: true, } ``` ## HTML Emails ```go message := &email.Message{ To: []string{"user@example.com"}, Subject: "Welcome to Memos!", Body: `

Welcome to Memos!

We're excited to have you on board.

Get Started `, IsHTML: true, } email.Send(config, message) ``` ## Multiple Recipients ```go message := &email.Message{ To: []string{"user1@example.com", "user2@example.com"}, Cc: []string{"manager@example.com"}, Bcc: []string{"admin@example.com"}, Subject: "Team Update", Body: "Important team announcement...", ReplyTo: "support@yourdomain.com", } email.Send(config, message) ``` ## Testing ### Run Tests ```bash # All tests go test ./plugin/email/... -v # With coverage go test ./plugin/email/... -v -cover # With race detector go test ./plugin/email/... -race ``` ### Manual Testing Create a simple test program: ```go package main import ( "log" "github.com/usememos/memos/plugin/email" ) func main() { config := &email.Config{ SMTPHost: "smtp.gmail.com", SMTPPort: 587, SMTPUsername: "your-email@gmail.com", SMTPPassword: "your-app-password", FromEmail: "your-email@gmail.com", FromName: "Test", UseTLS: true, } message := &email.Message{ To: []string{"recipient@example.com"}, Subject: "Test Email", Body: "This is a test email from Memos email plugin.", } if err := email.Send(config, message); err != nil { log.Fatalf("Failed to send email: %v", err) } log.Println("Email sent successfully!") } ``` ## Security Best Practices ### 1. Use TLS/SSL Encryption Always enable encryption in production: ```go // STARTTLS (port 587) - Recommended config.UseTLS = true // SSL/TLS (port 465) config.UseSSL = true ``` ### 2. Secure Credential Storage Never hardcode credentials. Use environment variables: ```go import "os" config := &email.Config{ SMTPHost: os.Getenv("SMTP_HOST"), SMTPPort: 587, SMTPUsername: os.Getenv("SMTP_USERNAME"), SMTPPassword: os.Getenv("SMTP_PASSWORD"), FromEmail: os.Getenv("SMTP_FROM_EMAIL"), UseTLS: true, } ``` ### 3. Use App-Specific Passwords For Gmail and similar services, use app passwords instead of your main account password. ### 4. Validate and Sanitize Input Always validate email addresses and sanitize content: ```go // Validate before sending if err := message.Validate(); err != nil { return err } ``` ### 5. Implement Rate Limiting Prevent abuse by limiting email sending: ```go // Example using golang.org/x/time/rate limiter := rate.NewLimiter(rate.Every(time.Second), 10) // 10 emails per second if !limiter.Allow() { return errors.New("rate limit exceeded") } ``` ### 6. Monitor and Log Log email sending activity for security monitoring: ```go if err := email.Send(config, message); err != nil { slog.Error("Email send failed", slog.String("recipient", message.To[0]), slog.Any("error", err)) } ``` ## Common Ports | Port | Protocol | Security | Use Case | |------|----------|----------|----------| | **587** | SMTP + STARTTLS | Encrypted | **Recommended** for most providers | | **465** | SMTP over SSL/TLS | Encrypted | Alternative secure option | | **25** | SMTP | Unencrypted | Legacy, often blocked by ISPs | | **2525** | SMTP + STARTTLS | Encrypted | Alternative when 587 is blocked | **Port 587 (STARTTLS)** is the recommended standard for modern SMTP: ```go config := &email.Config{ SMTPPort: 587, UseTLS: true, } ``` **Port 465 (SSL/TLS)** is the alternative: ```go config := &email.Config{ SMTPPort: 465, UseSSL: true, } ``` ## Error Handling The package provides detailed, contextual errors: ```go err := email.Send(config, message) if err != nil { // Error messages include context: switch { case strings.Contains(err.Error(), "invalid email configuration"): // Configuration error (missing host, invalid port, etc.) log.Printf("Configuration error: %v", err) case strings.Contains(err.Error(), "invalid email message"): // Message validation error (missing recipients, subject, body) log.Printf("Message error: %v", err) case strings.Contains(err.Error(), "authentication failed"): // SMTP authentication failed (wrong credentials) log.Printf("Auth error: %v", err) case strings.Contains(err.Error(), "failed to connect"): // Network/connection error log.Printf("Connection error: %v", err) case strings.Contains(err.Error(), "recipient rejected"): // SMTP server rejected recipient log.Printf("Recipient error: %v", err) default: log.Printf("Unknown error: %v", err) } } ``` ### Common Error Messages ``` ❌ "invalid email configuration: SMTP host is required" → Fix: Set config.SMTPHost ❌ "invalid email configuration: SMTP port must be between 1 and 65535" → Fix: Set valid config.SMTPPort (usually 587 or 465) ❌ "invalid email configuration: from email is required" → Fix: Set config.FromEmail ❌ "invalid email message: at least one recipient is required" → Fix: Set message.To with at least one email address ❌ "invalid email message: subject is required" → Fix: Set message.Subject ❌ "invalid email message: body is required" → Fix: Set message.Body ❌ "SMTP authentication failed" → Fix: Check credentials (username/password) ❌ "failed to connect to SMTP server" → Fix: Verify host/port, check firewall, ensure TLS/SSL settings match server ``` ### Async Error Handling For async sending, errors are logged automatically: ```go email.SendAsync(config, message) // Errors logged as: // [WARN] Failed to send email asynchronously recipients=user@example.com error=... ``` ## Dependencies ### Required - **Go 1.25+** - Standard library: `net/smtp`, `crypto/tls` - `github.com/pkg/errors` - Error wrapping with context ### No External SMTP Libraries This plugin uses Go's standard `net/smtp` library for maximum compatibility and minimal dependencies. ## API Reference ### Types #### `Config` ```go type Config struct { SMTPHost string // SMTP server hostname SMTPPort int // SMTP server port SMTPUsername string // SMTP auth username SMTPPassword string // SMTP auth password FromEmail string // From email address FromName string // From display name (optional) UseTLS bool // Enable STARTTLS (port 587) UseSSL bool // Enable SSL/TLS (port 465) } ``` #### `Message` ```go type Message struct { To []string // Recipients Cc []string // CC recipients (optional) Bcc []string // BCC recipients (optional) Subject string // Email subject Body string // Email body (plain text or HTML) IsHTML bool // true for HTML, false for plain text ReplyTo string // Reply-To address (optional) } ``` ### Functions #### `Send(config *Config, message *Message) error` Sends an email synchronously. Blocks until email is sent or error occurs. #### `SendAsync(config *Config, message *Message)` Sends an email asynchronously in a goroutine. Returns immediately. Errors are logged. #### `NewClient(config *Config) *Client` Creates a new SMTP client for advanced usage. #### `Client.Send(message *Message) error` Sends email using the client's configuration. ## Architecture ``` plugin/email/ ├── config.go # SMTP configuration types ├── message.go # Email message types and formatting ├── client.go # SMTP client implementation ├── email.go # High-level Send/SendAsync API ├── doc.go # Package documentation └── *_test.go # Unit tests ``` ## License Part of the Memos project. See main repository for license details. ## Contributing This plugin follows the Memos contribution guidelines. Please ensure: 1. All code is tested (TDD approach) 2. Tests pass: `go test ./plugin/email/... -v` 3. Code is formatted: `go fmt ./plugin/email/...` 4. No linting errors: `golangci-lint run ./plugin/email/...` ## Support For issues and questions: - Memos GitHub Issues: https://github.com/usememos/memos/issues - Memos Documentation: https://usememos.com/docs ## Roadmap Future enhancements may include: - Email template system - Attachment support - Inline image embedding - Email queuing system - Delivery status tracking - Bounce handling ================================================ FILE: plugin/email/client.go ================================================ package email import ( "crypto/tls" "net/smtp" "github.com/pkg/errors" ) // Client represents an SMTP email client. type Client struct { config *Config } // NewClient creates a new email client with the given configuration. func NewClient(config *Config) *Client { return &Client{ config: config, } } // validateConfig validates the client configuration. func (c *Client) validateConfig() error { if c.config == nil { return errors.New("email configuration is required") } return c.config.Validate() } // createAuth creates an SMTP auth mechanism if credentials are provided. func (c *Client) createAuth() smtp.Auth { if c.config.SMTPUsername == "" && c.config.SMTPPassword == "" { return nil } return smtp.PlainAuth("", c.config.SMTPUsername, c.config.SMTPPassword, c.config.SMTPHost) } // createTLSConfig creates a TLS configuration for secure connections. func (c *Client) createTLSConfig() *tls.Config { return &tls.Config{ ServerName: c.config.SMTPHost, MinVersion: tls.VersionTLS12, } } // Send sends an email message via SMTP. func (c *Client) Send(message *Message) error { // Validate configuration if err := c.validateConfig(); err != nil { return errors.Wrap(err, "invalid email configuration") } // Validate message if message == nil { return errors.New("message is required") } if err := message.Validate(); err != nil { return errors.Wrap(err, "invalid email message") } // Format the message body := message.Format(c.config.FromEmail, c.config.FromName) // Get all recipients recipients := message.GetAllRecipients() // Create auth auth := c.createAuth() // Send based on encryption type if c.config.UseSSL { return c.sendWithSSL(auth, recipients, body) } return c.sendWithTLS(auth, recipients, body) } // sendWithTLS sends email using STARTTLS (port 587). func (c *Client) sendWithTLS(auth smtp.Auth, recipients []string, body string) error { serverAddr := c.config.GetServerAddress() if c.config.UseTLS { // Use STARTTLS return smtp.SendMail(serverAddr, auth, c.config.FromEmail, recipients, []byte(body)) } // Send without encryption (not recommended) return smtp.SendMail(serverAddr, auth, c.config.FromEmail, recipients, []byte(body)) } // sendWithSSL sends email using SSL/TLS (port 465). func (c *Client) sendWithSSL(auth smtp.Auth, recipients []string, body string) error { serverAddr := c.config.GetServerAddress() // Create TLS connection tlsConfig := c.createTLSConfig() conn, err := tls.Dial("tcp", serverAddr, tlsConfig) if err != nil { return errors.Wrapf(err, "failed to connect to SMTP server with SSL: %s", serverAddr) } defer conn.Close() // Create SMTP client client, err := smtp.NewClient(conn, c.config.SMTPHost) if err != nil { return errors.Wrap(err, "failed to create SMTP client") } defer client.Quit() // Authenticate if auth != nil { if err := client.Auth(auth); err != nil { return errors.Wrap(err, "SMTP authentication failed") } } // Set sender if err := client.Mail(c.config.FromEmail); err != nil { return errors.Wrap(err, "failed to set sender") } // Set recipients for _, recipient := range recipients { if err := client.Rcpt(recipient); err != nil { return errors.Wrapf(err, "failed to set recipient: %s", recipient) } } // Send message body writer, err := client.Data() if err != nil { return errors.Wrap(err, "failed to send DATA command") } if _, err := writer.Write([]byte(body)); err != nil { return errors.Wrap(err, "failed to write message body") } if err := writer.Close(); err != nil { return errors.Wrap(err, "failed to close message writer") } return nil } ================================================ FILE: plugin/email/client_test.go ================================================ package email import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewClient(t *testing.T) { config := &Config{ SMTPHost: "smtp.example.com", SMTPPort: 587, SMTPUsername: "user@example.com", SMTPPassword: "password", FromEmail: "noreply@example.com", FromName: "Test App", UseTLS: true, } client := NewClient(config) assert.NotNil(t, client) assert.Equal(t, config, client.config) } func TestClientValidateConfig(t *testing.T) { tests := []struct { name string config *Config wantErr bool }{ { name: "valid config", config: &Config{ SMTPHost: "smtp.example.com", SMTPPort: 587, FromEmail: "test@example.com", }, wantErr: false, }, { name: "nil config", config: nil, wantErr: true, }, { name: "invalid config", config: &Config{ SMTPHost: "", SMTPPort: 587, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := NewClient(tt.config) err := client.validateConfig() if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestClientSendValidation(t *testing.T) { config := &Config{ SMTPHost: "smtp.example.com", SMTPPort: 587, FromEmail: "test@example.com", } client := NewClient(config) tests := []struct { name string message *Message wantErr bool }{ { name: "valid message", message: &Message{ To: []string{"recipient@example.com"}, Subject: "Test", Body: "Test body", }, wantErr: false, // Will fail on actual send, but passes validation }, { name: "nil message", message: nil, wantErr: true, }, { name: "invalid message", message: &Message{ To: []string{}, Subject: "Test", Body: "Test", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := client.Send(tt.message) // We expect validation errors for invalid messages // For valid messages, we'll get connection errors (which is expected in tests) if tt.wantErr { assert.Error(t, err) // Should fail validation before attempting connection assert.NotContains(t, err.Error(), "dial") } // Note: We don't assert NoError for valid messages because // we don't have a real SMTP server in tests }) } } ================================================ FILE: plugin/email/config.go ================================================ package email import ( "fmt" "github.com/pkg/errors" ) // Config represents the SMTP configuration for email sending. // These settings should be provided by the self-hosted instance administrator. type Config struct { // SMTPHost is the SMTP server hostname (e.g., "smtp.gmail.com") SMTPHost string // SMTPPort is the SMTP server port (common: 587 for TLS, 465 for SSL, 25 for unencrypted) SMTPPort int // SMTPUsername is the SMTP authentication username (usually the email address) SMTPUsername string // SMTPPassword is the SMTP authentication password or app-specific password SMTPPassword string // FromEmail is the email address that will appear in the "From" field FromEmail string // FromName is the display name that will appear in the "From" field FromName string // UseTLS enables STARTTLS encryption (recommended for port 587) UseTLS bool // UseSSL enables SSL/TLS encryption (for port 465) UseSSL bool } // Validate checks if the configuration is valid. func (c *Config) Validate() error { if c.SMTPHost == "" { return errors.New("SMTP host is required") } if c.SMTPPort <= 0 || c.SMTPPort > 65535 { return errors.New("SMTP port must be between 1 and 65535") } if c.FromEmail == "" { return errors.New("from email is required") } return nil } // GetServerAddress returns the SMTP server address in the format "host:port". func (c *Config) GetServerAddress() string { return fmt.Sprintf("%s:%d", c.SMTPHost, c.SMTPPort) } ================================================ FILE: plugin/email/config_test.go ================================================ package email import ( "testing" "github.com/stretchr/testify/assert" ) func TestConfigValidation(t *testing.T) { tests := []struct { name string config *Config wantErr bool }{ { name: "valid config", config: &Config{ SMTPHost: "smtp.gmail.com", SMTPPort: 587, SMTPUsername: "user@example.com", SMTPPassword: "password", FromEmail: "noreply@example.com", FromName: "Memos", }, wantErr: false, }, { name: "missing host", config: &Config{ SMTPPort: 587, SMTPUsername: "user@example.com", SMTPPassword: "password", FromEmail: "noreply@example.com", }, wantErr: true, }, { name: "invalid port", config: &Config{ SMTPHost: "smtp.gmail.com", SMTPPort: 0, SMTPUsername: "user@example.com", SMTPPassword: "password", FromEmail: "noreply@example.com", }, wantErr: true, }, { name: "missing from email", config: &Config{ SMTPHost: "smtp.gmail.com", SMTPPort: 587, SMTPUsername: "user@example.com", SMTPPassword: "password", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.config.Validate() if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestConfigGetServerAddress(t *testing.T) { config := &Config{ SMTPHost: "smtp.gmail.com", SMTPPort: 587, } expected := "smtp.gmail.com:587" assert.Equal(t, expected, config.GetServerAddress()) } ================================================ FILE: plugin/email/doc.go ================================================ // Package email provides SMTP email sending functionality for self-hosted Memos instances. // // This package is designed for self-hosted environments where instance administrators // configure their own SMTP servers. It follows industry-standard patterns used by // platforms like GitHub, GitLab, and Discourse. // // # Configuration // // The package requires SMTP server configuration provided by the instance administrator: // // config := &email.Config{ // SMTPHost: "smtp.gmail.com", // SMTPPort: 587, // SMTPUsername: "your-email@gmail.com", // SMTPPassword: "your-app-password", // FromEmail: "noreply@yourdomain.com", // FromName: "Memos Notifications", // UseTLS: true, // } // // # Common SMTP Settings // // Gmail (requires App Password): // - Host: smtp.gmail.com // - Port: 587 (TLS) or 465 (SSL) // - Username: your-email@gmail.com // - UseTLS: true (for port 587) or UseSSL: true (for port 465) // // SendGrid: // - Host: smtp.sendgrid.net // - Port: 587 // - Username: apikey // - Password: your-sendgrid-api-key // - UseTLS: true // // AWS SES: // - Host: email-smtp.[region].amazonaws.com // - Port: 587 // - Username: your-smtp-username // - Password: your-smtp-password // - UseTLS: true // // Mailgun: // - Host: smtp.mailgun.org // - Port: 587 // - Username: your-mailgun-smtp-username // - Password: your-mailgun-smtp-password // - UseTLS: true // // # Sending Email // // Synchronous (waits for completion): // // message := &email.Message{ // To: []string{"user@example.com"}, // Subject: "Welcome to Memos", // Body: "Thank you for joining!", // IsHTML: false, // } // // err := email.Send(config, message) // if err != nil { // // Handle error // } // // Asynchronous (returns immediately): // // email.SendAsync(config, message) // // Errors are logged but not returned // // # HTML Email // // message := &email.Message{ // To: []string{"user@example.com"}, // Subject: "Welcome!", // Body: "

Welcome to Memos!

", // IsHTML: true, // } // // # Security Considerations // // - Always use TLS (port 587) or SSL (port 465) for production // - Store SMTP credentials securely (environment variables or secrets management) // - Use app-specific passwords for services like Gmail // - Validate and sanitize email content to prevent injection attacks // - Rate limit email sending to prevent abuse // // # Error Handling // // The package returns descriptive errors for common issues: // - Configuration validation errors (missing host, invalid port, etc.) // - Message validation errors (missing recipients, subject, or body) // - Connection errors (cannot reach SMTP server) // - Authentication errors (invalid credentials) // - SMTP protocol errors (recipient rejected, etc.) // // All errors are wrapped with context using github.com/pkg/errors for better debugging. package email ================================================ FILE: plugin/email/email.go ================================================ package email import ( "log/slog" "github.com/pkg/errors" ) // Send sends an email synchronously. // Returns an error if the email fails to send. func Send(config *Config, message *Message) error { if config == nil { return errors.New("email configuration is required") } if message == nil { return errors.New("email message is required") } client := NewClient(config) return client.Send(message) } // SendAsync sends an email asynchronously. // It spawns a new goroutine to handle the sending and does not wait for the response. // Any errors are logged but not returned. func SendAsync(config *Config, message *Message) { go func() { if err := Send(config, message); err != nil { // Since we're in a goroutine, we can only log the error recipients := "" if message != nil && len(message.To) > 0 { recipients = message.To[0] if len(message.To) > 1 { recipients += " and others" } } slog.Warn("Failed to send email asynchronously", slog.String("recipients", recipients), slog.Any("error", err)) } }() } ================================================ FILE: plugin/email/email_test.go ================================================ package email import ( "testing" "time" "github.com/stretchr/testify/assert" "golang.org/x/sync/errgroup" ) func TestSend(t *testing.T) { config := &Config{ SMTPHost: "smtp.example.com", SMTPPort: 587, FromEmail: "test@example.com", } message := &Message{ To: []string{"recipient@example.com"}, Subject: "Test", Body: "Test body", } // This will fail to connect (no real server), but should validate inputs err := Send(config, message) // We expect an error because there's no real SMTP server // But it should be a connection error, not a validation error assert.Error(t, err) assert.Contains(t, err.Error(), "dial") } func TestSendValidation(t *testing.T) { tests := []struct { name string config *Config message *Message wantErr bool errMsg string }{ { name: "nil config", config: nil, message: &Message{To: []string{"test@example.com"}, Subject: "Test", Body: "Test"}, wantErr: true, errMsg: "configuration is required", }, { name: "nil message", config: &Config{SMTPHost: "smtp.example.com", SMTPPort: 587, FromEmail: "from@example.com"}, message: nil, wantErr: true, errMsg: "message is required", }, { name: "invalid config", config: &Config{ SMTPHost: "", SMTPPort: 587, }, message: &Message{To: []string{"test@example.com"}, Subject: "Test", Body: "Test"}, wantErr: true, errMsg: "invalid email configuration", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := Send(tt.config, tt.message) if tt.wantErr { assert.Error(t, err) assert.Contains(t, err.Error(), tt.errMsg) } }) } } func TestSendAsync(t *testing.T) { config := &Config{ SMTPHost: "smtp.example.com", SMTPPort: 587, FromEmail: "test@example.com", } message := &Message{ To: []string{"recipient@example.com"}, Subject: "Test Async", Body: "Test async body", } // SendAsync should not block start := time.Now() SendAsync(config, message) duration := time.Since(start) // Should return almost immediately (< 100ms) assert.Less(t, duration, 100*time.Millisecond) // Give goroutine time to start time.Sleep(50 * time.Millisecond) } func TestSendAsyncConcurrent(t *testing.T) { config := &Config{ SMTPHost: "smtp.example.com", SMTPPort: 587, FromEmail: "test@example.com", } g := errgroup.Group{} count := 5 for i := 0; i < count; i++ { g.Go(func() error { message := &Message{ To: []string{"recipient@example.com"}, Subject: "Concurrent Test", Body: "Test body", } SendAsync(config, message) return nil }) } if err := g.Wait(); err != nil { t.Fatalf("SendAsync calls failed: %v", err) } } ================================================ FILE: plugin/email/message.go ================================================ package email import ( "errors" "fmt" "strings" "time" ) // Message represents an email message to be sent. type Message struct { To []string // Required: recipient email addresses Cc []string // Optional: carbon copy recipients Bcc []string // Optional: blind carbon copy recipients Subject string // Required: email subject Body string // Required: email body content IsHTML bool // Whether the body is HTML (default: false for plain text) ReplyTo string // Optional: reply-to address } // Validate checks that the message has all required fields. func (m *Message) Validate() error { if len(m.To) == 0 { return errors.New("at least one recipient is required") } if m.Subject == "" { return errors.New("subject is required") } if m.Body == "" { return errors.New("body is required") } return nil } // Format creates an RFC 5322 formatted email message. func (m *Message) Format(fromEmail, fromName string) string { var sb strings.Builder // From header if fromName != "" { fmt.Fprintf(&sb, "From: %s <%s>\r\n", fromName, fromEmail) } else { fmt.Fprintf(&sb, "From: %s\r\n", fromEmail) } // To header fmt.Fprintf(&sb, "To: %s\r\n", strings.Join(m.To, ", ")) // Cc header (optional) if len(m.Cc) > 0 { fmt.Fprintf(&sb, "Cc: %s\r\n", strings.Join(m.Cc, ", ")) } // Reply-To header (optional) if m.ReplyTo != "" { fmt.Fprintf(&sb, "Reply-To: %s\r\n", m.ReplyTo) } // Subject header fmt.Fprintf(&sb, "Subject: %s\r\n", m.Subject) // Date header (RFC 5322 format) fmt.Fprintf(&sb, "Date: %s\r\n", time.Now().Format(time.RFC1123Z)) // MIME headers sb.WriteString("MIME-Version: 1.0\r\n") // Content-Type header if m.IsHTML { sb.WriteString("Content-Type: text/html; charset=utf-8\r\n") } else { sb.WriteString("Content-Type: text/plain; charset=utf-8\r\n") } // Empty line separating headers from body sb.WriteString("\r\n") // Body sb.WriteString(m.Body) return sb.String() } // GetAllRecipients returns all recipients (To, Cc, Bcc) as a single slice. func (m *Message) GetAllRecipients() []string { var recipients []string recipients = append(recipients, m.To...) recipients = append(recipients, m.Cc...) recipients = append(recipients, m.Bcc...) return recipients } ================================================ FILE: plugin/email/message_test.go ================================================ package email import ( "strings" "testing" ) func TestMessageValidation(t *testing.T) { tests := []struct { name string msg Message wantErr bool }{ { name: "valid message", msg: Message{ To: []string{"user@example.com"}, Subject: "Test Subject", Body: "Test Body", }, wantErr: false, }, { name: "no recipients", msg: Message{ To: []string{}, Subject: "Test Subject", Body: "Test Body", }, wantErr: true, }, { name: "no subject", msg: Message{ To: []string{"user@example.com"}, Subject: "", Body: "Test Body", }, wantErr: true, }, { name: "no body", msg: Message{ To: []string{"user@example.com"}, Subject: "Test Subject", Body: "", }, wantErr: true, }, { name: "multiple recipients", msg: Message{ To: []string{"user1@example.com", "user2@example.com"}, Cc: []string{"cc@example.com"}, Subject: "Test Subject", Body: "Test Body", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.msg.Validate() if (err != nil) != tt.wantErr { t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestMessageFormatPlainText(t *testing.T) { msg := Message{ To: []string{"user@example.com"}, Subject: "Test Subject", Body: "Test Body", IsHTML: false, } formatted := msg.Format("sender@example.com", "Sender Name") // Check required headers if !strings.Contains(formatted, "From: Sender Name ") { t.Error("Missing or incorrect From header") } if !strings.Contains(formatted, "To: user@example.com") { t.Error("Missing or incorrect To header") } if !strings.Contains(formatted, "Subject: Test Subject") { t.Error("Missing or incorrect Subject header") } if !strings.Contains(formatted, "Content-Type: text/plain; charset=utf-8") { t.Error("Missing or incorrect Content-Type header for plain text") } if !strings.Contains(formatted, "Test Body") { t.Error("Missing message body") } } func TestMessageFormatHTML(t *testing.T) { msg := Message{ To: []string{"user@example.com"}, Subject: "Test Subject", Body: "Test Body", IsHTML: true, } formatted := msg.Format("sender@example.com", "Sender Name") // Check HTML content-type if !strings.Contains(formatted, "Content-Type: text/html; charset=utf-8") { t.Error("Missing or incorrect Content-Type header for HTML") } if !strings.Contains(formatted, "Test Body") { t.Error("Missing HTML body") } } func TestMessageFormatMultipleRecipients(t *testing.T) { msg := Message{ To: []string{"user1@example.com", "user2@example.com"}, Cc: []string{"cc1@example.com", "cc2@example.com"}, Bcc: []string{"bcc@example.com"}, Subject: "Test Subject", Body: "Test Body", ReplyTo: "reply@example.com", } formatted := msg.Format("sender@example.com", "Sender Name") // Check To header formatting if !strings.Contains(formatted, "To: user1@example.com, user2@example.com") { t.Error("Missing or incorrect To header with multiple recipients") } // Check Cc header formatting if !strings.Contains(formatted, "Cc: cc1@example.com, cc2@example.com") { t.Error("Missing or incorrect Cc header") } // Bcc should NOT appear in the formatted message if strings.Contains(formatted, "Bcc:") { t.Error("Bcc header should not appear in formatted message") } // Check Reply-To header if !strings.Contains(formatted, "Reply-To: reply@example.com") { t.Error("Missing or incorrect Reply-To header") } } func TestGetAllRecipients(t *testing.T) { msg := Message{ To: []string{"user1@example.com", "user2@example.com"}, Cc: []string{"cc@example.com"}, Bcc: []string{"bcc@example.com"}, } recipients := msg.GetAllRecipients() // Should have all 4 recipients if len(recipients) != 4 { t.Errorf("GetAllRecipients() returned %d recipients, want 4", len(recipients)) } // Check all recipients are present expectedRecipients := map[string]bool{ "user1@example.com": true, "user2@example.com": true, "cc@example.com": true, "bcc@example.com": true, } for _, recipient := range recipients { if !expectedRecipients[recipient] { t.Errorf("Unexpected recipient: %s", recipient) } delete(expectedRecipients, recipient) } if len(expectedRecipients) > 0 { t.Error("Not all expected recipients were returned") } } ================================================ FILE: plugin/filter/MAINTENANCE.md ================================================ # Maintaining the Memo Filter Engine The engine is memo-specific; any future field or behavior changes must stay consistent with the memo schema and store implementations. Use this guide when extending or debugging the package. ## Adding a New Memo Field 1. **Update the schema** - Add the field entry in `schema.go`. - Define the backing column (`Column`), JSON path (if applicable), type, and allowed operators. - Include the CEL variable in `EnvOptions`. 2. **Adjust parser or renderer (if needed)** - For non-scalar fields (JSON booleans, lists), add handling in `parser.go` or extend the renderer helpers. - Keep validation in the parser (e.g., reject unsupported operators). 3. **Write a golden test** - Extend the dialect-specific memo filter tests under `store/db/{sqlite,mysql,postgres}/memo_filter_test.go` with a case that exercises the new field. 4. **Run `go test ./...`** to ensure the SQL output matches expectations across all dialects. ## Supporting Dialect Nuances - Centralize differences inside `render.go`. If a new dialect-specific behavior emerges (e.g., JSON operators), add the logic there rather than leaking it into store code. - Use the renderer helpers (`jsonExtractExpr`, `jsonArrayExpr`, etc.) rather than sprinkling ad-hoc SQL strings. - When placeholders change, adjust `addArg` so that argument numbering stays in sync with store queries. ## Debugging Tips - **Parser errors** – Most originate in `buildCondition` or schema validation. Enable logging around `parser.go` when diagnosing unknown identifier/operator messages. - **Renderer output** – Temporary printf/log statements in `renderCondition` help identify which IR node produced unexpected SQL. - **Store integration** – Ensure drivers call `filter.DefaultEngine()` exactly once per process; the singleton caches the parsed CEL environment. ## Testing Checklist - `go test ./store/...` ensures all dialect tests consume the engine correctly. - Add targeted unit tests whenever new IR nodes or renderer paths are introduced. - When changing boolean or JSON handling, verify all three dialect test suites (SQLite, MySQL, Postgres) to avoid regression. ================================================ FILE: plugin/filter/README.md ================================================ # Memo Filter Engine This package houses the memo-only filter engine that turns CEL expressions into SQL fragments. The engine follows a three phase pipeline inspired by systems such as Calcite or Prisma: 1. **Parsing** – CEL expressions are parsed with `cel-go` and validated against the memo-specific environment declared in `schema.go`. Only fields that exist in the schema can surface in the filter. 2. **Normalization** – the raw CEL AST is converted into an intermediate representation (IR) defined in `ir.go`. The IR is a dialect-agnostic tree of conditions (logical operators, comparisons, list membership, etc.). This step enforces schema rules (e.g. operator compatibility, type checks). 3. **Rendering** – the renderer in `render.go` walks the IR and produces a SQL fragment plus placeholder arguments tailored to a target dialect (`sqlite`, `mysql`, or `postgres`). Dialect differences such as JSON access, boolean semantics, placeholders, and `LIKE` vs `ILIKE` are encapsulated in renderer helpers. The entry point is `filter.DefaultEngine()` from `engine.go`. It lazily constructs an `Engine` configured with the memo schema and exposes: ```go engine, _ := filter.DefaultEngine() stmt, _ := engine.CompileToStatement(ctx, `has_task_list && visibility == "PUBLIC"`, filter.RenderOptions{ Dialect: filter.DialectPostgres, }) // stmt.SQL -> "((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE AND memo.visibility = $1)" // stmt.Args -> ["PUBLIC"] ``` ## Core Files | File | Responsibility | | ------------- | ------------------------------------------------------------------------------- | | `schema.go` | Declares memo fields, their types, backing columns, CEL environment options | | `ir.go` | IR node definitions used across the pipeline | | `parser.go` | Converts CEL `Expr` into IR while applying schema validation | | `render.go` | Translates IR into SQL, handling dialect-specific behavior | | `engine.go` | Glue between the phases; exposes `Compile`, `CompileToStatement`, and `DefaultEngine` | | `helpers.go` | Convenience helpers for store integration (appending conditions) | ## SQL Generation Notes - **Placeholders** — `?` is used for SQLite/MySQL, `$n` for Postgres. The renderer tracks offsets to compose queries with pre-existing arguments. - **JSON Fields** — Memo metadata lives in `memo.payload`. The renderer handles `JSON_EXTRACT`/`json_extract`/`->`/`->>` variations and boolean coercion. - **Tag Operations** — `tag in [...]` and `"tag" in tags` become JSON array predicates. SQLite uses `LIKE` patterns, MySQL uses `JSON_CONTAINS`, and Postgres uses `@>`. - **Boolean Flags** — Fields such as `has_task_list` render as `IS TRUE` equality checks, or comparisons against `CAST('true' AS JSON)` depending on the dialect. ## Typical Integration 1. Fetch the engine with `filter.DefaultEngine()`. 2. Call `CompileToStatement` using the appropriate dialect enum. 3. Append the emitted SQL fragment/args to the existing `WHERE` clause. 4. Execute the resulting query through the store driver. The `helpers.AppendConditions` helper encapsulates steps 2–3 when a driver needs to process an array of filters. ================================================ FILE: plugin/filter/engine.go ================================================ package filter import ( "context" "fmt" "strings" "sync" "github.com/google/cel-go/cel" "github.com/pkg/errors" ) // Engine parses CEL filters into a dialect-agnostic condition tree. type Engine struct { schema Schema env *cel.Env } // NewEngine builds a new Engine for the provided schema. func NewEngine(schema Schema) (*Engine, error) { env, err := cel.NewEnv(schema.EnvOptions...) if err != nil { return nil, errors.Wrap(err, "failed to create CEL environment") } return &Engine{ schema: schema, env: env, }, nil } // Program stores a compiled filter condition. type Program struct { schema Schema condition Condition } // ConditionTree exposes the underlying condition tree. func (p *Program) ConditionTree() Condition { return p.condition } // Compile parses the filter string into an executable program. func (e *Engine) Compile(_ context.Context, filter string) (*Program, error) { if strings.TrimSpace(filter) == "" { return nil, errors.New("filter expression is empty") } filter = normalizeLegacyFilter(filter) ast, issues := e.env.Compile(filter) if issues != nil && issues.Err() != nil { return nil, errors.Wrap(issues.Err(), "failed to compile filter") } parsed, err := cel.AstToParsedExpr(ast) if err != nil { return nil, errors.Wrap(err, "failed to convert AST") } cond, err := buildCondition(parsed.GetExpr(), e.schema) if err != nil { return nil, err } return &Program{ schema: e.schema, condition: cond, }, nil } // CompileToStatement compiles and renders the filter in a single step. func (e *Engine) CompileToStatement(ctx context.Context, filter string, opts RenderOptions) (Statement, error) { program, err := e.Compile(ctx, filter) if err != nil { return Statement{}, err } return program.Render(opts) } // RenderOptions configure SQL rendering. type RenderOptions struct { Dialect DialectName PlaceholderOffset int DisableNullChecks bool } // Statement contains the rendered SQL fragment and its args. type Statement struct { SQL string Args []any } // Render converts the program into a dialect-specific SQL fragment. func (p *Program) Render(opts RenderOptions) (Statement, error) { renderer := newRenderer(p.schema, opts) return renderer.Render(p.condition) } var ( defaultOnce sync.Once defaultInst *Engine defaultErr error defaultAttachmentOnce sync.Once defaultAttachmentInst *Engine defaultAttachmentErr error ) // DefaultEngine returns the process-wide memo filter engine. func DefaultEngine() (*Engine, error) { defaultOnce.Do(func() { defaultInst, defaultErr = NewEngine(NewSchema()) }) return defaultInst, defaultErr } // DefaultAttachmentEngine returns the process-wide attachment filter engine. func DefaultAttachmentEngine() (*Engine, error) { defaultAttachmentOnce.Do(func() { defaultAttachmentInst, defaultAttachmentErr = NewEngine(NewAttachmentSchema()) }) return defaultAttachmentInst, defaultAttachmentErr } func normalizeLegacyFilter(expr string) string { expr = rewriteNumericLogicalOperand(expr, "&&") expr = rewriteNumericLogicalOperand(expr, "||") return expr } func rewriteNumericLogicalOperand(expr, op string) string { var builder strings.Builder n := len(expr) i := 0 var inQuote rune for i < n { ch := expr[i] if inQuote != 0 { builder.WriteByte(ch) if ch == '\\' && i+1 < n { builder.WriteByte(expr[i+1]) i += 2 continue } if ch == byte(inQuote) { inQuote = 0 } i++ continue } if ch == '\'' || ch == '"' { inQuote = rune(ch) builder.WriteByte(ch) i++ continue } if strings.HasPrefix(expr[i:], op) { builder.WriteString(op) i += len(op) // Preserve whitespace following the operator. wsStart := i for i < n && (expr[i] == ' ' || expr[i] == '\t') { i++ } builder.WriteString(expr[wsStart:i]) signStart := i if i < n && (expr[i] == '+' || expr[i] == '-') { i++ } for i < n && expr[i] >= '0' && expr[i] <= '9' { i++ } if i > signStart { numLiteral := expr[signStart:i] fmt.Fprintf(&builder, "(%s != 0)", numLiteral) } else { builder.WriteString(expr[signStart:i]) } continue } builder.WriteByte(ch) i++ } return builder.String() } ================================================ FILE: plugin/filter/helpers.go ================================================ package filter import ( "context" "fmt" ) // AppendConditions compiles the provided filters and appends the resulting SQL fragments and args. func AppendConditions(ctx context.Context, engine *Engine, filters []string, dialect DialectName, where *[]string, args *[]any) error { for _, filterStr := range filters { stmt, err := engine.CompileToStatement(ctx, filterStr, RenderOptions{ Dialect: dialect, PlaceholderOffset: len(*args), }) if err != nil { return err } if stmt.SQL == "" { continue } *where = append(*where, fmt.Sprintf("(%s)", stmt.SQL)) *args = append(*args, stmt.Args...) } return nil } ================================================ FILE: plugin/filter/ir.go ================================================ package filter // Condition represents a boolean expression derived from the CEL filter. type Condition interface { isCondition() } // LogicalOperator enumerates the supported logical operators. type LogicalOperator string const ( LogicalAnd LogicalOperator = "AND" LogicalOr LogicalOperator = "OR" ) // LogicalCondition composes two conditions with a logical operator. type LogicalCondition struct { Operator LogicalOperator Left Condition Right Condition } func (*LogicalCondition) isCondition() {} // NotCondition negates a child condition. type NotCondition struct { Expr Condition } func (*NotCondition) isCondition() {} // FieldPredicateCondition asserts that a field evaluates to true. type FieldPredicateCondition struct { Field string } func (*FieldPredicateCondition) isCondition() {} // ComparisonOperator lists supported comparison operators. type ComparisonOperator string const ( CompareEq ComparisonOperator = "=" CompareNeq ComparisonOperator = "!=" CompareLt ComparisonOperator = "<" CompareLte ComparisonOperator = "<=" CompareGt ComparisonOperator = ">" CompareGte ComparisonOperator = ">=" ) // ComparisonCondition represents a binary comparison. type ComparisonCondition struct { Left ValueExpr Operator ComparisonOperator Right ValueExpr } func (*ComparisonCondition) isCondition() {} // InCondition represents an IN predicate with literal list values. type InCondition struct { Left ValueExpr Values []ValueExpr } func (*InCondition) isCondition() {} // ElementInCondition represents the CEL syntax `"value" in field`. type ElementInCondition struct { Element ValueExpr Field string } func (*ElementInCondition) isCondition() {} // ContainsCondition models the .contains() call. type ContainsCondition struct { Field string Value string } func (*ContainsCondition) isCondition() {} // ConstantCondition captures a literal boolean outcome. type ConstantCondition struct { Value bool } func (*ConstantCondition) isCondition() {} // ValueExpr models arithmetic or scalar expressions whose result feeds a comparison. type ValueExpr interface { isValueExpr() } // FieldRef references a named schema field. type FieldRef struct { Name string } func (*FieldRef) isValueExpr() {} // LiteralValue holds a literal scalar. type LiteralValue struct { Value interface{} } func (*LiteralValue) isValueExpr() {} // FunctionValue captures simple function calls like size(tags). type FunctionValue struct { Name string Args []ValueExpr } func (*FunctionValue) isValueExpr() {} // ListComprehensionCondition represents CEL macros like exists(), all(), filter(). type ListComprehensionCondition struct { Kind ComprehensionKind Field string // The list field to iterate over (e.g., "tags") IterVar string // The iteration variable name (e.g., "t") Predicate PredicateExpr // The predicate to evaluate for each element } func (*ListComprehensionCondition) isCondition() {} // ComprehensionKind enumerates the types of list comprehensions. type ComprehensionKind string const ( ComprehensionExists ComprehensionKind = "exists" ) // PredicateExpr represents predicates used in comprehensions. type PredicateExpr interface { isPredicateExpr() } // StartsWithPredicate represents t.startsWith("prefix"). type StartsWithPredicate struct { Prefix string } func (*StartsWithPredicate) isPredicateExpr() {} // EndsWithPredicate represents t.endsWith("suffix"). type EndsWithPredicate struct { Suffix string } func (*EndsWithPredicate) isPredicateExpr() {} // ContainsPredicate represents t.contains("substring"). type ContainsPredicate struct { Substring string } func (*ContainsPredicate) isPredicateExpr() {} ================================================ FILE: plugin/filter/parser.go ================================================ package filter import ( "time" "github.com/pkg/errors" exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) func buildCondition(expr *exprv1.Expr, schema Schema) (Condition, error) { switch v := expr.ExprKind.(type) { case *exprv1.Expr_CallExpr: return buildCallCondition(v.CallExpr, schema) case *exprv1.Expr_ConstExpr: val, err := getConstValue(expr) if err != nil { return nil, err } switch v := val.(type) { case bool: return &ConstantCondition{Value: v}, nil case int64: return &ConstantCondition{Value: v != 0}, nil case float64: return &ConstantCondition{Value: v != 0}, nil default: return nil, errors.New("filter must evaluate to a boolean value") } case *exprv1.Expr_IdentExpr: name := v.IdentExpr.GetName() field, ok := schema.Field(name) if !ok { return nil, errors.Errorf("unknown identifier %q", name) } if field.Type != FieldTypeBool { return nil, errors.Errorf("identifier %q is not boolean", name) } return &FieldPredicateCondition{Field: name}, nil case *exprv1.Expr_ComprehensionExpr: return buildComprehensionCondition(v.ComprehensionExpr, schema) default: return nil, errors.New("unsupported top-level expression") } } func buildCallCondition(call *exprv1.Expr_Call, schema Schema) (Condition, error) { switch call.Function { case "_&&_": if len(call.Args) != 2 { return nil, errors.New("logical AND expects two arguments") } left, err := buildCondition(call.Args[0], schema) if err != nil { return nil, err } right, err := buildCondition(call.Args[1], schema) if err != nil { return nil, err } return &LogicalCondition{ Operator: LogicalAnd, Left: left, Right: right, }, nil case "_||_": if len(call.Args) != 2 { return nil, errors.New("logical OR expects two arguments") } left, err := buildCondition(call.Args[0], schema) if err != nil { return nil, err } right, err := buildCondition(call.Args[1], schema) if err != nil { return nil, err } return &LogicalCondition{ Operator: LogicalOr, Left: left, Right: right, }, nil case "!_": if len(call.Args) != 1 { return nil, errors.New("logical NOT expects one argument") } child, err := buildCondition(call.Args[0], schema) if err != nil { return nil, err } return &NotCondition{Expr: child}, nil case "_==_", "_!=_", "_<_", "_>_", "_<=_", "_>=_": return buildComparisonCondition(call, schema) case "@in": return buildInCondition(call, schema) case "contains": return buildContainsCondition(call, schema) default: val, ok, err := evaluateBool(call) if err != nil { return nil, err } if ok { return &ConstantCondition{Value: val}, nil } return nil, errors.Errorf("unsupported call expression %q", call.Function) } } func buildComparisonCondition(call *exprv1.Expr_Call, schema Schema) (Condition, error) { if len(call.Args) != 2 { return nil, errors.New("comparison expects two arguments") } op, err := toComparisonOperator(call.Function) if err != nil { return nil, err } left, err := buildValueExpr(call.Args[0], schema) if err != nil { return nil, err } right, err := buildValueExpr(call.Args[1], schema) if err != nil { return nil, err } // If the left side is a field, validate allowed operators. if field, ok := left.(*FieldRef); ok { def, exists := schema.Field(field.Name) if !exists { return nil, errors.Errorf("unknown identifier %q", field.Name) } if def.Kind == FieldKindVirtualAlias { def, exists = schema.ResolveAlias(field.Name) if !exists { return nil, errors.Errorf("invalid alias %q", field.Name) } } if def.AllowedComparisonOps != nil { if _, allowed := def.AllowedComparisonOps[op]; !allowed { return nil, errors.Errorf("operator %s not allowed for field %q", op, field.Name) } } } return &ComparisonCondition{ Left: left, Operator: op, Right: right, }, nil } func buildInCondition(call *exprv1.Expr_Call, schema Schema) (Condition, error) { if len(call.Args) != 2 { return nil, errors.New("in operator expects two arguments") } // Handle identifier in list syntax. if identName, err := getIdentName(call.Args[0]); err == nil { if field, ok := schema.Field(identName); ok && field.Kind == FieldKindVirtualAlias { if _, aliasOk := schema.ResolveAlias(identName); !aliasOk { return nil, errors.Errorf("invalid alias %q", identName) } } else if !ok { return nil, errors.Errorf("unknown identifier %q", identName) } if listExpr := call.Args[1].GetListExpr(); listExpr != nil { values := make([]ValueExpr, 0, len(listExpr.Elements)) for _, element := range listExpr.Elements { value, err := buildValueExpr(element, schema) if err != nil { return nil, err } values = append(values, value) } return &InCondition{ Left: &FieldRef{Name: identName}, Values: values, }, nil } } // Handle "value in identifier" syntax. if identName, err := getIdentName(call.Args[1]); err == nil { if _, ok := schema.Field(identName); !ok { return nil, errors.Errorf("unknown identifier %q", identName) } element, err := buildValueExpr(call.Args[0], schema) if err != nil { return nil, err } return &ElementInCondition{ Element: element, Field: identName, }, nil } return nil, errors.New("invalid use of in operator") } func buildContainsCondition(call *exprv1.Expr_Call, schema Schema) (Condition, error) { if call.Target == nil { return nil, errors.New("contains requires a target") } targetName, err := getIdentName(call.Target) if err != nil { return nil, err } field, ok := schema.Field(targetName) if !ok { return nil, errors.Errorf("unknown identifier %q", targetName) } if !field.SupportsContains { return nil, errors.Errorf("identifier %q does not support contains()", targetName) } if len(call.Args) != 1 { return nil, errors.New("contains expects exactly one argument") } value, err := getConstValue(call.Args[0]) if err != nil { return nil, errors.Wrap(err, "contains only supports literal arguments") } str, ok := value.(string) if !ok { return nil, errors.New("contains argument must be a string") } return &ContainsCondition{ Field: targetName, Value: str, }, nil } func buildValueExpr(expr *exprv1.Expr, schema Schema) (ValueExpr, error) { if identName, err := getIdentName(expr); err == nil { if _, ok := schema.Field(identName); !ok { return nil, errors.Errorf("unknown identifier %q", identName) } return &FieldRef{Name: identName}, nil } if literal, err := getConstValue(expr); err == nil { return &LiteralValue{Value: literal}, nil } if value, ok, err := evaluateNumeric(expr); err != nil { return nil, err } else if ok { return &LiteralValue{Value: value}, nil } if boolVal, ok, err := evaluateBoolExpr(expr); err != nil { return nil, err } else if ok { return &LiteralValue{Value: boolVal}, nil } if call := expr.GetCallExpr(); call != nil { switch call.Function { case "size": if len(call.Args) != 1 { return nil, errors.New("size() expects one argument") } arg, err := buildValueExpr(call.Args[0], schema) if err != nil { return nil, err } return &FunctionValue{ Name: "size", Args: []ValueExpr{arg}, }, nil case "now": return &LiteralValue{Value: timeNowUnix()}, nil case "_+_", "_-_", "_*_": value, ok, err := evaluateNumeric(expr) if err != nil { return nil, err } if ok { return &LiteralValue{Value: value}, nil } default: // Fall through to error return below } } return nil, errors.New("unsupported value expression") } func toComparisonOperator(fn string) (ComparisonOperator, error) { switch fn { case "_==_": return CompareEq, nil case "_!=_": return CompareNeq, nil case "_<_": return CompareLt, nil case "_>_": return CompareGt, nil case "_<=_": return CompareLte, nil case "_>=_": return CompareGte, nil default: return "", errors.Errorf("unsupported comparison operator %q", fn) } } func getIdentName(expr *exprv1.Expr) (string, error) { if ident := expr.GetIdentExpr(); ident != nil { return ident.GetName(), nil } return "", errors.New("expression is not an identifier") } func getConstValue(expr *exprv1.Expr) (interface{}, error) { v, ok := expr.ExprKind.(*exprv1.Expr_ConstExpr) if !ok { return nil, errors.New("expression is not a literal") } switch x := 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 int64(v.ConstExpr.GetUint64Value()), nil case *exprv1.Constant_DoubleValue: return v.ConstExpr.GetDoubleValue(), nil case *exprv1.Constant_BoolValue: return v.ConstExpr.GetBoolValue(), nil case *exprv1.Constant_NullValue: return nil, nil default: return nil, errors.Errorf("unsupported constant %T", x) } } func evaluateBool(call *exprv1.Expr_Call) (bool, bool, error) { val, ok, err := evaluateBoolExpr(&exprv1.Expr{ExprKind: &exprv1.Expr_CallExpr{CallExpr: call}}) return val, ok, err } func evaluateBoolExpr(expr *exprv1.Expr) (bool, bool, error) { if literal, err := getConstValue(expr); err == nil { if b, ok := literal.(bool); ok { return b, true, nil } return false, false, nil } if call := expr.GetCallExpr(); call != nil && call.Function == "!_" { if len(call.Args) != 1 { return false, false, errors.New("NOT expects exactly one argument") } val, ok, err := evaluateBoolExpr(call.Args[0]) if err != nil || !ok { return false, false, err } return !val, true, nil } return false, false, nil } func evaluateNumeric(expr *exprv1.Expr) (int64, bool, error) { if literal, err := getConstValue(expr); err == nil { switch v := literal.(type) { case int64: return v, true, nil case float64: return int64(v), true, nil } return 0, false, nil } call := expr.GetCallExpr() if call == nil { return 0, false, nil } switch call.Function { case "now": return timeNowUnix(), true, nil case "_+_", "_-_", "_*_": if len(call.Args) != 2 { return 0, false, errors.New("arithmetic requires two arguments") } left, ok, err := evaluateNumeric(call.Args[0]) if err != nil { return 0, false, err } if !ok { return 0, false, nil } right, ok, err := evaluateNumeric(call.Args[1]) if err != nil { return 0, false, err } if !ok { return 0, false, nil } switch call.Function { case "_+_": return left + right, true, nil case "_-_": return left - right, true, nil case "_*_": return left * right, true, nil default: return 0, false, errors.Errorf("unsupported arithmetic operator %q", call.Function) } default: return 0, false, nil } } func timeNowUnix() int64 { return time.Now().Unix() } // buildComprehensionCondition handles CEL comprehension expressions (exists, all, etc.). func buildComprehensionCondition(comp *exprv1.Expr_Comprehension, schema Schema) (Condition, error) { // Determine the comprehension kind by examining the loop initialization and step kind, err := detectComprehensionKind(comp) if err != nil { return nil, err } // Get the field being iterated over iterRangeIdent := comp.IterRange.GetIdentExpr() if iterRangeIdent == nil { return nil, errors.New("comprehension range must be a field identifier") } fieldName := iterRangeIdent.GetName() // Validate the field field, ok := schema.Field(fieldName) if !ok { return nil, errors.Errorf("unknown field %q in comprehension", fieldName) } if field.Kind != FieldKindJSONList { return nil, errors.Errorf("field %q does not support comprehension (must be a list)", fieldName) } // Extract the predicate from the loop step predicate, err := extractPredicate(comp, schema) if err != nil { return nil, err } return &ListComprehensionCondition{ Kind: kind, Field: fieldName, IterVar: comp.IterVar, Predicate: predicate, }, nil } // detectComprehensionKind determines if this is an exists() macro. // Only exists() is currently supported. func detectComprehensionKind(comp *exprv1.Expr_Comprehension) (ComprehensionKind, error) { // Check the accumulator initialization accuInit := comp.AccuInit.GetConstExpr() if accuInit == nil { return "", errors.New("comprehension accumulator must be initialized with a constant") } // exists() starts with false and uses OR (||) in loop step if !accuInit.GetBoolValue() { if step := comp.LoopStep.GetCallExpr(); step != nil && step.Function == "_||_" { return ComprehensionExists, nil } } // all() starts with true and uses AND (&&) - not supported if accuInit.GetBoolValue() { if step := comp.LoopStep.GetCallExpr(); step != nil && step.Function == "_&&_" { return "", errors.New("all() comprehension is not supported; use exists() instead") } } return "", errors.New("unsupported comprehension type; only exists() is supported") } // extractPredicate extracts the predicate expression from the comprehension loop step. func extractPredicate(comp *exprv1.Expr_Comprehension, _ Schema) (PredicateExpr, error) { // The loop step is: @result || predicate(t) for exists // or: @result && predicate(t) for all step := comp.LoopStep.GetCallExpr() if step == nil { return nil, errors.New("comprehension loop step must be a call expression") } if len(step.Args) != 2 { return nil, errors.New("comprehension loop step must have two arguments") } // The predicate is the second argument predicateExpr := step.Args[1] predicateCall := predicateExpr.GetCallExpr() if predicateCall == nil { return nil, errors.New("comprehension predicate must be a function call") } // Handle different predicate functions switch predicateCall.Function { case "startsWith": return buildStartsWithPredicate(predicateCall, comp.IterVar) case "endsWith": return buildEndsWithPredicate(predicateCall, comp.IterVar) case "contains": return buildContainsPredicate(predicateCall, comp.IterVar) default: return nil, errors.Errorf("unsupported predicate function %q in comprehension (supported: startsWith, endsWith, contains)", predicateCall.Function) } } // buildStartsWithPredicate extracts the pattern from t.startsWith("prefix"). func buildStartsWithPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) { // Verify the target is the iteration variable if target := call.Target.GetIdentExpr(); target == nil || target.GetName() != iterVar { return nil, errors.Errorf("startsWith target must be the iteration variable %q", iterVar) } if len(call.Args) != 1 { return nil, errors.New("startsWith expects exactly one argument") } prefix, err := getConstValue(call.Args[0]) if err != nil { return nil, errors.Wrap(err, "startsWith argument must be a constant string") } prefixStr, ok := prefix.(string) if !ok { return nil, errors.New("startsWith argument must be a string") } return &StartsWithPredicate{Prefix: prefixStr}, nil } // buildEndsWithPredicate extracts the pattern from t.endsWith("suffix"). func buildEndsWithPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) { if target := call.Target.GetIdentExpr(); target == nil || target.GetName() != iterVar { return nil, errors.Errorf("endsWith target must be the iteration variable %q", iterVar) } if len(call.Args) != 1 { return nil, errors.New("endsWith expects exactly one argument") } suffix, err := getConstValue(call.Args[0]) if err != nil { return nil, errors.Wrap(err, "endsWith argument must be a constant string") } suffixStr, ok := suffix.(string) if !ok { return nil, errors.New("endsWith argument must be a string") } return &EndsWithPredicate{Suffix: suffixStr}, nil } // buildContainsPredicate extracts the pattern from t.contains("substring"). func buildContainsPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) { if target := call.Target.GetIdentExpr(); target == nil || target.GetName() != iterVar { return nil, errors.Errorf("contains target must be the iteration variable %q", iterVar) } if len(call.Args) != 1 { return nil, errors.New("contains expects exactly one argument") } substring, err := getConstValue(call.Args[0]) if err != nil { return nil, errors.Wrap(err, "contains argument must be a constant string") } substringStr, ok := substring.(string) if !ok { return nil, errors.New("contains argument must be a string") } return &ContainsPredicate{Substring: substringStr}, nil } ================================================ FILE: plugin/filter/render.go ================================================ package filter import ( "fmt" "strings" "github.com/pkg/errors" ) type renderer struct { schema Schema dialect DialectName placeholderOffset int placeholderCounter int args []any } type renderResult struct { sql string trivial bool unsatisfiable bool } func newRenderer(schema Schema, opts RenderOptions) *renderer { return &renderer{ schema: schema, dialect: opts.Dialect, placeholderOffset: opts.PlaceholderOffset, } } func (r *renderer) Render(cond Condition) (Statement, error) { result, err := r.renderCondition(cond) if err != nil { return Statement{}, err } args := r.args if args == nil { args = []any{} } switch { case result.unsatisfiable: return Statement{ SQL: "1 = 0", Args: args, }, nil case result.trivial: return Statement{ SQL: "", Args: args, }, nil default: return Statement{ SQL: result.sql, Args: args, }, nil } } func (r *renderer) renderCondition(cond Condition) (renderResult, error) { switch c := cond.(type) { case *LogicalCondition: return r.renderLogicalCondition(c) case *NotCondition: return r.renderNotCondition(c) case *FieldPredicateCondition: return r.renderFieldPredicate(c) case *ComparisonCondition: return r.renderComparison(c) case *InCondition: return r.renderInCondition(c) case *ElementInCondition: return r.renderElementInCondition(c) case *ContainsCondition: return r.renderContainsCondition(c) case *ListComprehensionCondition: return r.renderListComprehension(c) case *ConstantCondition: if c.Value { return renderResult{trivial: true}, nil } return renderResult{sql: "1 = 0", unsatisfiable: true}, nil default: return renderResult{}, errors.Errorf("unsupported condition type %T", c) } } func (r *renderer) renderLogicalCondition(cond *LogicalCondition) (renderResult, error) { left, err := r.renderCondition(cond.Left) if err != nil { return renderResult{}, err } right, err := r.renderCondition(cond.Right) if err != nil { return renderResult{}, err } switch cond.Operator { case LogicalAnd: return combineAnd(left, right), nil case LogicalOr: return combineOr(left, right), nil default: return renderResult{}, errors.Errorf("unsupported logical operator %s", cond.Operator) } } func (r *renderer) renderNotCondition(cond *NotCondition) (renderResult, error) { child, err := r.renderCondition(cond.Expr) if err != nil { return renderResult{}, err } if child.trivial { return renderResult{sql: "1 = 0", unsatisfiable: true}, nil } if child.unsatisfiable { return renderResult{trivial: true}, nil } return renderResult{ sql: fmt.Sprintf("NOT (%s)", child.sql), }, nil } func (r *renderer) renderFieldPredicate(cond *FieldPredicateCondition) (renderResult, error) { field, ok := r.schema.Field(cond.Field) if !ok { return renderResult{}, errors.Errorf("unknown field %q", cond.Field) } switch field.Kind { case FieldKindBoolColumn: column := qualifyColumn(r.dialect, field.Column) return renderResult{ sql: fmt.Sprintf("%s IS TRUE", column), }, nil case FieldKindJSONBool: sql, err := r.jsonBoolPredicate(field) if err != nil { return renderResult{}, err } return renderResult{sql: sql}, nil default: return renderResult{}, errors.Errorf("field %q cannot be used as a predicate", cond.Field) } } func (r *renderer) renderComparison(cond *ComparisonCondition) (renderResult, error) { switch left := cond.Left.(type) { case *FieldRef: field, ok := r.schema.Field(left.Name) if !ok { return renderResult{}, errors.Errorf("unknown field %q", left.Name) } switch field.Kind { case FieldKindBoolColumn: return r.renderBoolColumnComparison(field, cond.Operator, cond.Right) case FieldKindJSONBool: return r.renderJSONBoolComparison(field, cond.Operator, cond.Right) case FieldKindScalar: return r.renderScalarComparison(field, cond.Operator, cond.Right) default: return renderResult{}, errors.Errorf("field %q does not support comparison", field.Name) } case *FunctionValue: return r.renderFunctionComparison(left, cond.Operator, cond.Right) default: return renderResult{}, errors.New("comparison must start with a field reference or supported function") } } func (r *renderer) renderFunctionComparison(fn *FunctionValue, op ComparisonOperator, right ValueExpr) (renderResult, error) { if fn.Name != "size" { return renderResult{}, errors.Errorf("unsupported function %s in comparison", fn.Name) } if len(fn.Args) != 1 { return renderResult{}, errors.New("size() expects one argument") } fieldArg, ok := fn.Args[0].(*FieldRef) if !ok { return renderResult{}, errors.New("size() argument must be a field") } field, ok := r.schema.Field(fieldArg.Name) if !ok { return renderResult{}, errors.Errorf("unknown field %q", fieldArg.Name) } if field.Kind != FieldKindJSONList { return renderResult{}, errors.Errorf("size() only supports tag lists, got %q", field.Name) } value, err := expectNumericLiteral(right) if err != nil { return renderResult{}, err } expr := jsonArrayLengthExpr(r.dialect, field) placeholder := r.addArg(value) return renderResult{ sql: fmt.Sprintf("%s %s %s", expr, sqlOperator(op), placeholder), }, nil } func (r *renderer) renderScalarComparison(field Field, op ComparisonOperator, right ValueExpr) (renderResult, error) { lit, err := expectLiteral(right) if err != nil { return renderResult{}, err } columnExpr := field.columnExpr(r.dialect) if lit == nil { switch op { case CompareEq: return renderResult{sql: fmt.Sprintf("%s IS NULL", columnExpr)}, nil case CompareNeq: return renderResult{sql: fmt.Sprintf("%s IS NOT NULL", columnExpr)}, nil default: return renderResult{}, errors.Errorf("operator %s not supported for null comparison", op) } } placeholder := "" switch field.Type { case FieldTypeString: value, ok := lit.(string) if !ok { return renderResult{}, errors.Errorf("field %q expects string value", field.Name) } placeholder = r.addArg(value) case FieldTypeInt, FieldTypeTimestamp: num, err := toInt64(lit) if err != nil { return renderResult{}, errors.Wrapf(err, "field %q expects integer value", field.Name) } placeholder = r.addArg(num) default: return renderResult{}, errors.Errorf("unsupported data type %q for field %s", field.Type, field.Name) } return renderResult{ sql: fmt.Sprintf("%s %s %s", columnExpr, sqlOperator(op), placeholder), }, nil } func (r *renderer) renderBoolColumnComparison(field Field, op ComparisonOperator, right ValueExpr) (renderResult, error) { value, err := expectBool(right) if err != nil { return renderResult{}, err } placeholder := r.addBoolArg(value) column := qualifyColumn(r.dialect, field.Column) return renderResult{ sql: fmt.Sprintf("%s %s %s", column, sqlOperator(op), placeholder), }, nil } func (r *renderer) renderJSONBoolComparison(field Field, op ComparisonOperator, right ValueExpr) (renderResult, error) { value, err := expectBool(right) if err != nil { return renderResult{}, err } jsonExpr := jsonExtractExpr(r.dialect, field) switch r.dialect { case DialectSQLite: switch op { case CompareEq: if field.Name == "has_task_list" { target := "0" if value { target = "1" } return renderResult{sql: fmt.Sprintf("%s = %s", jsonExpr, target)}, nil } if value { return renderResult{sql: fmt.Sprintf("%s IS TRUE", jsonExpr)}, nil } return renderResult{sql: fmt.Sprintf("NOT(%s IS TRUE)", jsonExpr)}, nil case CompareNeq: if field.Name == "has_task_list" { target := "0" if value { target = "1" } return renderResult{sql: fmt.Sprintf("%s != %s", jsonExpr, target)}, nil } if value { return renderResult{sql: fmt.Sprintf("NOT(%s IS TRUE)", jsonExpr)}, nil } return renderResult{sql: fmt.Sprintf("%s IS TRUE", jsonExpr)}, nil default: return renderResult{}, errors.Errorf("operator %s not supported for boolean JSON field", op) } case DialectMySQL: boolStr := "false" if value { boolStr = "true" } return renderResult{ sql: fmt.Sprintf("%s %s CAST('%s' AS JSON)", jsonExpr, sqlOperator(op), boolStr), }, nil case DialectPostgres: placeholder := r.addArg(value) return renderResult{ sql: fmt.Sprintf("(%s)::boolean %s %s", jsonExpr, sqlOperator(op), placeholder), }, nil default: return renderResult{}, errors.Errorf("unsupported dialect %s", r.dialect) } } func (r *renderer) renderInCondition(cond *InCondition) (renderResult, error) { fieldRef, ok := cond.Left.(*FieldRef) if !ok { return renderResult{}, errors.New("IN operator requires a field on the left-hand side") } if fieldRef.Name == "tag" { return r.renderTagInList(cond.Values) } field, ok := r.schema.Field(fieldRef.Name) if !ok { return renderResult{}, errors.Errorf("unknown field %q", fieldRef.Name) } if field.Kind != FieldKindScalar { return renderResult{}, errors.Errorf("field %q does not support IN()", fieldRef.Name) } return r.renderScalarInCondition(field, cond.Values) } func (r *renderer) renderTagInList(values []ValueExpr) (renderResult, error) { field, ok := r.schema.ResolveAlias("tag") if !ok { return renderResult{}, errors.New("tag attribute is not configured") } conditions := make([]string, 0, len(values)) for _, v := range values { lit, err := expectLiteral(v) if err != nil { return renderResult{}, err } str, ok := lit.(string) if !ok { return renderResult{}, errors.New("tags must be compared with string literals") } switch r.dialect { case DialectSQLite: // Support hierarchical tags: match exact tag OR tags with this prefix (e.g., "book" matches "book" and "book/something") exactMatch := fmt.Sprintf("%s LIKE %s", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%"%s"%%`, str))) prefixMatch := fmt.Sprintf("%s LIKE %s", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%"%s/%%`, str))) expr := fmt.Sprintf("(%s OR %s)", exactMatch, prefixMatch) conditions = append(conditions, expr) case DialectMySQL: // Support hierarchical tags: match exact tag OR tags with this prefix exactMatch := fmt.Sprintf("JSON_CONTAINS(%s, %s)", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`"%s"`, str))) prefixMatch := fmt.Sprintf("%s LIKE %s", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%"%s/%%`, str))) expr := fmt.Sprintf("(%s OR %s)", exactMatch, prefixMatch) conditions = append(conditions, expr) case DialectPostgres: // Support hierarchical tags: match exact tag OR tags with this prefix exactMatch := fmt.Sprintf("%s @> jsonb_build_array(%s::json)", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`"%s"`, str))) prefixMatch := fmt.Sprintf("(%s)::text LIKE %s", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%"%s/%%`, str))) expr := fmt.Sprintf("(%s OR %s)", exactMatch, prefixMatch) conditions = append(conditions, expr) default: return renderResult{}, errors.Errorf("unsupported dialect %s", r.dialect) } } if len(conditions) == 1 { return renderResult{sql: conditions[0]}, nil } return renderResult{ sql: fmt.Sprintf("(%s)", strings.Join(conditions, " OR ")), }, nil } func (r *renderer) renderElementInCondition(cond *ElementInCondition) (renderResult, error) { field, ok := r.schema.Field(cond.Field) if !ok { return renderResult{}, errors.Errorf("unknown field %q", cond.Field) } if field.Kind != FieldKindJSONList { return renderResult{}, errors.Errorf("field %q is not a tag list", cond.Field) } lit, err := expectLiteral(cond.Element) if err != nil { return renderResult{}, err } str, ok := lit.(string) if !ok { return renderResult{}, errors.New("tags membership requires string literal") } switch r.dialect { case DialectSQLite: sql := fmt.Sprintf("%s LIKE %s", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%"%s"%%`, str))) return renderResult{sql: sql}, nil case DialectMySQL: sql := fmt.Sprintf("JSON_CONTAINS(%s, %s)", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`"%s"`, str))) return renderResult{sql: sql}, nil case DialectPostgres: sql := fmt.Sprintf("%s @> jsonb_build_array(%s::json)", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`"%s"`, str))) return renderResult{sql: sql}, nil default: return renderResult{}, errors.Errorf("unsupported dialect %s", r.dialect) } } func (r *renderer) renderScalarInCondition(field Field, values []ValueExpr) (renderResult, error) { placeholders := make([]string, 0, len(values)) for _, v := range values { lit, err := expectLiteral(v) if err != nil { return renderResult{}, err } switch field.Type { case FieldTypeString: str, ok := lit.(string) if !ok { return renderResult{}, errors.Errorf("field %q expects string values", field.Name) } placeholders = append(placeholders, r.addArg(str)) case FieldTypeInt: num, err := toInt64(lit) if err != nil { return renderResult{}, err } placeholders = append(placeholders, r.addArg(num)) default: return renderResult{}, errors.Errorf("field %q does not support IN() comparisons", field.Name) } } column := field.columnExpr(r.dialect) return renderResult{ sql: fmt.Sprintf("%s IN (%s)", column, strings.Join(placeholders, ",")), }, nil } func (r *renderer) renderContainsCondition(cond *ContainsCondition) (renderResult, error) { field, ok := r.schema.Field(cond.Field) if !ok { return renderResult{}, errors.Errorf("unknown field %q", cond.Field) } column := field.columnExpr(r.dialect) arg := fmt.Sprintf("%%%s%%", cond.Value) switch r.dialect { case DialectSQLite: // Use custom Unicode-aware case folding function for case-insensitive comparison. // This overcomes SQLite's ASCII-only LOWER() limitation. sql := fmt.Sprintf("memos_unicode_lower(%s) LIKE memos_unicode_lower(%s)", column, r.addArg(arg)) return renderResult{sql: sql}, nil case DialectPostgres: sql := fmt.Sprintf("%s ILIKE %s", column, r.addArg(arg)) return renderResult{sql: sql}, nil default: sql := fmt.Sprintf("%s LIKE %s", column, r.addArg(arg)) return renderResult{sql: sql}, nil } } func (r *renderer) renderListComprehension(cond *ListComprehensionCondition) (renderResult, error) { field, ok := r.schema.Field(cond.Field) if !ok { return renderResult{}, errors.Errorf("unknown field %q", cond.Field) } if field.Kind != FieldKindJSONList { return renderResult{}, errors.Errorf("field %q is not a JSON list", cond.Field) } // Render based on predicate type switch pred := cond.Predicate.(type) { case *StartsWithPredicate: return r.renderTagStartsWith(field, pred.Prefix, cond.Kind) case *EndsWithPredicate: return r.renderTagEndsWith(field, pred.Suffix, cond.Kind) case *ContainsPredicate: return r.renderTagContains(field, pred.Substring, cond.Kind) default: return renderResult{}, errors.Errorf("unsupported predicate type %T in comprehension", pred) } } // renderTagStartsWith generates SQL for tags.exists(t, t.startsWith("prefix")). func (r *renderer) renderTagStartsWith(field Field, prefix string, _ ComprehensionKind) (renderResult, error) { arrayExpr := jsonArrayExpr(r.dialect, field) switch r.dialect { case DialectSQLite, DialectMySQL: // Match exact tag or tags with this prefix (hierarchical support) exactMatch := r.buildJSONArrayLike(arrayExpr, fmt.Sprintf(`%%"%s"%%`, prefix)) prefixMatch := r.buildJSONArrayLike(arrayExpr, fmt.Sprintf(`%%"%s%%`, prefix)) condition := fmt.Sprintf("(%s OR %s)", exactMatch, prefixMatch) return renderResult{sql: r.wrapWithNullCheck(arrayExpr, condition)}, nil case DialectPostgres: // Use PostgreSQL's powerful JSON operators exactMatch := fmt.Sprintf("%s @> jsonb_build_array(%s::json)", arrayExpr, r.addArg(fmt.Sprintf(`"%s"`, prefix))) prefixMatch := fmt.Sprintf("(%s)::text LIKE %s", arrayExpr, r.addArg(fmt.Sprintf(`%%"%s%%`, prefix))) condition := fmt.Sprintf("(%s OR %s)", exactMatch, prefixMatch) return renderResult{sql: r.wrapWithNullCheck(arrayExpr, condition)}, nil default: return renderResult{}, errors.Errorf("unsupported dialect %s", r.dialect) } } // renderTagEndsWith generates SQL for tags.exists(t, t.endsWith("suffix")). func (r *renderer) renderTagEndsWith(field Field, suffix string, _ ComprehensionKind) (renderResult, error) { arrayExpr := jsonArrayExpr(r.dialect, field) pattern := fmt.Sprintf(`%%%s"%%`, suffix) likeExpr := r.buildJSONArrayLike(arrayExpr, pattern) return renderResult{sql: r.wrapWithNullCheck(arrayExpr, likeExpr)}, nil } // renderTagContains generates SQL for tags.exists(t, t.contains("substring")). func (r *renderer) renderTagContains(field Field, substring string, _ ComprehensionKind) (renderResult, error) { arrayExpr := jsonArrayExpr(r.dialect, field) pattern := fmt.Sprintf(`%%%s%%`, substring) likeExpr := r.buildJSONArrayLike(arrayExpr, pattern) return renderResult{sql: r.wrapWithNullCheck(arrayExpr, likeExpr)}, nil } // buildJSONArrayLike builds a LIKE expression for matching within a JSON array. // Returns the LIKE clause without NULL/empty checks. func (r *renderer) buildJSONArrayLike(arrayExpr, pattern string) string { switch r.dialect { case DialectSQLite, DialectMySQL: return fmt.Sprintf("%s LIKE %s", arrayExpr, r.addArg(pattern)) case DialectPostgres: return fmt.Sprintf("(%s)::text LIKE %s", arrayExpr, r.addArg(pattern)) default: return "" } } // wrapWithNullCheck wraps a condition with NULL and empty array checks. // This ensures we don't match against NULL or empty JSON arrays. func (r *renderer) wrapWithNullCheck(arrayExpr, condition string) string { var nullCheck string switch r.dialect { case DialectSQLite: nullCheck = fmt.Sprintf("%s IS NOT NULL AND %s != '[]'", arrayExpr, arrayExpr) case DialectMySQL: nullCheck = fmt.Sprintf("%s IS NOT NULL AND JSON_LENGTH(%s) > 0", arrayExpr, arrayExpr) case DialectPostgres: nullCheck = fmt.Sprintf("%s IS NOT NULL AND jsonb_array_length(%s) > 0", arrayExpr, arrayExpr) default: return condition } return fmt.Sprintf("(%s AND %s)", condition, nullCheck) } func (r *renderer) jsonBoolPredicate(field Field) (string, error) { expr := jsonExtractExpr(r.dialect, field) switch r.dialect { case DialectSQLite: return fmt.Sprintf("%s IS TRUE", expr), nil case DialectMySQL: return fmt.Sprintf("COALESCE(%s, CAST('false' AS JSON)) = CAST('true' AS JSON)", expr), nil case DialectPostgres: return fmt.Sprintf("(%s)::boolean IS TRUE", expr), nil default: return "", errors.Errorf("unsupported dialect %s", r.dialect) } } func combineAnd(left, right renderResult) renderResult { if left.unsatisfiable || right.unsatisfiable { return renderResult{sql: "1 = 0", unsatisfiable: true} } if left.trivial { return right } if right.trivial { return left } return renderResult{ sql: fmt.Sprintf("(%s AND %s)", left.sql, right.sql), } } func combineOr(left, right renderResult) renderResult { if left.trivial || right.trivial { return renderResult{trivial: true} } if left.unsatisfiable { return right } if right.unsatisfiable { return left } return renderResult{ sql: fmt.Sprintf("(%s OR %s)", left.sql, right.sql), } } func (r *renderer) addArg(value any) string { r.placeholderCounter++ r.args = append(r.args, value) if r.dialect == DialectPostgres { return fmt.Sprintf("$%d", r.placeholderOffset+r.placeholderCounter) } return "?" } func (r *renderer) addBoolArg(value bool) string { var v any switch r.dialect { case DialectSQLite: if value { v = 1 } else { v = 0 } default: v = value } return r.addArg(v) } func expectLiteral(expr ValueExpr) (any, error) { lit, ok := expr.(*LiteralValue) if !ok { return nil, errors.New("expression must be a literal") } return lit.Value, nil } func expectBool(expr ValueExpr) (bool, error) { lit, err := expectLiteral(expr) if err != nil { return false, err } value, ok := lit.(bool) if !ok { return false, errors.New("boolean literal required") } return value, nil } func expectNumericLiteral(expr ValueExpr) (int64, error) { lit, err := expectLiteral(expr) if err != nil { return 0, err } return toInt64(lit) } func toInt64(value any) (int64, error) { switch v := value.(type) { case int: return int64(v), nil case int32: return int64(v), nil case int64: return v, nil case uint32: return int64(v), nil case uint64: return int64(v), nil case float32: return int64(v), nil case float64: return int64(v), nil default: return 0, errors.Errorf("cannot convert %T to int64", value) } } func sqlOperator(op ComparisonOperator) string { return string(op) } func qualifyColumn(d DialectName, col Column) string { switch d { case DialectPostgres: return fmt.Sprintf("%s.%s", col.Table, col.Name) default: return fmt.Sprintf("`%s`.`%s`", col.Table, col.Name) } } func jsonPath(field Field) string { return "$." + strings.Join(field.JSONPath, ".") } func jsonExtractExpr(d DialectName, field Field) string { column := qualifyColumn(d, field.Column) switch d { case DialectSQLite, DialectMySQL: return fmt.Sprintf("JSON_EXTRACT(%s, '%s')", column, jsonPath(field)) case DialectPostgres: return buildPostgresJSONAccessor(column, field.JSONPath, true) default: return "" } } func jsonArrayExpr(d DialectName, field Field) string { column := qualifyColumn(d, field.Column) switch d { case DialectSQLite, DialectMySQL: return fmt.Sprintf("JSON_EXTRACT(%s, '%s')", column, jsonPath(field)) case DialectPostgres: return buildPostgresJSONAccessor(column, field.JSONPath, false) default: return "" } } func jsonArrayLengthExpr(d DialectName, field Field) string { arrayExpr := jsonArrayExpr(d, field) switch d { case DialectSQLite: return fmt.Sprintf("JSON_ARRAY_LENGTH(COALESCE(%s, JSON_ARRAY()))", arrayExpr) case DialectMySQL: return fmt.Sprintf("JSON_LENGTH(COALESCE(%s, JSON_ARRAY()))", arrayExpr) case DialectPostgres: return fmt.Sprintf("jsonb_array_length(COALESCE(%s, '[]'::jsonb))", arrayExpr) default: return "" } } func buildPostgresJSONAccessor(base string, path []string, terminalText bool) string { expr := base for idx, part := range path { if idx == len(path)-1 && terminalText { expr = fmt.Sprintf("%s->>'%s'", expr, part) } else { expr = fmt.Sprintf("%s->'%s'", expr, part) } } return expr } ================================================ FILE: plugin/filter/schema.go ================================================ package filter import ( "fmt" "time" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" ) // DialectName enumerates supported SQL dialects. type DialectName string const ( DialectSQLite DialectName = "sqlite" DialectMySQL DialectName = "mysql" DialectPostgres DialectName = "postgres" ) // FieldType represents the logical type of a field. type FieldType string const ( FieldTypeString FieldType = "string" FieldTypeInt FieldType = "int" FieldTypeBool FieldType = "bool" FieldTypeTimestamp FieldType = "timestamp" ) // FieldKind describes how a field is stored. type FieldKind string const ( FieldKindScalar FieldKind = "scalar" FieldKindBoolColumn FieldKind = "bool_column" FieldKindJSONBool FieldKind = "json_bool" FieldKindJSONList FieldKind = "json_list" FieldKindVirtualAlias FieldKind = "virtual_alias" ) // Column identifies the backing table column. type Column struct { Table string Name string } // Field captures the schema metadata for an exposed CEL identifier. type Field struct { Name string Kind FieldKind Type FieldType Column Column JSONPath []string AliasFor string SupportsContains bool Expressions map[DialectName]string AllowedComparisonOps map[ComparisonOperator]bool } // Schema collects CEL environment options and field metadata. type Schema struct { Name string Fields map[string]Field EnvOptions []cel.EnvOption } // Field returns the field metadata if present. func (s Schema) Field(name string) (Field, bool) { f, ok := s.Fields[name] return f, ok } // ResolveAlias resolves a virtual alias to its target field. func (s Schema) ResolveAlias(name string) (Field, bool) { field, ok := s.Fields[name] if !ok { return Field{}, false } if field.Kind == FieldKindVirtualAlias { target, ok := s.Fields[field.AliasFor] if !ok { return Field{}, false } return target, true } return field, true } var nowFunction = cel.Function("now", cel.Overload("now", []*cel.Type{}, cel.IntType, cel.FunctionBinding(func(_ ...ref.Val) ref.Val { return types.Int(time.Now().Unix()) }), ), ) // NewSchema constructs the memo filter schema and CEL environment. func NewSchema() Schema { fields := map[string]Field{ "content": { Name: "content", Kind: FieldKindScalar, Type: FieldTypeString, Column: Column{Table: "memo", Name: "content"}, SupportsContains: true, Expressions: map[DialectName]string{}, }, "creator_id": { Name: "creator_id", Kind: FieldKindScalar, Type: FieldTypeInt, Column: Column{Table: "memo", Name: "creator_id"}, Expressions: map[DialectName]string{}, AllowedComparisonOps: map[ComparisonOperator]bool{ CompareEq: true, CompareNeq: true, }, }, "created_ts": { Name: "created_ts", Kind: FieldKindScalar, Type: FieldTypeTimestamp, Column: Column{Table: "memo", Name: "created_ts"}, Expressions: map[DialectName]string{ // MySQL stores created_ts as TIMESTAMP, needs conversion to epoch DialectMySQL: "UNIX_TIMESTAMP(%s)", // PostgreSQL and SQLite store created_ts as BIGINT (epoch), no conversion needed DialectPostgres: "%s", DialectSQLite: "%s", }, }, "updated_ts": { Name: "updated_ts", Kind: FieldKindScalar, Type: FieldTypeTimestamp, Column: Column{Table: "memo", Name: "updated_ts"}, Expressions: map[DialectName]string{ // MySQL stores updated_ts as TIMESTAMP, needs conversion to epoch DialectMySQL: "UNIX_TIMESTAMP(%s)", // PostgreSQL and SQLite store updated_ts as BIGINT (epoch), no conversion needed DialectPostgres: "%s", DialectSQLite: "%s", }, }, "pinned": { Name: "pinned", Kind: FieldKindBoolColumn, Type: FieldTypeBool, Column: Column{Table: "memo", Name: "pinned"}, Expressions: map[DialectName]string{}, AllowedComparisonOps: map[ComparisonOperator]bool{ CompareEq: true, CompareNeq: true, }, }, "visibility": { Name: "visibility", Kind: FieldKindScalar, Type: FieldTypeString, Column: Column{Table: "memo", Name: "visibility"}, Expressions: map[DialectName]string{}, AllowedComparisonOps: map[ComparisonOperator]bool{ CompareEq: true, CompareNeq: true, }, }, "tags": { Name: "tags", Kind: FieldKindJSONList, Type: FieldTypeString, Column: Column{Table: "memo", Name: "payload"}, JSONPath: []string{"tags"}, }, "tag": { Name: "tag", Kind: FieldKindVirtualAlias, Type: FieldTypeString, AliasFor: "tags", }, "has_task_list": { Name: "has_task_list", Kind: FieldKindJSONBool, Type: FieldTypeBool, Column: Column{Table: "memo", Name: "payload"}, JSONPath: []string{"property", "hasTaskList"}, AllowedComparisonOps: map[ComparisonOperator]bool{ CompareEq: true, CompareNeq: true, }, }, "has_link": { Name: "has_link", Kind: FieldKindJSONBool, Type: FieldTypeBool, Column: Column{Table: "memo", Name: "payload"}, JSONPath: []string{"property", "hasLink"}, AllowedComparisonOps: map[ComparisonOperator]bool{ CompareEq: true, CompareNeq: true, }, }, "has_code": { Name: "has_code", Kind: FieldKindJSONBool, Type: FieldTypeBool, Column: Column{Table: "memo", Name: "payload"}, JSONPath: []string{"property", "hasCode"}, AllowedComparisonOps: map[ComparisonOperator]bool{ CompareEq: true, CompareNeq: true, }, }, "has_incomplete_tasks": { Name: "has_incomplete_tasks", Kind: FieldKindJSONBool, Type: FieldTypeBool, Column: Column{Table: "memo", Name: "payload"}, JSONPath: []string{"property", "hasIncompleteTasks"}, AllowedComparisonOps: map[ComparisonOperator]bool{ CompareEq: true, CompareNeq: true, }, }, } envOptions := []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), cel.Variable("has_link", cel.BoolType), cel.Variable("has_code", cel.BoolType), cel.Variable("has_incomplete_tasks", cel.BoolType), nowFunction, } return Schema{ Name: "memo", Fields: fields, EnvOptions: envOptions, } } // NewAttachmentSchema constructs the attachment filter schema and CEL environment. func NewAttachmentSchema() Schema { fields := map[string]Field{ "filename": { Name: "filename", Kind: FieldKindScalar, Type: FieldTypeString, Column: Column{Table: "attachment", Name: "filename"}, SupportsContains: true, Expressions: map[DialectName]string{}, }, "mime_type": { Name: "mime_type", Kind: FieldKindScalar, Type: FieldTypeString, Column: Column{Table: "attachment", Name: "type"}, Expressions: map[DialectName]string{}, }, "create_time": { Name: "create_time", Kind: FieldKindScalar, Type: FieldTypeTimestamp, Column: Column{Table: "attachment", Name: "created_ts"}, Expressions: map[DialectName]string{ // MySQL stores created_ts as TIMESTAMP, needs conversion to epoch DialectMySQL: "UNIX_TIMESTAMP(%s)", // PostgreSQL and SQLite store created_ts as BIGINT (epoch), no conversion needed DialectPostgres: "%s", DialectSQLite: "%s", }, }, "memo_id": { Name: "memo_id", Kind: FieldKindScalar, Type: FieldTypeInt, Column: Column{Table: "attachment", Name: "memo_id"}, Expressions: map[DialectName]string{}, AllowedComparisonOps: map[ComparisonOperator]bool{ CompareEq: true, CompareNeq: true, }, }, } envOptions := []cel.EnvOption{ cel.Variable("filename", cel.StringType), cel.Variable("mime_type", cel.StringType), cel.Variable("create_time", cel.IntType), cel.Variable("memo_id", cel.AnyType), nowFunction, } return Schema{ Name: "attachment", Fields: fields, EnvOptions: envOptions, } } // columnExpr returns the field expression for the given dialect, applying // any schema-specific overrides (e.g. UNIX timestamp conversions). func (f Field) columnExpr(d DialectName) string { base := qualifyColumn(d, f.Column) if expr, ok := f.Expressions[d]; ok && expr != "" { return fmt.Sprintf(expr, base) } return base } ================================================ FILE: plugin/httpgetter/html_meta.go ================================================ 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) } } } } ================================================ FILE: plugin/httpgetter/html_meta_test.go ================================================ 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) } } ================================================ FILE: plugin/httpgetter/http_getter.go ================================================ package httpgetter ================================================ FILE: plugin/httpgetter/image.go ================================================ 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 } ================================================ FILE: plugin/httpgetter/util.go ================================================ 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 } ================================================ FILE: plugin/idp/idp.go ================================================ package idp type IdentityProviderUserInfo struct { Identifier string DisplayName string Email string AvatarURL string } ================================================ FILE: plugin/idp/oauth2/oauth2.go ================================================ // 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. // If codeVerifier is provided, it will be used for PKCE (Proof Key for Code Exchange) validation. func (p *IdentityProvider) ExchangeToken(ctx context.Context, redirectURL, code, codeVerifier 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, }, } // Prepare token exchange options opts := []oauth2.AuthCodeOption{} // Add PKCE code_verifier if provided if codeVerifier != "" { opts = append(opts, oauth2.SetAuthURLParam("code_verifier", codeVerifier)) } token, err := conf.Exchange(ctx, code, opts...) if err != nil { return "", errors.Wrap(err, "failed to exchange access token") } // Use the standard AccessToken field instead of Extra() // This is more reliable across different OAuth providers if token.AccessToken == "" { return "", errors.New("missing access token from authorization response") } return token.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 create 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") } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(err, "failed to read response body") } 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 } ================================================ FILE: plugin/idp/oauth2/oauth2_test.go ================================================ 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" // Test without PKCE (backward compatibility) 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) } ================================================ FILE: plugin/markdown/ast/tag.go ================================================ package ast import ( gast "github.com/yuin/goldmark/ast" ) // TagNode represents a #tag in the markdown AST. type TagNode struct { gast.BaseInline // Tag name without the # prefix Tag []byte } // KindTag is the NodeKind for TagNode. var KindTag = gast.NewNodeKind("Tag") // Kind returns KindTag. func (*TagNode) Kind() gast.NodeKind { return KindTag } // Dump implements Node.Dump for debugging. func (n *TagNode) Dump(source []byte, level int) { gast.DumpHelper(n, source, level, map[string]string{ "Tag": string(n.Tag), }, nil) } ================================================ FILE: plugin/markdown/extensions/tag.go ================================================ package extensions import ( "github.com/yuin/goldmark" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/util" mparser "github.com/usememos/memos/plugin/markdown/parser" ) type tagExtension struct{} // TagExtension is a goldmark extension for #tag syntax. var TagExtension = &tagExtension{} // Extend extends the goldmark parser with tag support. func (*tagExtension) Extend(m goldmark.Markdown) { m.Parser().AddOptions( parser.WithInlineParsers( // Priority 200 - run before standard link parser (500) util.Prioritized(mparser.NewTagParser(), 200), ), ) } ================================================ FILE: plugin/markdown/markdown.go ================================================ package markdown import ( "bytes" "strings" "github.com/yuin/goldmark" gast "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" east "github.com/yuin/goldmark/extension/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" mast "github.com/usememos/memos/plugin/markdown/ast" "github.com/usememos/memos/plugin/markdown/extensions" "github.com/usememos/memos/plugin/markdown/renderer" storepb "github.com/usememos/memos/proto/gen/store" ) // ExtractedData contains all metadata extracted from markdown in a single pass. type ExtractedData struct { Tags []string Property *storepb.MemoPayload_Property } // Service handles markdown metadata extraction. // It uses goldmark to parse markdown and extract tags, properties, and snippets. // HTML rendering is primarily done on frontend using markdown-it, but backend provides // RenderHTML for RSS feeds and other server-side rendering needs. type Service interface { // ExtractAll extracts tags, properties, and references in a single parse (most efficient) ExtractAll(content []byte) (*ExtractedData, error) // ExtractTags returns all #tags found in content ExtractTags(content []byte) ([]string, error) // ExtractProperties computes boolean properties ExtractProperties(content []byte) (*storepb.MemoPayload_Property, error) // RenderMarkdown renders goldmark AST back to markdown text RenderMarkdown(content []byte) (string, error) // RenderHTML renders markdown content to HTML RenderHTML(content []byte) (string, error) // GenerateSnippet creates plain text summary GenerateSnippet(content []byte, maxLength int) (string, error) // ValidateContent checks for syntax errors ValidateContent(content []byte) error // RenameTag renames all occurrences of oldTag to newTag in content RenameTag(content []byte, oldTag, newTag string) (string, error) } // service implements the Service interface. type service struct { md goldmark.Markdown } // Option configures the markdown service. type Option func(*config) type config struct { enableTags bool } // WithTagExtension enables #tag parsing. func WithTagExtension() Option { return func(c *config) { c.enableTags = true } } // NewService creates a new markdown service with the given options. func NewService(opts ...Option) Service { cfg := &config{} for _, opt := range opts { opt(cfg) } exts := []goldmark.Extender{ extension.GFM, // GitHub Flavored Markdown (tables, strikethrough, task lists, autolinks) } // Add custom extensions based on config if cfg.enableTags { exts = append(exts, extensions.TagExtension) } md := goldmark.New( goldmark.WithExtensions(exts...), goldmark.WithParserOptions( parser.WithAutoHeadingID(), // Generate heading IDs ), ) return &service{ md: md, } } // parse is an internal helper to parse content into AST. func (s *service) parse(content []byte) (gast.Node, error) { reader := text.NewReader(content) doc := s.md.Parser().Parse(reader) return doc, nil } // ExtractTags returns all #tags found in content. func (s *service) ExtractTags(content []byte) ([]string, error) { root, err := s.parse(content) if err != nil { return nil, err } var tags []string // Walk the AST to find tag nodes err = gast.Walk(root, func(n gast.Node, entering bool) (gast.WalkStatus, error) { if !entering { return gast.WalkContinue, nil } // Check for custom TagNode if tagNode, ok := n.(*mast.TagNode); ok { tags = append(tags, string(tagNode.Tag)) } return gast.WalkContinue, nil }) if err != nil { return nil, err } // Deduplicate tags while preserving original case return uniquePreserveCase(tags), nil } // extractHeadingText extracts plain text content from a heading node. func extractHeadingText(n gast.Node, source []byte) string { var buf strings.Builder for child := n.FirstChild(); child != nil; child = child.NextSibling() { extractTextFromNode(child, source, &buf) } return buf.String() } // extractTextFromNode recursively extracts plain text from a node and its children. func extractTextFromNode(n gast.Node, source []byte, buf *strings.Builder) { if textNode, ok := n.(*gast.Text); ok { buf.Write(textNode.Segment.Value(source)) return } for child := n.FirstChild(); child != nil; child = child.NextSibling() { extractTextFromNode(child, source, buf) } } // ExtractProperties computes boolean properties about the content. func (s *service) ExtractProperties(content []byte) (*storepb.MemoPayload_Property, error) { root, err := s.parse(content) if err != nil { return nil, err } prop := &storepb.MemoPayload_Property{} firstBlockChecked := false err = gast.Walk(root, func(n gast.Node, entering bool) (gast.WalkStatus, error) { if !entering { return gast.WalkContinue, nil } // Check if the first block-level child of the document is an H1 heading. if !firstBlockChecked && n.Parent() != nil && n.Parent().Kind() == gast.KindDocument { firstBlockChecked = true if heading, ok := n.(*gast.Heading); ok && heading.Level == 1 { prop.Title = extractHeadingText(n, content) } } switch n.Kind() { case gast.KindLink: prop.HasLink = true case gast.KindCodeBlock, gast.KindFencedCodeBlock, gast.KindCodeSpan: prop.HasCode = true case east.KindTaskCheckBox: prop.HasTaskList = true if checkBox, ok := n.(*east.TaskCheckBox); ok { if !checkBox.IsChecked { prop.HasIncompleteTasks = true } } default: // No special handling for other node types } return gast.WalkContinue, nil }) if err != nil { return nil, err } return prop, nil } // RenderMarkdown renders goldmark AST back to markdown text. func (s *service) RenderMarkdown(content []byte) (string, error) { root, err := s.parse(content) if err != nil { return "", err } mdRenderer := renderer.NewMarkdownRenderer() return mdRenderer.Render(root, content), nil } // RenderHTML renders markdown content to HTML using goldmark's built-in HTML renderer. func (s *service) RenderHTML(content []byte) (string, error) { var buf bytes.Buffer if err := s.md.Convert(content, &buf); err != nil { return "", err } return buf.String(), nil } // GenerateSnippet creates a plain text summary from markdown content. func (s *service) GenerateSnippet(content []byte, maxLength int) (string, error) { root, err := s.parse(content) if err != nil { return "", err } var buf strings.Builder var lastNodeWasBlock bool err = gast.Walk(root, func(n gast.Node, entering bool) (gast.WalkStatus, error) { if entering { // Skip code blocks entirely (but keep inline code spans for snippet text) switch n.Kind() { case gast.KindCodeBlock, gast.KindFencedCodeBlock: return gast.WalkSkipChildren, nil default: // Continue walking for other node types } // Add space before block elements (except first) switch n.Kind() { case gast.KindParagraph, gast.KindHeading, gast.KindListItem, east.KindTableCell, east.KindTableRow, east.KindTableHeader: if buf.Len() > 0 && lastNodeWasBlock { buf.WriteByte(' ') } default: // No space needed for other node types } } if !entering { // Mark that we just exited a block element switch n.Kind() { case gast.KindParagraph, gast.KindHeading, gast.KindListItem, east.KindTableCell, east.KindTableRow, east.KindTableHeader: lastNodeWasBlock = true default: // Not a block element } return gast.WalkContinue, nil } lastNodeWasBlock = false // Extract text from various node types switch node := n.(type) { case *gast.Text: segment := node.Segment buf.Write(segment.Value(content)) if node.SoftLineBreak() { buf.WriteByte(' ') } case *gast.AutoLink: buf.Write(node.URL(content)) return gast.WalkSkipChildren, nil case *mast.TagNode: buf.WriteByte('#') buf.Write(node.Tag) default: // Ignore other node types. } // Stop walking if we've exceeded double the max length // (we'll truncate precisely later) if buf.Len() > maxLength*2 { return gast.WalkStop, nil } return gast.WalkContinue, nil }) if err != nil { return "", err } snippet := buf.String() // Truncate at word boundary if needed if len(snippet) > maxLength { snippet = truncateAtWord(snippet, maxLength) } return strings.TrimSpace(snippet), nil } // ValidateContent checks if the markdown content is valid. func (s *service) ValidateContent(content []byte) error { // Try to parse the content _, err := s.parse(content) return err } // ExtractAll extracts tags, properties, and references in a single parse for efficiency. func (s *service) ExtractAll(content []byte) (*ExtractedData, error) { root, err := s.parse(content) if err != nil { return nil, err } data := &ExtractedData{ Tags: []string{}, Property: &storepb.MemoPayload_Property{}, } firstBlockChecked := false // Single walk to collect all data err = gast.Walk(root, func(n gast.Node, entering bool) (gast.WalkStatus, error) { if !entering { return gast.WalkContinue, nil } // Extract tags if tagNode, ok := n.(*mast.TagNode); ok { data.Tags = append(data.Tags, string(tagNode.Tag)) } // Check if the first block-level child of the document is an H1 heading. if !firstBlockChecked && n.Parent() != nil && n.Parent().Kind() == gast.KindDocument { firstBlockChecked = true if heading, ok := n.(*gast.Heading); ok && heading.Level == 1 { data.Property.Title = extractHeadingText(n, content) } } // Extract properties based on node kind switch n.Kind() { case gast.KindLink: data.Property.HasLink = true case gast.KindCodeBlock, gast.KindFencedCodeBlock, gast.KindCodeSpan: data.Property.HasCode = true case east.KindTaskCheckBox: data.Property.HasTaskList = true if checkBox, ok := n.(*east.TaskCheckBox); ok { if !checkBox.IsChecked { data.Property.HasIncompleteTasks = true } } default: // No special handling for other node types } return gast.WalkContinue, nil }) if err != nil { return nil, err } // Deduplicate tags while preserving original case data.Tags = uniquePreserveCase(data.Tags) return data, nil } // RenameTag renames all occurrences of oldTag to newTag in content. func (s *service) RenameTag(content []byte, oldTag, newTag string) (string, error) { root, err := s.parse(content) if err != nil { return "", err } // Walk the AST to find and rename tag nodes err = gast.Walk(root, func(n gast.Node, entering bool) (gast.WalkStatus, error) { if !entering { return gast.WalkContinue, nil } // Check for custom TagNode and rename if it matches if tagNode, ok := n.(*mast.TagNode); ok { if string(tagNode.Tag) == oldTag { tagNode.Tag = []byte(newTag) } } return gast.WalkContinue, nil }) if err != nil { return "", err } // Render back to markdown using the already-parsed AST mdRenderer := renderer.NewMarkdownRenderer() return mdRenderer.Render(root, content), nil } // uniquePreserveCase returns unique strings from input while preserving case. func uniquePreserveCase(strs []string) []string { seen := make(map[string]struct{}) var result []string for _, s := range strs { if _, exists := seen[s]; !exists { seen[s] = struct{}{} result = append(result, s) } } return result } // truncateAtWord truncates a string at the last word boundary before maxLength. // maxLength is treated as a rune (character) count to properly handle UTF-8 multi-byte characters. func truncateAtWord(s string, maxLength int) string { // Convert to runes to properly handle multi-byte UTF-8 characters runes := []rune(s) if len(runes) <= maxLength { return s } // Truncate to max length (by character count, not byte count) truncated := string(runes[:maxLength]) // Find last space to avoid cutting in the middle of a word lastSpace := strings.LastIndexAny(truncated, " \t\n\r") if lastSpace > 0 { truncated = truncated[:lastSpace] } return truncated + " ..." } ================================================ FILE: plugin/markdown/markdown_test.go ================================================ package markdown import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewService(t *testing.T) { svc := NewService() assert.NotNil(t, svc) } func TestValidateContent(t *testing.T) { svc := NewService() tests := []struct { name string content string wantErr bool }{ { name: "valid markdown", content: "# Hello\n\nThis is **bold** text.", wantErr: false, }, { name: "empty content", content: "", wantErr: false, }, { name: "complex markdown", content: "# Title\n\n- List item 1\n- List item 2\n\n```go\ncode block\n```", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := svc.ValidateContent([]byte(tt.content)) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestGenerateSnippet(t *testing.T) { svc := NewService() tests := []struct { name string content string maxLength int expected string }{ { name: "simple text", content: "Hello world", maxLength: 100, expected: "Hello world", }, { name: "text with formatting", content: "This is **bold** and *italic* text.", maxLength: 100, expected: "This is bold and italic text.", }, { name: "truncate long text", content: "This is a very long piece of text that should be truncated at a word boundary.", maxLength: 30, expected: "This is a very long piece of ...", }, { name: "heading and paragraph", content: "# My Title\n\nThis is the first paragraph.", maxLength: 100, expected: "My Title This is the first paragraph.", }, { name: "code block removed", content: "Text before\n\n```go\ncode\n```\n\nText after", maxLength: 100, expected: "Text before Text after", }, { name: "list items", content: "- Item 1\n- Item 2\n- Item 3", maxLength: 100, expected: "Item 1 Item 2 Item 3", }, { name: "inline code preserved", content: "`console.log('hello')`", maxLength: 100, expected: "console.log('hello')", }, { name: "text with inline code", content: "Use `fmt.Println` to print output.", maxLength: 100, expected: "Use fmt.Println to print output.", }, { name: "image alt text", content: "![alt text](https://example.com/img.png)", maxLength: 100, expected: "alt text", }, { name: "strikethrough text", content: "~~deleted text~~", maxLength: 100, expected: "deleted text", }, { name: "blockquote", content: "> quoted text", maxLength: 100, expected: "quoted text", }, { name: "table cells spaced", content: "| a | b |\n|---|---|\n| 1 | 2 |", maxLength: 100, expected: "a b 1 2", }, { name: "plain URL autolink", content: "https://usememos.com", maxLength: 100, expected: "https://usememos.com", }, { name: "text with plain URL", content: "Check out https://usememos.com for more info.", maxLength: 100, expected: "Check out https://usememos.com for more info.", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { snippet, err := svc.GenerateSnippet([]byte(tt.content), tt.maxLength) require.NoError(t, err) assert.Equal(t, tt.expected, snippet) }) } // Test with tag extension enabled (matches production config). svcWithTags := NewService(WithTagExtension()) tagTests := []struct { name string content string maxLength int expected string }{ { name: "tag only", content: "#todo", maxLength: 100, expected: "#todo", }, { name: "text with tags", content: "Remember to #review the #code", maxLength: 100, expected: "Remember to #review the #code", }, } for _, tt := range tagTests { t.Run(tt.name, func(t *testing.T) { snippet, err := svcWithTags.GenerateSnippet([]byte(tt.content), tt.maxLength) require.NoError(t, err) assert.Equal(t, tt.expected, snippet) }) } } func TestExtractProperties(t *testing.T) { tests := []struct { name string content string hasLink bool hasCode bool hasTasks bool hasInc bool title string }{ { name: "plain text", content: "Just plain text", hasLink: false, hasCode: false, hasTasks: false, hasInc: false, title: "", }, { name: "with link", content: "Check out [this link](https://example.com)", hasLink: true, hasCode: false, hasTasks: false, hasInc: false, title: "", }, { name: "with inline code", content: "Use `console.log()` to debug", hasLink: false, hasCode: true, hasTasks: false, hasInc: false, title: "", }, { name: "with code block", content: "```go\nfunc main() {}\n```", hasLink: false, hasCode: true, hasTasks: false, hasInc: false, title: "", }, { name: "with completed task", content: "- [x] Completed task", hasLink: false, hasCode: false, hasTasks: true, hasInc: false, title: "", }, { name: "with incomplete task", content: "- [ ] Todo item", hasLink: false, hasCode: false, hasTasks: true, hasInc: true, title: "", }, { name: "mixed tasks", content: "- [x] Done\n- [ ] Not done", hasLink: false, hasCode: false, hasTasks: true, hasInc: true, title: "", }, { name: "everything", content: "# Title\n\n[Link](url)\n\n`code`\n\n- [ ] Task", hasLink: true, hasCode: true, hasTasks: true, hasInc: true, title: "Title", }, { name: "h1 as first node extracts title", content: "# My Article Title\n\nBody text here.", title: "My Article Title", }, { name: "h2 as first node does not extract title", content: "## Sub Heading\n\nBody text.", title: "", }, { name: "h1 not first node does not extract title", content: "Some text\n\n# Heading Later", title: "", }, { name: "h1 with inline formatting extracts plain text", content: "# Title with **bold** and *italic*\n\nBody.", title: "Title with bold and italic", }, { name: "empty content has no title", content: "", title: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svc := NewService() props, err := svc.ExtractProperties([]byte(tt.content)) require.NoError(t, err) assert.Equal(t, tt.hasLink, props.HasLink, "HasLink") assert.Equal(t, tt.hasCode, props.HasCode, "HasCode") assert.Equal(t, tt.hasTasks, props.HasTaskList, "HasTaskList") assert.Equal(t, tt.hasInc, props.HasIncompleteTasks, "HasIncompleteTasks") assert.Equal(t, tt.title, props.Title, "Title") }) } } func TestExtractAllTitle(t *testing.T) { svc := NewService(WithTagExtension()) tests := []struct { name string content string title string }{ { name: "h1 first node", content: "# Article Title\n\nContent with #tag", title: "Article Title", }, { name: "no h1", content: "Just text with #tag", title: "", }, { name: "h1 not first", content: "Intro\n\n# Late Heading", title: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { data, err := svc.ExtractAll([]byte(tt.content)) require.NoError(t, err) assert.Equal(t, tt.title, data.Property.Title, "Title") }) } } func TestExtractTags(t *testing.T) { tests := []struct { name string content string withExt bool expected []string }{ { name: "no tags", content: "Just plain text", withExt: false, expected: []string{}, }, { name: "single tag", content: "Text with #tag", withExt: true, expected: []string{"tag"}, }, { name: "multiple tags", content: "Text with #tag1 and #tag2", withExt: true, expected: []string{"tag1", "tag2"}, }, { name: "duplicate tags", content: "#work is important. #Work #WORK", withExt: true, expected: []string{"work", "Work", "WORK"}, }, { name: "tags with hyphens and underscores", content: "Tags: #work-notes #2024_plans", withExt: true, expected: []string{"work-notes", "2024_plans"}, }, { name: "tags at end of sentence", content: "This is important #urgent.", withExt: true, expected: []string{"urgent"}, }, { name: "headings not tags", content: "## Heading\n\n# Title\n\nText with #realtag", withExt: true, expected: []string{"realtag"}, }, { name: "numeric tag", content: "Issue #123", withExt: true, expected: []string{"123"}, }, { name: "tag in list", content: "- Item 1 #todo\n- Item 2 #done", withExt: true, expected: []string{"todo", "done"}, }, { name: "no extension enabled", content: "Text with #tag", withExt: false, expected: []string{}, }, { name: "Chinese tag", content: "Text with #测试", withExt: true, expected: []string{"测试"}, }, { name: "Chinese tag followed by punctuation", content: "Text #测试。 More text", withExt: true, expected: []string{"测试"}, }, { name: "mixed Chinese and ASCII tag", content: "#测试test123 content", withExt: true, expected: []string{"测试test123"}, }, { name: "Japanese tag", content: "#日本語 content", withExt: true, expected: []string{"日本語"}, }, { name: "Korean tag", content: "#한국어 content", withExt: true, expected: []string{"한국어"}, }, { name: "hierarchical tag with Chinese", content: "#work/测试/项目", withExt: true, expected: []string{"work/测试/项目"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var svc Service if tt.withExt { svc = NewService(WithTagExtension()) } else { svc = NewService() } tags, err := svc.ExtractTags([]byte(tt.content)) require.NoError(t, err) assert.ElementsMatch(t, tt.expected, tags) }) } } func TestUniquePreserveCase(t *testing.T) { tests := []struct { name string input []string expected []string }{ { name: "empty", input: []string{}, expected: []string{}, }, { name: "unique items", input: []string{"tag1", "tag2", "tag3"}, expected: []string{"tag1", "tag2", "tag3"}, }, { name: "duplicates", input: []string{"tag", "TAG", "Tag"}, expected: []string{"tag", "TAG", "Tag"}, }, { name: "mixed", input: []string{"Work", "work", "Important", "work"}, expected: []string{"Work", "work", "Important"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := uniquePreserveCase(tt.input) assert.ElementsMatch(t, tt.expected, result) }) } } func TestTruncateAtWord(t *testing.T) { tests := []struct { name string input string maxLength int expected string }{ { name: "no truncation needed", input: "short", maxLength: 10, expected: "short", }, { name: "exact length", input: "exactly ten", maxLength: 11, expected: "exactly ten", }, { name: "truncate at word", input: "this is a long sentence", maxLength: 10, expected: "this is a ...", }, { name: "truncate very long word", input: "supercalifragilisticexpialidocious", maxLength: 10, expected: "supercalif ...", }, { name: "CJK characters without spaces", input: "这是一个很长的中文句子没有空格的情况下也要正确处理", maxLength: 15, expected: "这是一个很长的中文句子没有空格 ...", }, { name: "mixed CJK and Latin", input: "这是中文mixed with English文字", maxLength: 10, expected: "这是中文mixed ...", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := truncateAtWord(tt.input, tt.maxLength) assert.Equal(t, tt.expected, result) }) } } // Benchmark tests. func BenchmarkGenerateSnippet(b *testing.B) { svc := NewService() content := []byte(`# Large Document This is a large document with multiple paragraphs and formatting. ## Section 1 Here is some **bold** text and *italic* text with [links](https://example.com). - List item 1 - List item 2 - List item 3 ## Section 2 More content here with ` + "`inline code`" + ` and other elements. ` + "```go\nfunc example() {\n return true\n}\n```") b.ResetTimer() for i := 0; i < b.N; i++ { _, err := svc.GenerateSnippet(content, 200) if err != nil { b.Fatal(err) } } } func BenchmarkExtractProperties(b *testing.B) { svc := NewService() content := []byte("# Title\n\n[Link](url)\n\n`code`\n\n- [ ] Task\n- [x] Done") b.ResetTimer() for i := 0; i < b.N; i++ { _, err := svc.ExtractProperties(content) if err != nil { b.Fatal(err) } } } ================================================ FILE: plugin/markdown/parser/tag.go ================================================ package parser import ( "unicode" "unicode/utf8" gast "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" mast "github.com/usememos/memos/plugin/markdown/ast" ) const ( // MaxTagLength defines the maximum number of runes allowed in a tag. MaxTagLength = 100 ) type tagParser struct{} // NewTagParser creates a new inline parser for #tag syntax. func NewTagParser() parser.InlineParser { return &tagParser{} } // Trigger returns the characters that trigger this parser. func (*tagParser) Trigger() []byte { return []byte{'#'} } // isValidTagRune checks if a Unicode rune is valid in a tag. // Uses Unicode categories for proper international character support. func isValidTagRune(r rune) bool { // Allow Unicode letters (any script: Latin, CJK, Arabic, Cyrillic, etc.) if unicode.IsLetter(r) { return true } // Allow Unicode digits if unicode.IsNumber(r) { return true } // Allow emoji and symbols (So category: Symbol, Other) // This includes emoji, which are essential for social media-style tagging if unicode.IsSymbol(r) { return true } // Allow marks (non-spacing, spacing combining, enclosing) // This covers variation selectors (e.g. VS16 \uFE0F) and combining marks (e.g. Keycap \u20E3, accents) if unicode.IsMark(r) { return true } // Allow Zero Width Joiner (ZWJ) for emoji sequences if r == '\u200D' { return true } // Allow specific ASCII symbols for tag structure // Underscore: word separation (snake_case) // Hyphen: word separation (kebab-case) // Forward slash: hierarchical tags (category/subcategory) // Ampersand: compound tags (science&tech) if r == '_' || r == '-' || r == '/' || r == '&' { return true } return false } // Parse parses #tag syntax using Unicode-aware validation. // Tags support international characters and follow these rules: // - Must start with # followed by valid tag characters // - Valid characters: Unicode letters, Unicode digits, underscore (_), hyphen (-), forward slash (/) // - Maximum length: 100 runes (Unicode characters) // - Stops at: whitespace, punctuation, or other invalid characters func (*tagParser) Parse(_ gast.Node, block text.Reader, _ parser.Context) gast.Node { line, _ := block.PeekLine() // Must start with # if len(line) == 0 || line[0] != '#' { return nil } // Check if it's a heading (## or space after #) if len(line) > 1 { if line[1] == '#' { // It's a heading (##), not a tag return nil } if line[1] == ' ' { // Space after # - heading or just a hash return nil } } else { // Just a lone # return nil } // Parse tag using UTF-8 aware rune iteration tagStart := 1 pos := tagStart runeCount := 0 for pos < len(line) { r, size := utf8.DecodeRune(line[pos:]) // Stop at invalid UTF-8 if r == utf8.RuneError && size == 1 { break } // Validate character using Unicode categories if !isValidTagRune(r) { break } // Enforce max length (by rune count, not byte count) runeCount++ if runeCount > MaxTagLength { break } pos += size } // Must have at least one character after # if pos <= tagStart { return nil } // Extract tag (without #) tagName := line[tagStart:pos] // Make a copy of the tag name tagCopy := make([]byte, len(tagName)) copy(tagCopy, tagName) // Advance reader block.Advance(pos) // Create node node := &mast.TagNode{ Tag: tagCopy, } return node } ================================================ FILE: plugin/markdown/parser/tag_test.go ================================================ package parser import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" mast "github.com/usememos/memos/plugin/markdown/ast" ) func TestTagParser(t *testing.T) { tests := []struct { name string input string expectedTag string shouldParse bool }{ { name: "basic tag", input: "#tag", expectedTag: "tag", shouldParse: true, }, { name: "tag with hyphen", input: "#work-notes", expectedTag: "work-notes", shouldParse: true, }, { name: "tag with ampersand", input: "#science&tech", expectedTag: "science&tech", shouldParse: true, }, { name: "tag with underscore", input: "#2024_plans", expectedTag: "2024_plans", shouldParse: true, }, { name: "numeric tag", input: "#123", expectedTag: "123", shouldParse: true, }, { name: "tag followed by space", input: "#tag ", expectedTag: "tag", shouldParse: true, }, { name: "tag followed by punctuation", input: "#tag.", expectedTag: "tag", shouldParse: true, }, { name: "tag in sentence", input: "#important task", expectedTag: "important", shouldParse: true, }, { name: "heading (##)", input: "## Heading", expectedTag: "", shouldParse: false, }, { name: "space after hash", input: "# heading", expectedTag: "", shouldParse: false, }, { name: "lone hash", input: "#", expectedTag: "", shouldParse: false, }, { name: "hash with space", input: "# ", expectedTag: "", shouldParse: false, }, { name: "special characters", input: "#tag@special", expectedTag: "tag", shouldParse: true, }, { name: "mixed case", input: "#WorkNotes", expectedTag: "WorkNotes", shouldParse: true, }, { name: "hierarchical tag with slash", input: "#tag1/subtag", expectedTag: "tag1/subtag", shouldParse: true, }, { name: "hierarchical tag with multiple levels", input: "#tag1/subtag/subtag2", expectedTag: "tag1/subtag/subtag2", shouldParse: true, }, { name: "hierarchical tag followed by space", input: "#work/notes ", expectedTag: "work/notes", shouldParse: true, }, { name: "hierarchical tag followed by punctuation", input: "#project/2024.", expectedTag: "project/2024", shouldParse: true, }, { name: "hierarchical tag with numbers and dashes", input: "#work-log/2024/q1", expectedTag: "work-log/2024/q1", shouldParse: true, }, { name: "Chinese characters", input: "#测试", expectedTag: "测试", shouldParse: true, }, { name: "Chinese tag followed by space", input: "#测试 some text", expectedTag: "测试", shouldParse: true, }, { name: "Chinese tag followed by punctuation", input: "#测试。", expectedTag: "测试", shouldParse: true, }, { name: "mixed Chinese and ASCII", input: "#测试test123", expectedTag: "测试test123", shouldParse: true, }, { name: "Japanese characters", input: "#テスト", expectedTag: "テスト", shouldParse: true, }, { name: "Korean characters", input: "#테스트", expectedTag: "테스트", shouldParse: true, }, { name: "emoji", input: "#test🚀", expectedTag: "test🚀", shouldParse: true, }, { name: "emoji with VS16", input: "#test👁️", // Eye + VS16 expectedTag: "test👁️", shouldParse: true, }, { name: "emoji with ZWJ sequence", input: "#family👨‍👩‍👧‍👦", // Family ZWJ sequence expectedTag: "family👨‍👩‍👧‍👦", shouldParse: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := NewTagParser() reader := text.NewReader([]byte(tt.input)) ctx := parser.NewContext() node := p.Parse(nil, reader, ctx) if tt.shouldParse { require.NotNil(t, node, "Expected tag to be parsed") require.IsType(t, &mast.TagNode{}, node) tagNode, ok := node.(*mast.TagNode) require.True(t, ok, "Expected node to be *mast.TagNode") assert.Equal(t, tt.expectedTag, string(tagNode.Tag)) } else { assert.Nil(t, node, "Expected tag NOT to be parsed") } }) } } func TestTagParser_Trigger(t *testing.T) { p := NewTagParser() triggers := p.Trigger() assert.Equal(t, []byte{'#'}, triggers) } func TestTagParser_MultipleTags(t *testing.T) { // Test that parser correctly handles multiple tags in sequence input := "#tag1 #tag2" p := NewTagParser() reader := text.NewReader([]byte(input)) ctx := parser.NewContext() // Parse first tag node1 := p.Parse(nil, reader, ctx) require.NotNil(t, node1) tagNode1, ok := node1.(*mast.TagNode) require.True(t, ok, "Expected node1 to be *mast.TagNode") assert.Equal(t, "tag1", string(tagNode1.Tag)) // Advance past the space reader.Advance(1) // Parse second tag node2 := p.Parse(nil, reader, ctx) require.NotNil(t, node2) tagNode2, ok := node2.(*mast.TagNode) require.True(t, ok, "Expected node2 to be *mast.TagNode") assert.Equal(t, "tag2", string(tagNode2.Tag)) } func TestTagNode_Kind(t *testing.T) { node := &mast.TagNode{ Tag: []byte("test"), } assert.Equal(t, mast.KindTag, node.Kind()) } func TestTagNode_Dump(t *testing.T) { node := &mast.TagNode{ Tag: []byte("test"), } // Should not panic assert.NotPanics(t, func() { node.Dump([]byte("#test"), 0) }) } ================================================ FILE: plugin/markdown/renderer/markdown_renderer.go ================================================ package renderer import ( "bytes" "fmt" "strings" gast "github.com/yuin/goldmark/ast" east "github.com/yuin/goldmark/extension/ast" mast "github.com/usememos/memos/plugin/markdown/ast" ) // MarkdownRenderer renders goldmark AST back to markdown text. type MarkdownRenderer struct { buf *bytes.Buffer } // NewMarkdownRenderer creates a new markdown renderer. func NewMarkdownRenderer() *MarkdownRenderer { return &MarkdownRenderer{ buf: &bytes.Buffer{}, } } // Render renders the AST node to markdown and returns the result. func (r *MarkdownRenderer) Render(node gast.Node, source []byte) string { r.buf.Reset() r.renderNode(node, source, 0) return r.buf.String() } // renderNode renders a single node and its children. func (r *MarkdownRenderer) renderNode(node gast.Node, source []byte, depth int) { switch n := node.(type) { case *gast.Document: r.renderChildren(n, source, depth) case *gast.Paragraph: r.renderChildren(n, source, depth) if node.NextSibling() != nil { r.buf.WriteString("\n\n") } case *gast.Text: // Text nodes store their content as segments in the source segment := n.Segment r.buf.Write(segment.Value(source)) if n.SoftLineBreak() { r.buf.WriteByte('\n') } else if n.HardLineBreak() { r.buf.WriteString(" \n") } case *gast.CodeSpan: r.buf.WriteByte('`') r.renderChildren(n, source, depth) r.buf.WriteByte('`') case *gast.Emphasis: symbol := "*" if n.Level == 2 { symbol = "**" } r.buf.WriteString(symbol) r.renderChildren(n, source, depth) r.buf.WriteString(symbol) case *gast.Link: r.buf.WriteString("[") r.renderChildren(n, source, depth) r.buf.WriteString("](") r.buf.Write(n.Destination) if len(n.Title) > 0 { r.buf.WriteString(` "`) r.buf.Write(n.Title) r.buf.WriteString(`"`) } r.buf.WriteString(")") case *gast.AutoLink: url := n.URL(source) if n.AutoLinkType == gast.AutoLinkEmail { r.buf.WriteString("<") r.buf.Write(url) r.buf.WriteString(">") } else { r.buf.Write(url) } case *gast.Image: r.buf.WriteString("![") r.renderChildren(n, source, depth) r.buf.WriteString("](") r.buf.Write(n.Destination) if len(n.Title) > 0 { r.buf.WriteString(` "`) r.buf.Write(n.Title) r.buf.WriteString(`"`) } r.buf.WriteString(")") case *gast.Heading: r.buf.WriteString(strings.Repeat("#", n.Level)) r.buf.WriteByte(' ') r.renderChildren(n, source, depth) if node.NextSibling() != nil { r.buf.WriteString("\n\n") } case *gast.CodeBlock, *gast.FencedCodeBlock: r.renderCodeBlock(n, source) case *gast.Blockquote: // Render each child line with "> " prefix r.renderBlockquote(n, source, depth) if node.NextSibling() != nil { r.buf.WriteString("\n\n") } case *gast.List: r.renderChildren(n, source, depth) if node.NextSibling() != nil { r.buf.WriteString("\n\n") } case *gast.ListItem: r.renderListItem(n, source, depth) case *gast.ThematicBreak: r.buf.WriteString("---") if node.NextSibling() != nil { r.buf.WriteString("\n\n") } case *east.Strikethrough: r.buf.WriteString("~~") r.renderChildren(n, source, depth) r.buf.WriteString("~~") case *east.TaskCheckBox: if n.IsChecked { r.buf.WriteString("[x] ") } else { r.buf.WriteString("[ ] ") } case *east.Table: r.renderTable(n, source) if node.NextSibling() != nil { r.buf.WriteString("\n\n") } // Custom Memos nodes case *mast.TagNode: r.buf.WriteByte('#') r.buf.Write(n.Tag) default: // For unknown nodes, try to render children r.renderChildren(n, source, depth) } } // renderChildren renders all children of a node. func (r *MarkdownRenderer) renderChildren(node gast.Node, source []byte, depth int) { child := node.FirstChild() for child != nil { r.renderNode(child, source, depth+1) child = child.NextSibling() } } // renderCodeBlock renders a code block. func (r *MarkdownRenderer) renderCodeBlock(node gast.Node, source []byte) { if fenced, ok := node.(*gast.FencedCodeBlock); ok { // Fenced code block with language r.buf.WriteString("```") if lang := fenced.Language(source); len(lang) > 0 { r.buf.Write(lang) } r.buf.WriteByte('\n') // Write all lines lines := fenced.Lines() for i := 0; i < lines.Len(); i++ { line := lines.At(i) r.buf.Write(line.Value(source)) } r.buf.WriteString("```") if node.NextSibling() != nil { r.buf.WriteString("\n\n") } } else if codeBlock, ok := node.(*gast.CodeBlock); ok { // Indented code block lines := codeBlock.Lines() for i := 0; i < lines.Len(); i++ { line := lines.At(i) r.buf.WriteString(" ") r.buf.Write(line.Value(source)) } if node.NextSibling() != nil { r.buf.WriteString("\n\n") } } } // renderBlockquote renders a blockquote with "> " prefix. func (r *MarkdownRenderer) renderBlockquote(node *gast.Blockquote, source []byte, depth int) { // Create a temporary buffer for the blockquote content tempBuf := &bytes.Buffer{} tempRenderer := &MarkdownRenderer{buf: tempBuf} tempRenderer.renderChildren(node, source, depth) // Add "> " prefix to each line content := tempBuf.String() lines := strings.Split(strings.TrimRight(content, "\n"), "\n") for i, line := range lines { r.buf.WriteString("> ") r.buf.WriteString(line) if i < len(lines)-1 { r.buf.WriteByte('\n') } } } // renderListItem renders a list item with proper indentation and markers. func (r *MarkdownRenderer) renderListItem(node *gast.ListItem, source []byte, depth int) { parent := node.Parent() list, ok := parent.(*gast.List) if !ok { r.renderChildren(node, source, depth) return } // Add indentation only for nested lists // Document=0, List=1, ListItem=2 (no indent), nested ListItem=3+ (indent) if depth > 2 { indent := strings.Repeat(" ", depth-2) r.buf.WriteString(indent) } // Add list marker if list.IsOrdered() { fmt.Fprintf(r.buf, "%d. ", list.Start) list.Start++ // Increment for next item } else { r.buf.WriteString("- ") } // Render content r.renderChildren(node, source, depth) // Add newline if there's a next sibling if node.NextSibling() != nil { r.buf.WriteByte('\n') } } // renderTable renders a table in markdown format. func (r *MarkdownRenderer) renderTable(table *east.Table, source []byte) { // This is a simplified table renderer // A full implementation would need to handle alignment, etc. r.renderChildren(table, source, 0) } ================================================ FILE: plugin/markdown/renderer/markdown_renderer_test.go ================================================ package renderer import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" "github.com/usememos/memos/plugin/markdown/extensions" ) func TestMarkdownRenderer(t *testing.T) { // Create goldmark instance with all extensions md := goldmark.New( goldmark.WithExtensions( extension.GFM, extensions.TagExtension, ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), ) tests := []struct { name string input string expected string }{ { name: "simple text", input: "Hello world", expected: "Hello world", }, { name: "paragraph with newlines", input: "First paragraph\n\nSecond paragraph", expected: "First paragraph\n\nSecond paragraph", }, { name: "emphasis", input: "This is *italic* and **bold** text", expected: "This is *italic* and **bold** text", }, { name: "headings", input: "# Heading 1\n\n## Heading 2\n\n### Heading 3", expected: "# Heading 1\n\n## Heading 2\n\n### Heading 3", }, { name: "link", input: "Check [this link](https://example.com)", expected: "Check [this link](https://example.com)", }, { name: "image", input: "![alt text](image.png)", expected: "![alt text](image.png)", }, { name: "code inline", input: "This is `inline code` here", expected: "This is `inline code` here", }, { name: "code block fenced", input: "```go\nfunc main() {\n}\n```", expected: "```go\nfunc main() {\n}\n```", }, { name: "unordered list", input: "- Item 1\n- Item 2\n- Item 3", expected: "- Item 1\n- Item 2\n- Item 3", }, { name: "ordered list", input: "1. First\n2. Second\n3. Third", expected: "1. First\n2. Second\n3. Third", }, { name: "blockquote", input: "> This is a quote\n> Second line", expected: "> This is a quote\n> Second line", }, { name: "horizontal rule", input: "Text before\n\n---\n\nText after", expected: "Text before\n\n---\n\nText after", }, { name: "strikethrough", input: "This is ~~deleted~~ text", expected: "This is ~~deleted~~ text", }, { name: "task list", input: "- [x] Completed task\n- [ ] Incomplete task", expected: "- [x] Completed task\n- [ ] Incomplete task", }, { name: "tag", input: "This has #tag in it", expected: "This has #tag in it", }, { name: "multiple tags", input: "#work #important meeting notes", expected: "#work #important meeting notes", }, { name: "complex mixed content", input: "# Meeting Notes\n\n**Date**: 2024-01-01\n\n## Attendees\n- Alice\n- Bob\n\n## Discussion\n\nWe discussed #project status.\n\n```python\nprint('hello')\n```", expected: "# Meeting Notes\n\n**Date**: 2024-01-01\n\n## Attendees\n\n- Alice\n- Bob\n\n## Discussion\n\nWe discussed #project status.\n\n```python\nprint('hello')\n```", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Parse the input source := []byte(tt.input) reader := text.NewReader(source) doc := md.Parser().Parse(reader) require.NotNil(t, doc) // Render back to markdown renderer := NewMarkdownRenderer() result := renderer.Render(doc, source) // For debugging if result != tt.expected { t.Logf("Input: %q", tt.input) t.Logf("Expected: %q", tt.expected) t.Logf("Got: %q", result) } assert.Equal(t, tt.expected, result) }) } } func TestMarkdownRendererPreservesStructure(t *testing.T) { // Test that parsing and rendering preserves structure md := goldmark.New( goldmark.WithExtensions( extension.GFM, extensions.TagExtension, ), ) inputs := []string{ "# Title\n\nParagraph", "**Bold** and *italic*", "- List\n- Items", "#tag #another", "> Quote", } renderer := NewMarkdownRenderer() for _, input := range inputs { t.Run(input, func(t *testing.T) { source := []byte(input) reader := text.NewReader(source) doc := md.Parser().Parse(reader) result := renderer.Render(doc, source) // The result should be structurally similar // (may have minor formatting differences) assert.NotEmpty(t, result) }) } } ================================================ FILE: plugin/scheduler/README.md ================================================ # Scheduler Plugin A production-ready, GitHub Actions-inspired cron job scheduler for Go. ## Features - **Standard Cron Syntax**: Supports both 5-field and 6-field (with seconds) cron expressions - **Timezone-Aware**: Explicit timezone handling to avoid DST surprises - **Middleware Pattern**: Composable job wrappers for logging, metrics, panic recovery, timeouts - **Graceful Shutdown**: Jobs complete cleanly or cancel when context expires - **Zero Dependencies**: Core functionality uses only the standard library - **Type-Safe**: Strong typing with clear error messages - **Well-Tested**: Comprehensive test coverage ## Installation This package is included with Memos. No separate installation required. ## Quick Start ```go package main import ( "context" "fmt" "github.com/usememos/memos/plugin/scheduler" ) func main() { s := scheduler.New() s.Register(&scheduler.Job{ Name: "daily-cleanup", Schedule: "0 2 * * *", // 2 AM daily Handler: func(ctx context.Context) error { fmt.Println("Running cleanup...") return nil }, }) s.Start() defer s.Stop(context.Background()) // Keep running... select {} } ``` ## Cron Expression Format ### 5-Field Format (Standard) ``` ┌───────────── minute (0 - 59) │ ┌───────────── hour (0 - 23) │ │ ┌───────────── day of month (1 - 31) │ │ │ ┌───────────── month (1 - 12) │ │ │ │ ┌───────────── day of week (0 - 7) (Sunday = 0 or 7) │ │ │ │ │ * * * * * ``` ### 6-Field Format (With Seconds) ``` ┌───────────── second (0 - 59) │ ┌───────────── minute (0 - 59) │ │ ┌───────────── hour (0 - 23) │ │ │ ┌───────────── day of month (1 - 31) │ │ │ │ ┌───────────── month (1 - 12) │ │ │ │ │ ┌───────────── day of week (0 - 7) │ │ │ │ │ │ * * * * * * ``` ### Special Characters - `*` - Any value (every minute, every hour, etc.) - `,` - List of values: `1,15,30` (1st, 15th, and 30th) - `-` - Range: `9-17` (9 AM through 5 PM) - `/` - Step: `*/15` (every 15 units) ### Common Examples | Schedule | Description | |----------|-------------| | `* * * * *` | Every minute | | `0 * * * *` | Every hour | | `0 0 * * *` | Daily at midnight | | `0 9 * * 1-5` | Weekdays at 9 AM | | `*/15 * * * *` | Every 15 minutes | | `0 0 1 * *` | First day of every month | | `0 0 * * 0` | Every Sunday at midnight | | `30 14 * * *` | Every day at 2:30 PM | ## Timezone Support ```go // Global timezone for all jobs s := scheduler.New( scheduler.WithTimezone("America/New_York"), ) // Per-job timezone (overrides global) s.Register(&scheduler.Job{ Name: "tokyo-report", Schedule: "0 9 * * *", // 9 AM Tokyo time Timezone: "Asia/Tokyo", Handler: func(ctx context.Context) error { // Runs at 9 AM in Tokyo return nil }, }) ``` **Important**: Always use IANA timezone names (`America/New_York`, not `EST`). ## Middleware Middleware wraps job handlers to add cross-cutting behavior. Multiple middleware can be chained together. ### Built-in Middleware #### Recovery (Panic Handling) ```go s := scheduler.New( scheduler.WithMiddleware( scheduler.Recovery(func(jobName string, r interface{}) { log.Printf("Job %s panicked: %v", jobName, r) }), ), ) ``` #### Logging ```go type Logger interface { Info(msg string, args ...interface{}) Error(msg string, args ...interface{}) } s := scheduler.New( scheduler.WithMiddleware( scheduler.Logging(myLogger), ), ) ``` #### Timeout ```go s := scheduler.New( scheduler.WithMiddleware( scheduler.Timeout(5 * time.Minute), ), ) ``` ### Combining Middleware ```go s := scheduler.New( scheduler.WithMiddleware( scheduler.Recovery(panicHandler), scheduler.Logging(logger), scheduler.Timeout(10 * time.Minute), ), ) ``` **Order matters**: Middleware are applied left-to-right. In the example above: 1. Recovery (outermost) catches panics from everything 2. Logging logs the execution 3. Timeout (innermost) wraps the actual handler ### Custom Middleware ```go func Metrics(recorder MetricsRecorder) scheduler.Middleware { return func(next scheduler.JobHandler) scheduler.JobHandler { return func(ctx context.Context) error { start := time.Now() err := next(ctx) duration := time.Since(start) jobName := scheduler.GetJobName(ctx) recorder.Record(jobName, duration, err) return err } } } ``` ## Graceful Shutdown Always use `Stop()` with a context to allow jobs to finish cleanly: ```go // Give jobs up to 30 seconds to complete ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := s.Stop(ctx); err != nil { log.Printf("Shutdown error: %v", err) } ``` Jobs should respect context cancellation: ```go Handler: func(ctx context.Context) error { for i := 0; i < 100; i++ { select { case <-ctx.Done(): return ctx.Err() // Canceled default: // Do work } } return nil } ``` ## Best Practices ### 1. Always Name Your Jobs Names are used for logging, metrics, and debugging: ```go Name: "user-cleanup-job" // Good Name: "job1" // Bad ``` ### 2. Add Descriptions and Tags ```go s.Register(&scheduler.Job{ Name: "stale-session-cleanup", Description: "Removes user sessions older than 30 days", Tags: []string{"maintenance", "security"}, Schedule: "0 3 * * *", Handler: cleanupSessions, }) ``` ### 3. Use Appropriate Middleware Always include Recovery and Logging in production: ```go scheduler.New( scheduler.WithMiddleware( scheduler.Recovery(logPanic), scheduler.Logging(logger), ), ) ``` ### 4. Avoid Scheduling Exactly on the Hour Many systems schedule jobs at `:00`, causing load spikes. Stagger your jobs: ```go "5 2 * * *" // 2:05 AM (good) "0 2 * * *" // 2:00 AM (often overloaded) ``` ### 5. Make Jobs Idempotent Jobs may run multiple times (crash recovery, etc.). Design them to be safely re-runnable: ```go Handler: func(ctx context.Context) error { // Use unique constraint or check-before-insert db.Exec("INSERT IGNORE INTO processed_items ...") return nil } ``` ### 6. Handle Timezones Explicitly Always specify timezone for business-hour jobs: ```go Timezone: "America/New_York" // Good // Timezone: "" // Bad (defaults to UTC) ``` ### 7. Test Your Cron Expressions Use a cron expression calculator before deploying: - [crontab.guru](https://crontab.guru/) - Write unit tests with the parser ## Testing Jobs Test job handlers independently of the scheduler: ```go func TestCleanupJob(t *testing.T) { ctx := context.Background() err := cleanupHandler(ctx) if err != nil { t.Fatalf("cleanup failed: %v", err) } // Verify cleanup occurred } ``` Test schedule parsing: ```go func TestScheduleParsing(t *testing.T) { job := &scheduler.Job{ Name: "test", Schedule: "0 2 * * *", Handler: func(ctx context.Context) error { return nil }, } if err := job.Validate(); err != nil { t.Fatalf("invalid job: %v", err) } } ``` ## Comparison to Other Solutions | Feature | scheduler | robfig/cron | github.com/go-co-op/gocron | |---------|-----------|-------------|----------------------------| | Standard cron syntax | ✅ | ✅ | ✅ | | Seconds support | ✅ | ✅ | ✅ | | Timezone support | ✅ | ✅ | ✅ | | Middleware pattern | ✅ | ⚠️ (basic) | ❌ | | Graceful shutdown | ✅ | ⚠️ (basic) | ✅ | | Zero dependencies | ✅ | ❌ | ❌ | | Job metadata | ✅ | ❌ | ⚠️ (limited) | ## API Reference See [example_test.go](./example_test.go) for comprehensive examples. ### Core Types - `Scheduler` - Manages scheduled jobs - `Job` - Job definition with schedule and handler - `Middleware` - Function that wraps job handlers ### Functions - `New(opts ...Option) *Scheduler` - Create new scheduler - `WithTimezone(tz string) Option` - Set default timezone - `WithMiddleware(mw ...Middleware) Option` - Add middleware ### Methods - `Register(job *Job) error` - Add job to scheduler - `Start() error` - Begin executing jobs - `Stop(ctx context.Context) error` - Graceful shutdown ## License This package is part of the Memos project and shares its license. ================================================ FILE: plugin/scheduler/doc.go ================================================ // Package scheduler provides a GitHub Actions-inspired cron job scheduler. // // Features: // - Standard cron expression syntax (5-field and 6-field formats) // - Timezone-aware scheduling // - Middleware pattern for cross-cutting concerns (logging, metrics, recovery) // - Graceful shutdown with context cancellation // - Zero external dependencies // // Basic usage: // // s := scheduler.New() // // s.Register(&scheduler.Job{ // Name: "daily-cleanup", // Schedule: "0 2 * * *", // 2 AM daily // Handler: func(ctx context.Context) error { // // Your cleanup logic here // return nil // }, // }) // // s.Start() // defer s.Stop(context.Background()) // // With middleware: // // s := scheduler.New( // scheduler.WithTimezone("America/New_York"), // scheduler.WithMiddleware( // scheduler.Recovery(), // scheduler.Logging(), // ), // ) package scheduler ================================================ FILE: plugin/scheduler/example_test.go ================================================ package scheduler_test import ( "context" "fmt" "log/slog" "os" "time" "github.com/usememos/memos/plugin/scheduler" ) // Example demonstrates basic scheduler usage. func Example_basic() { s := scheduler.New() s.Register(&scheduler.Job{ Name: "hello", Schedule: "*/5 * * * *", // Every 5 minutes Description: "Say hello", Handler: func(_ context.Context) error { fmt.Println("Hello from scheduler!") return nil }, }) s.Start() defer s.Stop(context.Background()) // Scheduler runs in background time.Sleep(100 * time.Millisecond) } // Example demonstrates timezone-aware scheduling. func Example_timezone() { s := scheduler.New( scheduler.WithTimezone("America/New_York"), ) s.Register(&scheduler.Job{ Name: "daily-report", Schedule: "0 9 * * *", // 9 AM in New York Handler: func(_ context.Context) error { fmt.Println("Generating daily report...") return nil }, }) s.Start() defer s.Stop(context.Background()) } // Example demonstrates middleware usage. func Example_middleware() { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) s := scheduler.New( scheduler.WithMiddleware( scheduler.Recovery(func(jobName string, r interface{}) { logger.Error("Job panicked", "job", jobName, "panic", r) }), scheduler.Logging(&slogAdapter{logger}), scheduler.Timeout(5*time.Minute), ), ) s.Register(&scheduler.Job{ Name: "data-sync", Schedule: "0 */2 * * *", // Every 2 hours Handler: func(_ context.Context) error { // Your sync logic here return nil }, }) s.Start() defer s.Stop(context.Background()) } // slogAdapter adapts slog.Logger to scheduler.Logger interface. type slogAdapter struct { logger *slog.Logger } func (a *slogAdapter) Info(msg string, args ...interface{}) { a.logger.Info(msg, args...) } func (a *slogAdapter) Error(msg string, args ...interface{}) { a.logger.Error(msg, args...) } // Example demonstrates multiple jobs with different schedules. func Example_multipleJobs() { s := scheduler.New() // Cleanup old data every night at 2 AM s.Register(&scheduler.Job{ Name: "cleanup", Schedule: "0 2 * * *", Tags: []string{"maintenance"}, Handler: func(_ context.Context) error { fmt.Println("Cleaning up old data...") return nil }, }) // Health check every 5 minutes s.Register(&scheduler.Job{ Name: "health-check", Schedule: "*/5 * * * *", Tags: []string{"monitoring"}, Handler: func(_ context.Context) error { fmt.Println("Running health check...") return nil }, }) // Weekly backup on Sundays at 1 AM s.Register(&scheduler.Job{ Name: "weekly-backup", Schedule: "0 1 * * 0", Tags: []string{"backup"}, Handler: func(_ context.Context) error { fmt.Println("Creating weekly backup...") return nil }, }) s.Start() defer s.Stop(context.Background()) } // Example demonstrates graceful shutdown with timeout. func Example_gracefulShutdown() { s := scheduler.New() s.Register(&scheduler.Job{ Name: "long-running", Schedule: "* * * * *", Handler: func(ctx context.Context) error { select { case <-time.After(30 * time.Second): fmt.Println("Job completed") case <-ctx.Done(): fmt.Println("Job canceled, cleaning up...") return ctx.Err() } return nil }, }) s.Start() // Simulate shutdown signal time.Sleep(5 * time.Second) // Give jobs 10 seconds to finish shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := s.Stop(shutdownCtx); err != nil { fmt.Printf("Shutdown error: %v\n", err) } } ================================================ FILE: plugin/scheduler/integration_test.go ================================================ package scheduler_test import ( "context" "errors" "fmt" "strings" "sync" "sync/atomic" "testing" "time" "github.com/usememos/memos/plugin/scheduler" ) // TestRealWorldScenario tests a realistic multi-job scenario. func TestRealWorldScenario(t *testing.T) { var ( quickJobCount atomic.Int32 hourlyJobCount atomic.Int32 logEntries []string logMu sync.Mutex ) logger := &testLogger{ onInfo: func(msg string, _ ...interface{}) { logMu.Lock() logEntries = append(logEntries, fmt.Sprintf("INFO: %s", msg)) logMu.Unlock() }, onError: func(msg string, _ ...interface{}) { logMu.Lock() logEntries = append(logEntries, fmt.Sprintf("ERROR: %s", msg)) logMu.Unlock() }, } s := scheduler.New( scheduler.WithTimezone("UTC"), scheduler.WithMiddleware( scheduler.Recovery(func(jobName string, r interface{}) { t.Logf("Job %s panicked: %v", jobName, r) }), scheduler.Logging(logger), scheduler.Timeout(5*time.Second), ), ) // Quick job (every second) s.Register(&scheduler.Job{ Name: "quick-check", Schedule: "* * * * * *", Handler: func(_ context.Context) error { quickJobCount.Add(1) time.Sleep(100 * time.Millisecond) return nil }, }) // Slower job (every 2 seconds) s.Register(&scheduler.Job{ Name: "slow-process", Schedule: "*/2 * * * * *", Handler: func(_ context.Context) error { hourlyJobCount.Add(1) time.Sleep(500 * time.Millisecond) return nil }, }) // Start scheduler if err := s.Start(); err != nil { t.Fatalf("failed to start scheduler: %v", err) } // Let it run for 5 seconds time.Sleep(5 * time.Second) // Graceful shutdown ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := s.Stop(ctx); err != nil { t.Fatalf("failed to stop scheduler: %v", err) } // Verify execution counts quick := quickJobCount.Load() slow := hourlyJobCount.Load() t.Logf("Quick job ran %d times", quick) t.Logf("Slow job ran %d times", slow) if quick < 4 { t.Errorf("expected quick job to run at least 4 times, ran %d", quick) } if slow < 2 { t.Errorf("expected slow job to run at least 2 times, ran %d", slow) } // Verify logging logMu.Lock() defer logMu.Unlock() hasStartLog := false hasCompleteLog := false for _, entry := range logEntries { if contains(entry, "Job started") { hasStartLog = true } if contains(entry, "Job completed") { hasCompleteLog = true } } if !hasStartLog { t.Error("expected job start logs") } if !hasCompleteLog { t.Error("expected job completion logs") } } // TestCancellationDuringExecution verifies jobs can be canceled mid-execution. func TestCancellationDuringExecution(t *testing.T) { var canceled atomic.Bool var started atomic.Bool s := scheduler.New() s.Register(&scheduler.Job{ Name: "long-job", Schedule: "* * * * * *", Handler: func(ctx context.Context) error { started.Store(true) // Simulate long-running work for i := 0; i < 100; i++ { select { case <-ctx.Done(): canceled.Store(true) return ctx.Err() case <-time.After(100 * time.Millisecond): // Keep working } } return nil }, }) if err := s.Start(); err != nil { t.Fatalf("failed to start: %v", err) } // Wait until job starts for i := 0; i < 30; i++ { if started.Load() { break } time.Sleep(100 * time.Millisecond) } if !started.Load() { t.Fatal("job did not start within timeout") } // Stop with reasonable timeout ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Stop(ctx); err != nil { t.Logf("stop returned error (may be expected): %v", err) } if !canceled.Load() { t.Error("expected job to detect cancellation") } } // TestTimezoneHandling verifies timezone-aware scheduling. func TestTimezoneHandling(t *testing.T) { // Parse a schedule in a specific timezone schedule, err := scheduler.ParseCronExpression("0 9 * * *") // 9 AM if err != nil { t.Fatalf("failed to parse schedule: %v", err) } // Test in New York timezone nyc, err := time.LoadLocation("America/New_York") if err != nil { t.Fatalf("failed to load timezone: %v", err) } // Current time: 8:30 AM in New York now := time.Date(2025, 1, 15, 8, 30, 0, 0, nyc) // Next run should be 9:00 AM same day next := schedule.Next(now) expected := time.Date(2025, 1, 15, 9, 0, 0, 0, nyc) if !next.Equal(expected) { t.Errorf("next = %v, expected %v", next, expected) } // If it's already past 9 AM now = time.Date(2025, 1, 15, 9, 30, 0, 0, nyc) next = schedule.Next(now) expected = time.Date(2025, 1, 16, 9, 0, 0, 0, nyc) if !next.Equal(expected) { t.Errorf("next = %v, expected %v", next, expected) } } // TestErrorPropagation verifies error handling. func TestErrorPropagation(t *testing.T) { var errorLogged atomic.Bool logger := &testLogger{ onError: func(msg string, _ ...interface{}) { if msg == "Job failed" { errorLogged.Store(true) } }, } s := scheduler.New( scheduler.WithMiddleware( scheduler.Logging(logger), ), ) s.Register(&scheduler.Job{ Name: "failing-job", Schedule: "* * * * * *", Handler: func(_ context.Context) error { return errors.New("intentional error") }, }) if err := s.Start(); err != nil { t.Fatalf("failed to start: %v", err) } // Let it run once time.Sleep(1500 * time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Stop(ctx); err != nil { t.Fatalf("failed to stop: %v", err) } if !errorLogged.Load() { t.Error("expected error to be logged") } } // TestPanicRecovery verifies panic recovery middleware. func TestPanicRecovery(t *testing.T) { var panicRecovered atomic.Bool s := scheduler.New( scheduler.WithMiddleware( scheduler.Recovery(func(jobName string, r interface{}) { panicRecovered.Store(true) t.Logf("Recovered from panic in job %s: %v", jobName, r) }), ), ) s.Register(&scheduler.Job{ Name: "panicking-job", Schedule: "* * * * * *", Handler: func(_ context.Context) error { panic("intentional panic for testing") }, }) if err := s.Start(); err != nil { t.Fatalf("failed to start: %v", err) } // Let it run once time.Sleep(1500 * time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Stop(ctx); err != nil { t.Fatalf("failed to stop: %v", err) } if !panicRecovered.Load() { t.Error("expected panic to be recovered") } } // TestMultipleJobsWithDifferentSchedules verifies concurrent job execution. func TestMultipleJobsWithDifferentSchedules(t *testing.T) { var ( job1Count atomic.Int32 job2Count atomic.Int32 job3Count atomic.Int32 ) s := scheduler.New() // Job 1: Every second s.Register(&scheduler.Job{ Name: "job-1sec", Schedule: "* * * * * *", Handler: func(_ context.Context) error { job1Count.Add(1) return nil }, }) // Job 2: Every 2 seconds s.Register(&scheduler.Job{ Name: "job-2sec", Schedule: "*/2 * * * * *", Handler: func(_ context.Context) error { job2Count.Add(1) return nil }, }) // Job 3: Every 3 seconds s.Register(&scheduler.Job{ Name: "job-3sec", Schedule: "*/3 * * * * *", Handler: func(_ context.Context) error { job3Count.Add(1) return nil }, }) if err := s.Start(); err != nil { t.Fatalf("failed to start: %v", err) } // Let them run for 6 seconds time.Sleep(6 * time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Stop(ctx); err != nil { t.Fatalf("failed to stop: %v", err) } // Verify counts (allowing for timing variance) c1 := job1Count.Load() c2 := job2Count.Load() c3 := job3Count.Load() t.Logf("Job 1 ran %d times, Job 2 ran %d times, Job 3 ran %d times", c1, c2, c3) if c1 < 5 { t.Errorf("expected job1 to run at least 5 times, ran %d", c1) } if c2 < 2 { t.Errorf("expected job2 to run at least 2 times, ran %d", c2) } if c3 < 1 { t.Errorf("expected job3 to run at least 1 time, ran %d", c3) } } // Helpers type testLogger struct { onInfo func(msg string, args ...interface{}) onError func(msg string, args ...interface{}) } func (l *testLogger) Info(msg string, args ...interface{}) { if l.onInfo != nil { l.onInfo(msg, args...) } } func (l *testLogger) Error(msg string, args ...interface{}) { if l.onError != nil { l.onError(msg, args...) } } func contains(s, substr string) bool { return strings.Contains(s, substr) } ================================================ FILE: plugin/scheduler/job.go ================================================ package scheduler import ( "context" "github.com/pkg/errors" ) // JobHandler is the function signature for scheduled job handlers. // The context passed to the handler will be canceled if the scheduler is shutting down. type JobHandler func(ctx context.Context) error // Job represents a scheduled task. type Job struct { // Name is a unique identifier for this job (required). // Used for logging and metrics. Name string // Schedule is a cron expression defining when this job runs (required). // Supports standard 5-field format: "minute hour day month weekday" // Examples: "0 * * * *" (hourly), "0 0 * * *" (daily at midnight) Schedule string // Timezone for schedule evaluation (optional, defaults to UTC). // Use IANA timezone names: "America/New_York", "Europe/London", etc. Timezone string // Handler is the function to execute when the job triggers (required). Handler JobHandler // Description provides human-readable context about what this job does (optional). Description string // Tags allow categorizing jobs for filtering/monitoring (optional). Tags []string } // Validate checks if the job definition is valid. func (j *Job) Validate() error { if j.Name == "" { return errors.New("job name is required") } if j.Schedule == "" { return errors.New("job schedule is required") } // Validate cron expression using parser if _, err := ParseCronExpression(j.Schedule); err != nil { return errors.Wrap(err, "invalid cron expression") } if j.Handler == nil { return errors.New("job handler is required") } return nil } ================================================ FILE: plugin/scheduler/job_test.go ================================================ package scheduler import ( "context" "testing" ) func TestJobDefinition(t *testing.T) { callCount := 0 job := &Job{ Name: "test-job", Handler: func(_ context.Context) error { callCount++ return nil }, } if job.Name != "test-job" { t.Errorf("expected name 'test-job', got %s", job.Name) } // Test handler execution if err := job.Handler(context.Background()); err != nil { t.Fatalf("handler failed: %v", err) } if callCount != 1 { t.Errorf("expected handler to be called once, called %d times", callCount) } } func TestJobValidation(t *testing.T) { tests := []struct { name string job *Job wantErr bool }{ { name: "valid job", job: &Job{ Name: "valid", Schedule: "0 * * * *", Handler: func(_ context.Context) error { return nil }, }, wantErr: false, }, { name: "missing name", job: &Job{ Schedule: "0 * * * *", Handler: func(_ context.Context) error { return nil }, }, wantErr: true, }, { name: "missing schedule", job: &Job{ Name: "test", Handler: func(_ context.Context) error { return nil }, }, wantErr: true, }, { name: "invalid cron expression", job: &Job{ Name: "test", Schedule: "invalid cron", Handler: func(_ context.Context) error { return nil }, }, wantErr: true, }, { name: "missing handler", job: &Job{ Name: "test", Schedule: "0 * * * *", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.job.Validate() if (err != nil) != tt.wantErr { t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) } }) } } ================================================ FILE: plugin/scheduler/middleware.go ================================================ package scheduler import ( "context" "time" "github.com/pkg/errors" ) // Middleware wraps a JobHandler to add cross-cutting behavior. type Middleware func(JobHandler) JobHandler // Chain combines multiple middleware into a single middleware. // Middleware are applied in the order they're provided (left to right). func Chain(middlewares ...Middleware) Middleware { return func(handler JobHandler) JobHandler { // Apply middleware in reverse order so first middleware wraps outermost for i := len(middlewares) - 1; i >= 0; i-- { handler = middlewares[i](handler) } return handler } } // Recovery recovers from panics in job handlers and converts them to errors. func Recovery(onPanic func(jobName string, recovered interface{})) Middleware { return func(next JobHandler) JobHandler { return func(ctx context.Context) (err error) { defer func() { if r := recover(); r != nil { jobName := getJobName(ctx) if onPanic != nil { onPanic(jobName, r) } err = errors.Errorf("job %q panicked: %v", jobName, r) } }() return next(ctx) } } } // Logger is a minimal logging interface. type Logger interface { Info(msg string, args ...interface{}) Error(msg string, args ...interface{}) } // Logging adds execution logging to jobs. func Logging(logger Logger) Middleware { return func(next JobHandler) JobHandler { return func(ctx context.Context) error { jobName := getJobName(ctx) start := time.Now() logger.Info("Job started", "job", jobName) err := next(ctx) duration := time.Since(start) if err != nil { logger.Error("Job failed", "job", jobName, "duration", duration, "error", err) } else { logger.Info("Job completed", "job", jobName, "duration", duration) } return err } } } // Timeout wraps a job handler with a timeout. func Timeout(duration time.Duration) Middleware { return func(next JobHandler) JobHandler { return func(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, duration) defer cancel() done := make(chan error, 1) go func() { done <- next(ctx) }() select { case err := <-done: return err case <-ctx.Done(): return errors.Errorf("job %q timed out after %v", getJobName(ctx), duration) } } } } // Context keys for job metadata. type contextKey int const ( jobNameKey contextKey = iota ) // withJobName adds the job name to the context. func withJobName(ctx context.Context, name string) context.Context { return context.WithValue(ctx, jobNameKey, name) } // getJobName retrieves the job name from the context. func getJobName(ctx context.Context) string { if name, ok := ctx.Value(jobNameKey).(string); ok { return name } return "unknown" } // GetJobName retrieves the job name from the context (public API). // Returns empty string if not found. // //nolint:revive // GetJobName is the public API, getJobName is internal func GetJobName(ctx context.Context) string { return getJobName(ctx) } ================================================ FILE: plugin/scheduler/middleware_test.go ================================================ package scheduler import ( "context" "errors" "sync/atomic" "testing" ) func TestMiddlewareChaining(t *testing.T) { var order []string mw1 := func(next JobHandler) JobHandler { return func(ctx context.Context) error { order = append(order, "before-1") err := next(ctx) order = append(order, "after-1") return err } } mw2 := func(next JobHandler) JobHandler { return func(ctx context.Context) error { order = append(order, "before-2") err := next(ctx) order = append(order, "after-2") return err } } handler := func(_ context.Context) error { order = append(order, "handler") return nil } chain := Chain(mw1, mw2) wrapped := chain(handler) if err := wrapped(context.Background()); err != nil { t.Fatalf("wrapped handler failed: %v", err) } expected := []string{"before-1", "before-2", "handler", "after-2", "after-1"} if len(order) != len(expected) { t.Fatalf("expected %d calls, got %d", len(expected), len(order)) } for i, want := range expected { if order[i] != want { t.Errorf("order[%d] = %q, want %q", i, order[i], want) } } } func TestRecoveryMiddleware(t *testing.T) { var panicRecovered atomic.Bool onPanic := func(_ string, _ interface{}) { panicRecovered.Store(true) } handler := func(_ context.Context) error { panic("simulated panic") } recovery := Recovery(onPanic) wrapped := recovery(handler) // Should not panic, error should be returned err := wrapped(withJobName(context.Background(), "test-job")) if err == nil { t.Error("expected error from recovered panic") } if !panicRecovered.Load() { t.Error("panic handler was not called") } } func TestLoggingMiddleware(t *testing.T) { var loggedStart, loggedEnd atomic.Bool var loggedError atomic.Bool logger := &testLogger{ onInfo: func(msg string, _ ...interface{}) { if msg == "Job started" { loggedStart.Store(true) } else if msg == "Job completed" { loggedEnd.Store(true) } }, onError: func(msg string, _ ...interface{}) { if msg == "Job failed" { loggedError.Store(true) } }, } // Test successful execution handler := func(_ context.Context) error { return nil } logging := Logging(logger) wrapped := logging(handler) if err := wrapped(withJobName(context.Background(), "test-job")); err != nil { t.Fatalf("handler failed: %v", err) } if !loggedStart.Load() { t.Error("start was not logged") } if !loggedEnd.Load() { t.Error("end was not logged") } // Test error handling handlerErr := func(_ context.Context) error { return errors.New("job error") } wrappedErr := logging(handlerErr) _ = wrappedErr(withJobName(context.Background(), "test-job-error")) if !loggedError.Load() { t.Error("error was not logged") } } type testLogger struct { onInfo func(msg string, args ...interface{}) onError func(msg string, args ...interface{}) } func (l *testLogger) Info(msg string, args ...interface{}) { if l.onInfo != nil { l.onInfo(msg, args...) } } func (l *testLogger) Error(msg string, args ...interface{}) { if l.onError != nil { l.onError(msg, args...) } } ================================================ FILE: plugin/scheduler/parser.go ================================================ package scheduler import ( "strconv" "strings" "time" "github.com/pkg/errors" ) // Schedule represents a parsed cron expression. type Schedule struct { seconds fieldMatcher // 0-59 (optional, for 6-field format) minutes fieldMatcher // 0-59 hours fieldMatcher // 0-23 days fieldMatcher // 1-31 months fieldMatcher // 1-12 weekdays fieldMatcher // 0-7 (0 and 7 are Sunday) hasSecs bool } // fieldMatcher determines if a field value matches. type fieldMatcher interface { matches(value int) bool } // ParseCronExpression parses a cron expression and returns a Schedule. // Supports both 5-field (minute hour day month weekday) and 6-field (second minute hour day month weekday) formats. func ParseCronExpression(expr string) (*Schedule, error) { if expr == "" { return nil, errors.New("empty cron expression") } fields := strings.Fields(expr) if len(fields) != 5 && len(fields) != 6 { return nil, errors.Errorf("invalid cron expression: expected 5 or 6 fields, got %d", len(fields)) } s := &Schedule{ hasSecs: len(fields) == 6, } var err error offset := 0 // Parse seconds (if 6-field format) if s.hasSecs { s.seconds, err = parseField(fields[0], 0, 59) if err != nil { return nil, errors.Wrap(err, "invalid seconds field") } offset = 1 } else { s.seconds = &exactMatcher{value: 0} // Default to 0 seconds } // Parse minutes s.minutes, err = parseField(fields[offset], 0, 59) if err != nil { return nil, errors.Wrap(err, "invalid minutes field") } // Parse hours s.hours, err = parseField(fields[offset+1], 0, 23) if err != nil { return nil, errors.Wrap(err, "invalid hours field") } // Parse days s.days, err = parseField(fields[offset+2], 1, 31) if err != nil { return nil, errors.Wrap(err, "invalid days field") } // Parse months s.months, err = parseField(fields[offset+3], 1, 12) if err != nil { return nil, errors.Wrap(err, "invalid months field") } // Parse weekdays (0-7, where both 0 and 7 represent Sunday) s.weekdays, err = parseField(fields[offset+4], 0, 7) if err != nil { return nil, errors.Wrap(err, "invalid weekdays field") } return s, nil } // Next returns the next time the schedule should run after the given time. func (s *Schedule) Next(from time.Time) time.Time { // Start from the next second/minute if s.hasSecs { from = from.Add(1 * time.Second).Truncate(time.Second) } else { from = from.Add(1 * time.Minute).Truncate(time.Minute) } // Cap search at 4 years to prevent infinite loops maxTime := from.AddDate(4, 0, 0) for from.Before(maxTime) { if s.matches(from) { return from } // Advance to next potential match if s.hasSecs { from = from.Add(1 * time.Second) } else { from = from.Add(1 * time.Minute) } } // Should never reach here with valid cron expressions return time.Time{} } // matches checks if the given time matches the schedule. func (s *Schedule) matches(t time.Time) bool { return s.seconds.matches(t.Second()) && s.minutes.matches(t.Minute()) && s.hours.matches(t.Hour()) && s.months.matches(int(t.Month())) && (s.days.matches(t.Day()) || s.weekdays.matches(int(t.Weekday()))) } // parseField parses a single cron field (supports *, ranges, lists, steps). func parseField(field string, min, max int) (fieldMatcher, error) { // Wildcard if field == "*" { return &wildcardMatcher{}, nil } // Step values (*/N) if strings.HasPrefix(field, "*/") { step, err := strconv.Atoi(field[2:]) if err != nil || step < 1 || step > max { return nil, errors.Errorf("invalid step value: %s", field) } return &stepMatcher{step: step, min: min, max: max}, nil } // List (1,2,3) if strings.Contains(field, ",") { parts := strings.Split(field, ",") values := make([]int, 0, len(parts)) for _, p := range parts { val, err := strconv.Atoi(strings.TrimSpace(p)) if err != nil || val < min || val > max { return nil, errors.Errorf("invalid list value: %s", p) } values = append(values, val) } return &listMatcher{values: values}, nil } // Range (1-5) if strings.Contains(field, "-") { parts := strings.Split(field, "-") if len(parts) != 2 { return nil, errors.Errorf("invalid range: %s", field) } start, err1 := strconv.Atoi(strings.TrimSpace(parts[0])) end, err2 := strconv.Atoi(strings.TrimSpace(parts[1])) if err1 != nil || err2 != nil || start < min || end > max || start > end { return nil, errors.Errorf("invalid range: %s", field) } return &rangeMatcher{start: start, end: end}, nil } // Exact value val, err := strconv.Atoi(field) if err != nil || val < min || val > max { return nil, errors.Errorf("invalid value: %s (must be between %d and %d)", field, min, max) } return &exactMatcher{value: val}, nil } // wildcardMatcher matches any value. type wildcardMatcher struct{} func (*wildcardMatcher) matches(_ int) bool { return true } // exactMatcher matches a specific value. type exactMatcher struct { value int } func (m *exactMatcher) matches(value int) bool { return value == m.value } // rangeMatcher matches values in a range. type rangeMatcher struct { start, end int } func (m *rangeMatcher) matches(value int) bool { return value >= m.start && value <= m.end } // listMatcher matches any value in a list. type listMatcher struct { values []int } func (m *listMatcher) matches(value int) bool { for _, v := range m.values { if v == value { return true } } return false } // stepMatcher matches values at regular intervals. type stepMatcher struct { step, min, max int } func (m *stepMatcher) matches(value int) bool { if value < m.min || value > m.max { return false } return (value-m.min)%m.step == 0 } ================================================ FILE: plugin/scheduler/parser_test.go ================================================ package scheduler import ( "testing" "time" ) func TestParseCronExpression(t *testing.T) { tests := []struct { name string expr string wantErr bool }{ // Standard 5-field format {"every minute", "* * * * *", false}, {"hourly", "0 * * * *", false}, {"daily midnight", "0 0 * * *", false}, {"weekly sunday", "0 0 * * 0", false}, {"monthly", "0 0 1 * *", false}, {"specific time", "30 14 * * *", false}, // 2:30 PM daily {"range", "0 9-17 * * *", false}, // Every hour 9 AM - 5 PM {"step", "*/15 * * * *", false}, // Every 15 minutes {"list", "0 8,12,18 * * *", false}, // 8 AM, 12 PM, 6 PM // 6-field format with seconds {"with seconds", "0 * * * * *", false}, {"every 30 seconds", "*/30 * * * * *", false}, // Invalid expressions {"empty", "", true}, {"too few fields", "* * *", true}, {"too many fields", "* * * * * * *", true}, {"invalid minute", "60 * * * *", true}, {"invalid hour", "0 24 * * *", true}, {"invalid day", "0 0 32 * *", true}, {"invalid month", "0 0 1 13 *", true}, {"invalid weekday", "0 0 * * 8", true}, {"garbage", "not a cron expression", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { schedule, err := ParseCronExpression(tt.expr) if (err != nil) != tt.wantErr { t.Errorf("ParseCronExpression(%q) error = %v, wantErr %v", tt.expr, err, tt.wantErr) return } if !tt.wantErr && schedule == nil { t.Errorf("ParseCronExpression(%q) returned nil schedule without error", tt.expr) } }) } } func TestScheduleNext(t *testing.T) { tests := []struct { name string expr string from time.Time expected time.Time }{ { name: "every minute from start of hour", expr: "* * * * *", from: time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC), expected: time.Date(2025, 1, 1, 10, 1, 0, 0, time.UTC), }, { name: "hourly at minute 30", expr: "30 * * * *", from: time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC), expected: time.Date(2025, 1, 1, 10, 30, 0, 0, time.UTC), }, { name: "hourly at minute 30 (already past)", expr: "30 * * * *", from: time.Date(2025, 1, 1, 10, 45, 0, 0, time.UTC), expected: time.Date(2025, 1, 1, 11, 30, 0, 0, time.UTC), }, { name: "daily at 2 AM", expr: "0 2 * * *", from: time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC), expected: time.Date(2025, 1, 2, 2, 0, 0, 0, time.UTC), }, { name: "every 15 minutes", expr: "*/15 * * * *", from: time.Date(2025, 1, 1, 10, 7, 0, 0, time.UTC), expected: time.Date(2025, 1, 1, 10, 15, 0, 0, time.UTC), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { schedule, err := ParseCronExpression(tt.expr) if err != nil { t.Fatalf("failed to parse expression: %v", err) } next := schedule.Next(tt.from) if !next.Equal(tt.expected) { t.Errorf("Next(%v) = %v, expected %v", tt.from, next, tt.expected) } }) } } func TestScheduleNextWithTimezone(t *testing.T) { nyc, _ := time.LoadLocation("America/New_York") // Schedule for 9 AM in New York schedule, err := ParseCronExpression("0 9 * * *") if err != nil { t.Fatalf("failed to parse expression: %v", err) } // Current time: 8 AM in New York from := time.Date(2025, 1, 1, 8, 0, 0, 0, nyc) next := schedule.Next(from) // Should be 9 AM same day in New York expected := time.Date(2025, 1, 1, 9, 0, 0, 0, nyc) if !next.Equal(expected) { t.Errorf("Next(%v) = %v, expected %v", from, next, expected) } } ================================================ FILE: plugin/scheduler/scheduler.go ================================================ package scheduler import ( "context" "sync" "time" "github.com/pkg/errors" ) // Scheduler manages scheduled jobs. type Scheduler struct { jobs map[string]*registeredJob jobsMu sync.RWMutex timezone *time.Location middleware Middleware running bool runningMu sync.RWMutex stopCh chan struct{} wg sync.WaitGroup } // registeredJob wraps a Job with runtime state. type registeredJob struct { job *Job cancelFn context.CancelFunc } // Option configures a Scheduler. type Option func(*Scheduler) // WithTimezone sets the default timezone for all jobs. func WithTimezone(tz string) Option { return func(s *Scheduler) { loc, err := time.LoadLocation(tz) if err != nil { // Default to UTC on invalid timezone loc = time.UTC } s.timezone = loc } } // WithMiddleware sets middleware to wrap all job handlers. func WithMiddleware(mw ...Middleware) Option { return func(s *Scheduler) { if len(mw) > 0 { s.middleware = Chain(mw...) } } } // New creates a new Scheduler with optional configuration. func New(opts ...Option) *Scheduler { s := &Scheduler{ jobs: make(map[string]*registeredJob), timezone: time.UTC, stopCh: make(chan struct{}), } for _, opt := range opts { opt(s) } return s } // Register adds a job to the scheduler. // Jobs must be registered before calling Start(). func (s *Scheduler) Register(job *Job) error { if job == nil { return errors.New("job cannot be nil") } if err := job.Validate(); err != nil { return errors.Wrap(err, "invalid job") } s.jobsMu.Lock() defer s.jobsMu.Unlock() if _, exists := s.jobs[job.Name]; exists { return errors.Errorf("job with name %q already registered", job.Name) } s.jobs[job.Name] = ®isteredJob{job: job} return nil } // Start begins executing scheduled jobs. func (s *Scheduler) Start() error { s.runningMu.Lock() defer s.runningMu.Unlock() if s.running { return errors.New("scheduler already running") } s.jobsMu.RLock() defer s.jobsMu.RUnlock() // Parse and schedule all jobs for _, rj := range s.jobs { schedule, err := ParseCronExpression(rj.job.Schedule) if err != nil { return errors.Wrapf(err, "failed to parse schedule for job %q", rj.job.Name) } ctx, cancel := context.WithCancel(context.Background()) rj.cancelFn = cancel s.wg.Add(1) go s.runJobWithSchedule(ctx, rj, schedule) } s.running = true return nil } // runJobWithSchedule executes a job according to its cron schedule. func (s *Scheduler) runJobWithSchedule(ctx context.Context, rj *registeredJob, schedule *Schedule) { defer s.wg.Done() // Apply middleware to handler handler := rj.job.Handler if s.middleware != nil { handler = s.middleware(handler) } for { // Calculate next run time now := time.Now() if rj.job.Timezone != "" { loc, err := time.LoadLocation(rj.job.Timezone) if err == nil { now = now.In(loc) } } else if s.timezone != nil { now = now.In(s.timezone) } next := schedule.Next(now) duration := time.Until(next) timer := time.NewTimer(duration) select { case <-timer.C: // Add job name to context and execute jobCtx := withJobName(ctx, rj.job.Name) if err := handler(jobCtx); err != nil { // Error already handled by middleware (if any) _ = err } case <-ctx.Done(): // Stop the timer to prevent it from firing. The timer will be garbage collected. timer.Stop() return case <-s.stopCh: // Stop the timer to prevent it from firing. The timer will be garbage collected. timer.Stop() return } } } // Stop gracefully shuts down the scheduler. // It waits for all running jobs to complete or until the context is canceled. func (s *Scheduler) Stop(ctx context.Context) error { s.runningMu.Lock() if !s.running { s.runningMu.Unlock() return errors.New("scheduler not running") } s.running = false s.runningMu.Unlock() // Cancel all job contexts s.jobsMu.RLock() for _, rj := range s.jobs { if rj.cancelFn != nil { rj.cancelFn() } } s.jobsMu.RUnlock() // Signal stop and wait for jobs to finish close(s.stopCh) done := make(chan struct{}) go func() { s.wg.Wait() close(done) }() select { case <-done: return nil case <-ctx.Done(): return ctx.Err() } } ================================================ FILE: plugin/scheduler/scheduler_test.go ================================================ package scheduler import ( "context" "fmt" "strings" "sync" "sync/atomic" "testing" "time" ) func TestSchedulerCreation(t *testing.T) { s := New() if s == nil { t.Fatal("New() returned nil") } } func TestSchedulerWithTimezone(t *testing.T) { s := New(WithTimezone("America/New_York")) if s == nil { t.Fatal("New() with timezone returned nil") } } func TestJobRegistration(t *testing.T) { s := New() job := &Job{ Name: "test-registration", Schedule: "0 * * * *", Handler: func(_ context.Context) error { return nil }, } if err := s.Register(job); err != nil { t.Fatalf("failed to register valid job: %v", err) } // Registering duplicate name should fail if err := s.Register(job); err == nil { t.Error("expected error when registering duplicate job name") } } func TestSchedulerStartStop(t *testing.T) { s := New() var runCount atomic.Int32 job := &Job{ Name: "test-start-stop", Schedule: "* * * * * *", // Every second (6-field format) Handler: func(_ context.Context) error { runCount.Add(1) return nil }, } if err := s.Register(job); err != nil { t.Fatalf("failed to register job: %v", err) } // Start scheduler if err := s.Start(); err != nil { t.Fatalf("failed to start scheduler: %v", err) } // Let it run for 2.5 seconds time.Sleep(2500 * time.Millisecond) // Stop scheduler ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Stop(ctx); err != nil { t.Fatalf("failed to stop scheduler: %v", err) } count := runCount.Load() // Should have run at least twice (at 0s and 1s, maybe 2s) if count < 2 { t.Errorf("expected job to run at least 2 times, ran %d times", count) } // Verify it stopped (count shouldn't increase) finalCount := count time.Sleep(1500 * time.Millisecond) if runCount.Load() != finalCount { t.Error("scheduler did not stop - job continued running") } } func TestSchedulerWithMiddleware(t *testing.T) { var executionLog []string var logMu sync.Mutex logger := &testLogger{ onInfo: func(msg string, _ ...interface{}) { logMu.Lock() executionLog = append(executionLog, fmt.Sprintf("INFO: %s", msg)) logMu.Unlock() }, onError: func(msg string, _ ...interface{}) { logMu.Lock() executionLog = append(executionLog, fmt.Sprintf("ERROR: %s", msg)) logMu.Unlock() }, } s := New(WithMiddleware( Recovery(func(jobName string, r interface{}) { logMu.Lock() executionLog = append(executionLog, fmt.Sprintf("PANIC: %s - %v", jobName, r)) logMu.Unlock() }), Logging(logger), )) job := &Job{ Name: "test-middleware", Schedule: "* * * * * *", // Every second Handler: func(_ context.Context) error { return nil }, } if err := s.Register(job); err != nil { t.Fatalf("failed to register job: %v", err) } if err := s.Start(); err != nil { t.Fatalf("failed to start: %v", err) } time.Sleep(1500 * time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Stop(ctx); err != nil { t.Fatalf("failed to stop: %v", err) } logMu.Lock() defer logMu.Unlock() // Should have at least one start and one completion log hasStart := false hasCompletion := false for _, log := range executionLog { if strings.Contains(log, "Job started") { hasStart = true } if strings.Contains(log, "Job completed") { hasCompletion = true } } if !hasStart { t.Error("expected job start log") } if !hasCompletion { t.Error("expected job completion log") } } ================================================ FILE: plugin/storage/s3/s3.go ================================================ 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/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) { putInput := s3.PutObjectInput{ Bucket: c.Bucket, Key: aws.String(key), ContentType: aws.String(fileType), Body: content, } if _, err := c.Client.PutObject(ctx, &putInput); err != nil { return "", err } return key, 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 get object") } return presignResult.URL, nil } // GetObject retrieves an object from S3. func (c *Client) GetObject(ctx context.Context, key string) ([]byte, error) { output, err := c.Client.GetObject(ctx, &s3.GetObjectInput{ Bucket: c.Bucket, Key: aws.String(key), }) if err != nil { return nil, errors.Wrap(err, "failed to download object") } defer output.Body.Close() data, err := io.ReadAll(output.Body) if err != nil { return nil, errors.Wrap(err, "failed to read object body") } return data, nil } // GetObjectStream retrieves an object from S3 as a stream. func (c *Client) GetObjectStream(ctx context.Context, key string) (io.ReadCloser, error) { output, err := c.Client.GetObject(ctx, &s3.GetObjectInput{ Bucket: c.Bucket, Key: aws.String(key), }) if err != nil { return nil, errors.Wrap(err, "failed to get object") } return output.Body, 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 } ================================================ FILE: plugin/webhook/validate.go ================================================ package webhook import ( "net" "net/url" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // reservedCIDRs lists IP ranges that must never be targeted by outbound webhook requests. // Covers loopback, RFC-1918 private, link-local (including cloud IMDS at 169.254.169.254), // and their IPv6 equivalents. var reservedCIDRs = []string{ "127.0.0.0/8", // IPv4 loopback "10.0.0.0/8", // RFC-1918 class A "172.16.0.0/12", // RFC-1918 class B "192.168.0.0/16", // RFC-1918 class C "169.254.0.0/16", // Link-local / cloud IMDS "::1/128", // IPv6 loopback "fc00::/7", // IPv6 unique local "fe80::/10", // IPv6 link-local } // reservedNetworks is the parsed form of reservedCIDRs, built once at startup. var reservedNetworks []*net.IPNet func init() { for _, cidr := range reservedCIDRs { _, network, err := net.ParseCIDR(cidr) if err != nil { panic("webhook: invalid reserved CIDR " + cidr + ": " + err.Error()) } reservedNetworks = append(reservedNetworks, network) } } // AllowPrivateIPs controls whether webhook URLs may resolve to reserved/private // IP addresses. When true, the SSRF protection is disabled. This is useful for // self-hosted deployments where webhooks target services on the local network. var AllowPrivateIPs bool // isReservedIP reports whether ip falls within any reserved/private range. func isReservedIP(ip net.IP) bool { if AllowPrivateIPs { return false } for _, network := range reservedNetworks { if network.Contains(ip) { return true } } return false } // ValidateURL checks that rawURL: // 1. Parses as a valid absolute URL. // 2. Uses the http or https scheme. // 3. Does not resolve to a reserved/private IP address. // // It returns a gRPC InvalidArgument status error so callers can return it directly. func ValidateURL(rawURL string) error { u, err := url.ParseRequestURI(rawURL) if err != nil { return status.Errorf(codes.InvalidArgument, "invalid webhook URL: %v", err) } if u.Scheme != "http" && u.Scheme != "https" { return status.Errorf(codes.InvalidArgument, "webhook URL must use http or https scheme, got %q", u.Scheme) } ips, err := net.LookupHost(u.Hostname()) if err != nil { return status.Errorf(codes.InvalidArgument, "webhook URL hostname could not be resolved: %v", err) } for _, ipStr := range ips { ip := net.ParseIP(ipStr) if ip != nil && isReservedIP(ip) { return status.Errorf(codes.InvalidArgument, "webhook URL must not resolve to a reserved or private IP address") } } return nil } ================================================ FILE: plugin/webhook/webhook.go ================================================ package webhook import ( "bytes" "context" "encoding/json" "io" "log/slog" "net" "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 // safeClient is the shared HTTP client used for all webhook dispatches. // Its Transport guards against SSRF by blocking connections to reserved/private // IP addresses at dial time, which also defeats DNS rebinding attacks. safeClient = &http.Client{ Timeout: timeout, Transport: &http.Transport{ DialContext: safeDialContext, }, } ) // safeDialContext is a net.Dialer.DialContext replacement that resolves the target // hostname and rejects any address that falls within a reserved/private IP range. func safeDialContext(ctx context.Context, network, addr string) (net.Conn, error) { host, port, err := net.SplitHostPort(addr) if err != nil { return nil, errors.Errorf("webhook: invalid address %q", addr) } ips, err := net.DefaultResolver.LookupHost(ctx, host) if err != nil { return nil, errors.Wrapf(err, "webhook: failed to resolve host %q", host) } for _, ipStr := range ips { if ip := net.ParseIP(ipStr); ip != nil && isReservedIP(ip) { return nil, errors.Errorf("webhook: connection to reserved/private IP address is not allowed") } } return (&net.Dialer{}).DialContext(ctx, network, net.JoinHostPort(host, port)) } 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") resp, err := safeClient.Do(req) if err != nil { return errors.Wrapf(err, "failed to post webhook to %s", requestPayload.URL) } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrapf(err, "failed to read webhook response from %s", requestPayload.URL) } if resp.StatusCode < 200 || resp.StatusCode > 299 { return errors.Errorf("failed to post webhook %s, status code: %d", requestPayload.URL, resp.StatusCode) } 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 { slog.Warn("Failed to dispatch webhook asynchronously", slog.String("url", requestPayload.URL), slog.String("activityType", requestPayload.ActivityType), slog.Any("err", err)) } }() } ================================================ FILE: plugin/webhook/webhook_test.go ================================================ package webhook ================================================ FILE: proto/README.md ================================================ # Guide ## Prerequisites - [buf](https://docs.buf.build/installation) ## Generate ```sh buf generate ``` ## Format ```sh buf format -w ``` ================================================ FILE: proto/api/v1/README.md ================================================ # 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/). ================================================ FILE: proto/api/v1/attachment_service.proto ================================================ 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 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 an attachment by name. rpc GetAttachment(GetAttachmentRequest) returns (Attachment) { option (google.api.http) = {get: "/api/v1/{name=attachments/*}"}; option (google.api.method_signature) = "name"; } // UpdateAttachment updates an 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 an 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: "mime_type==\"image/png\"" or "filename.contains(\"test\")" // Supported operators: =, !=, <, <=, >, >=, : (contains), in // Supported fields: filename, mime_type, 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 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"} ]; } ================================================ FILE: proto/api/v1/auth_service.proto ================================================ 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 { // GetCurrentUser returns the authenticated user's information. // Validates the access token and returns user details. // Similar to OIDC's /userinfo endpoint. rpc GetCurrentUser(GetCurrentUserRequest) returns (GetCurrentUserResponse) { option (google.api.http) = {get: "/api/v1/auth/me"}; } // SignIn authenticates a user with credentials and returns tokens. // On success, returns an access token and sets a refresh token cookie. // Supports password-based and SSO authentication methods. rpc SignIn(SignInRequest) returns (SignInResponse) { option (google.api.http) = { post: "/api/v1/auth/signin" body: "*" }; } // SignOut terminates the user's authentication. // Revokes the refresh token and clears the authentication cookie. rpc SignOut(SignOutRequest) returns (google.protobuf.Empty) { option (google.api.http) = {post: "/api/v1/auth/signout"}; } // RefreshToken exchanges a valid refresh token for a new access token. // The refresh token is read from the HttpOnly cookie. // Returns a new short-lived access token. rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse) { option (google.api.http) = { post: "/api/v1/auth/refresh" body: "*" }; } } message GetCurrentUserRequest {} message GetCurrentUserResponse { // The authenticated user's information. User user = 1; } message SignInRequest { // Nested message for password-based authentication credentials. message PasswordCredentials { // The username to sign in with. string username = 1 [(google.api.field_behavior) = REQUIRED]; // The password to sign in with. string password = 2 [(google.api.field_behavior) = REQUIRED]; } // Nested message for SSO authentication credentials. message SSOCredentials { // The resource name of the SSO provider. // Format: identity-providers/{uid} string idp_name = 1 [(google.api.field_behavior) = REQUIRED]; // The authorization code from the SSO provider. string code = 2 [(google.api.field_behavior) = REQUIRED]; // The redirect URI used in the SSO flow. string redirect_uri = 3 [(google.api.field_behavior) = REQUIRED]; // The PKCE code verifier for enhanced security (RFC 7636). // Optional - enables PKCE flow protection against authorization code interception. string code_verifier = 4 [(google.api.field_behavior) = OPTIONAL]; } // Authentication credentials. Provide one method. oneof credentials { // Username and password authentication. PasswordCredentials password_credentials = 1; // SSO provider authentication. SSOCredentials sso_credentials = 2; } } message SignInResponse { // The authenticated user's information. User user = 1; // The short-lived access token for API requests. // Store in memory only, not in localStorage. string access_token = 2; // When the access token expires. // Client should call RefreshToken before this time. google.protobuf.Timestamp access_token_expires_at = 3; } message SignOutRequest {} message RefreshTokenRequest {} message RefreshTokenResponse { // The new short-lived access token. string access_token = 1; // When the access token expires. google.protobuf.Timestamp expires_at = 2; } ================================================ FILE: proto/api/v1/common.proto ================================================ 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; } ================================================ FILE: proto/api/v1/idp_service.proto ================================================ 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/identity-providers"}; } // GetIdentityProvider gets an identity provider. rpc GetIdentityProvider(GetIdentityProviderRequest) returns (IdentityProvider) { option (google.api.http) = {get: "/api/v1/{name=identity-providers/*}"}; option (google.api.method_signature) = "name"; } // CreateIdentityProvider creates an identity provider. rpc CreateIdentityProvider(CreateIdentityProviderRequest) returns (IdentityProvider) { option (google.api.http) = { post: "/api/v1/identity-providers" 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=identity-providers/*}" 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=identity-providers/*}"}; option (google.api.method_signature) = "name"; } } message IdentityProvider { option (google.api.resource) = { type: "memos.api.v1/IdentityProvider" pattern: "identity-providers/{idp}" name_field: "name" singular: "identityProvider" plural: "identityProviders" }; // The resource name of the identity provider. // Format: identity-providers/{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: identity-providers/{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: identity-providers/{idp} string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/IdentityProvider"} ]; } ================================================ FILE: proto/api/v1/instance_service.proto ================================================ syntax = "proto3"; package memos.api.v1; import "api/v1/user_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/field_mask.proto"; import "google/type/color.proto"; option go_package = "gen/api/v1"; service InstanceService { // Gets the instance profile. rpc GetInstanceProfile(GetInstanceProfileRequest) returns (InstanceProfile) { option (google.api.http) = {get: "/api/v1/instance/profile"}; } // Gets an instance setting. rpc GetInstanceSetting(GetInstanceSettingRequest) returns (InstanceSetting) { option (google.api.http) = {get: "/api/v1/{name=instance/settings/*}"}; option (google.api.method_signature) = "name"; } // Updates an instance setting. rpc UpdateInstanceSetting(UpdateInstanceSettingRequest) returns (InstanceSetting) { option (google.api.http) = { patch: "/api/v1/{setting.name=instance/settings/*}" body: "setting" }; option (google.api.method_signature) = "setting,update_mask"; } } // Instance profile message containing basic instance information. message InstanceProfile { // Version is the current version of instance. string version = 2; // Demo indicates if the instance is in demo mode. bool demo = 3; // Instance URL is the URL of the instance. string instance_url = 6; // The first administrator who set up this instance. // When null, instance requires initial setup (creating the first admin account). User admin = 7; } // Request for instance profile. message GetInstanceProfileRequest {} // An instance setting resource. message InstanceSetting { option (google.api.resource) = { type: "memos.api.v1/InstanceSetting" pattern: "instance/settings/{setting}" singular: "instanceSetting" plural: "instanceSettings" }; // The name of the instance setting. // Format: instance/settings/{setting} string name = 1 [(google.api.field_behavior) = IDENTIFIER]; oneof value { GeneralSetting general_setting = 2; StorageSetting storage_setting = 3; MemoRelatedSetting memo_related_setting = 4; TagsSetting tags_setting = 5; NotificationSetting notification_setting = 6; } // Enumeration of instance setting keys. enum Key { KEY_UNSPECIFIED = 0; // GENERAL is the key for general settings. GENERAL = 1; // STORAGE is the key for storage settings. STORAGE = 2; // MEMO_RELATED is the key for memo related settings. MEMO_RELATED = 3; // TAGS is the key for tag metadata. TAGS = 4; // NOTIFICATION is the key for notification transport settings. NOTIFICATION = 5; } // General instance settings configuration. message GeneralSetting { // 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. CustomProfile 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; // Custom profile configuration for instance branding. message CustomProfile { string title = 1; string description = 2; string logo_url = 3; } } // Storage configuration settings for instance attachments. message StorageSetting { // Storage type enumeration for different storage backends. 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; // S3 configuration for cloud storage backend. // 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; } // Memo-related instance settings and policies. message MemoRelatedSetting { // 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; // reactions is the list of reactions. repeated string reactions = 7; } // Metadata for a tag. message TagMetadata { // Background color for the tag label. google.type.Color background_color = 1; } // Tag metadata configuration. message TagsSetting { map tags = 1; } // Notification transport configuration. message NotificationSetting { EmailSetting email = 1; // Email delivery configuration for notifications. message EmailSetting { bool enabled = 1; string smtp_host = 2; int32 smtp_port = 3; string smtp_username = 4; string smtp_password = 5; string from_email = 6; string from_name = 7; string reply_to = 8; bool use_tls = 9; bool use_ssl = 10; } } } // Request message for GetInstanceSetting method. message GetInstanceSettingRequest { // The resource name of the instance setting. // Format: instance/settings/{setting} string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/InstanceSetting"} ]; } // Request message for UpdateInstanceSetting method. message UpdateInstanceSettingRequest { // The instance setting resource which replaces the resource on the server. InstanceSetting setting = 1 [(google.api.field_behavior) = REQUIRED]; // The list of fields to update. google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = OPTIONAL]; } ================================================ FILE: proto/api/v1/memo_service.proto ================================================ syntax = "proto3"; package memos.api.v1; import "api/v1/attachment_service.proto"; import "api/v1/common.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"}; option (google.api.method_signature) = ""; } // 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"; } // 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=memos/*/reactions/*}"}; option (google.api.method_signature) = "name"; } // CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator. rpc CreateMemoShare(CreateMemoShareRequest) returns (MemoShare) { option (google.api.http) = { post: "/api/v1/{parent=memos/*}/shares" body: "memo_share" }; option (google.api.method_signature) = "parent,memo_share"; } // ListMemoShares lists all share links for a memo. Requires authentication as the memo creator. rpc ListMemoShares(ListMemoSharesRequest) returns (ListMemoSharesResponse) { option (google.api.http) = {get: "/api/v1/{parent=memos/*}/shares"}; option (google.api.method_signature) = "parent"; } // DeleteMemoShare revokes a share link. Requires authentication as the memo creator. rpc DeleteMemoShare(DeleteMemoShareRequest) returns (google.protobuf.Empty) { option (google.api.http) = {delete: "/api/v1/{name=memos/*/shares/*}"}; option (google.api.method_signature) = "name"; } // GetMemoByShare resolves a share token to its memo. No authentication required. // Returns NOT_FOUND if the token is invalid or expired. rpc GetMemoByShare(GetMemoByShareRequest) returns (Memo) { option (google.api.http) = {get: "/api/v1/shares/{share_id}"}; } } enum Visibility { VISIBILITY_UNSPECIFIED = 0; PRIVATE = 1; PROTECTED = 2; PUBLIC = 3; } message Reaction { option (google.api.resource) = { type: "memos.api.v1/Reaction" pattern: "memos/{memo}/reactions/{reaction}" name_field: "name" singular: "reaction" plural: "reactions" }; // The resource name of the reaction. // Format: memos/{memo}/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"} ]; // The creation timestamp. // If not set on creation, the server will set it to the current time. google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OPTIONAL]; // The last update timestamp. // If not set on creation, the server will set it to the current time. google.protobuf.Timestamp update_time = 5 [(google.api.field_behavior) = OPTIONAL]; // 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]; // 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; // The title extracted from the first H1 heading, if present. string title = 5; } } 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]; } message ListMemosRequest { // 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 = 1 [(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 = 2 [(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 = 3 [(google.api.field_behavior) = OPTIONAL]; // Optional. The order to sort results by. // Default to "display_time desc". // Supports comma-separated list of fields following AIP-132. // Example: "pinned desc, display_time desc" or "create_time asc" // Supported fields: pinned, display_time, create_time, update_time, name string order_by = 4 [(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 = 5 [(google.api.field_behavior) = OPTIONAL]; // Optional. If true, show deleted memos in the response. bool show_deleted = 6 [(google.api.field_behavior) = OPTIONAL]; } 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; } 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"} ]; } 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]; } 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 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; } 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; } 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: memos/{memo}/reactions/{reaction} string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/Reaction"} ]; } // MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token. message MemoShare { option (google.api.resource) = { type: "memos.api.v1/MemoShare" pattern: "memos/{memo}/shares/{share}" singular: "share" plural: "shares" }; // The resource name of the share. Format: memos/{memo}/shares/{share} // The {share} segment is the opaque token used in the share URL. string name = 1 [(google.api.field_behavior) = IDENTIFIER]; // Output only. When this share link was created. google.protobuf.Timestamp create_time = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; // Optional. When set, the share link stops working after this time. // If unset, the link never expires. optional google.protobuf.Timestamp expire_time = 3 [(google.api.field_behavior) = OPTIONAL]; } message CreateMemoShareRequest { // Required. The resource name of the memo to share. // Format: memos/{memo} string parent = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/Memo"} ]; // Required. The share to create. MemoShare memo_share = 2 [(google.api.field_behavior) = REQUIRED]; } message ListMemoSharesRequest { // Required. The resource name of the memo. // Format: memos/{memo} string parent = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/Memo"} ]; } message ListMemoSharesResponse { // The list of share links. repeated MemoShare memo_shares = 1; } message DeleteMemoShareRequest { // Required. The resource name of the share to delete. // Format: memos/{memo}/shares/{share} string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/MemoShare"} ]; } message GetMemoByShareRequest { // Required. The share token extracted from the share URL (/s/{share_id}). string share_id = 1 [(google.api.field_behavior) = REQUIRED]; } ================================================ FILE: proto/api/v1/shortcut_service.proto ================================================ 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"} ]; } ================================================ FILE: proto/api/v1/user_service.proto ================================================ 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/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 ID or username. // Supports both numeric IDs and username strings: // - users/{id} (e.g., users/101) // - users/{username} (e.g., users/steven) 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"; } // 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/*/settings/*}"}; 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/*/settings/*}" body: "setting" }; option (google.api.method_signature) = "setting,update_mask"; } // ListUserSettings returns a list of user settings. rpc ListUserSettings(ListUserSettingsRequest) returns (ListUserSettingsResponse) { option (google.api.http) = {get: "/api/v1/{parent=users/*}/settings"}; option (google.api.method_signature) = "parent"; } // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) { option (google.api.http) = {get: "/api/v1/{parent=users/*}/personalAccessTokens"}; option (google.api.method_signature) = "parent"; } // CreatePersonalAccessToken creates a new Personal Access Token for a user. // The token value is only returned once upon creation. rpc CreatePersonalAccessToken(CreatePersonalAccessTokenRequest) returns (CreatePersonalAccessTokenResponse) { option (google.api.http) = { post: "/api/v1/{parent=users/*}/personalAccessTokens" body: "*" }; } // DeletePersonalAccessToken deletes a Personal Access Token. rpc DeletePersonalAccessToken(DeletePersonalAccessTokenRequest) returns (google.protobuf.Empty) { option (google.api.http) = {delete: "/api/v1/{name=users/*/personalAccessTokens/*}"}; option (google.api.method_signature) = "name"; } // ListUserWebhooks returns a list of webhooks for a user. rpc ListUserWebhooks(ListUserWebhooksRequest) returns (ListUserWebhooksResponse) { option (google.api.http) = {get: "/api/v1/{parent=users/*}/webhooks"}; option (google.api.method_signature) = "parent"; } // CreateUserWebhook creates a new webhook for a user. rpc CreateUserWebhook(CreateUserWebhookRequest) returns (UserWebhook) { option (google.api.http) = { post: "/api/v1/{parent=users/*}/webhooks" body: "webhook" }; option (google.api.method_signature) = "parent,webhook"; } // UpdateUserWebhook updates an existing webhook for a user. rpc UpdateUserWebhook(UpdateUserWebhookRequest) returns (UserWebhook) { option (google.api.http) = { patch: "/api/v1/{webhook.name=users/*/webhooks/*}" body: "webhook" }; option (google.api.method_signature) = "webhook,update_mask"; } // DeleteUserWebhook deletes a webhook for a user. rpc DeleteUserWebhook(DeleteUserWebhookRequest) returns (google.protobuf.Empty) { option (google.api.http) = {delete: "/api/v1/{name=users/*/webhooks/*}"}; option (google.api.method_signature) = "name"; } // ListUserNotifications lists notifications for a user. rpc ListUserNotifications(ListUserNotificationsRequest) returns (ListUserNotificationsResponse) { option (google.api.http) = {get: "/api/v1/{parent=users/*}/notifications"}; option (google.api.method_signature) = "parent"; } // UpdateUserNotification updates a notification. rpc UpdateUserNotification(UpdateUserNotificationRequest) returns (UserNotification) { option (google.api.http) = { patch: "/api/v1/{notification.name=users/*/notifications/*}" body: "notification" }; option (google.api.method_signature) = "notification,update_mask"; } // DeleteUserNotification deletes a notification. rpc DeleteUserNotification(DeleteUserNotificationRequest) returns (google.protobuf.Empty) { option (google.api.http) = {delete: "/api/v1/{name=users/*/notifications/*}"}; 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 { ROLE_UNSPECIFIED = 0; // Admin role with system access. ADMIN = 2; // User role with limited access. 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: "username == 'steven'" // Supported operators: == // Supported fields: username string filter = 3 [(google.api.field_behavior) = OPTIONAL]; // Optional. If true, show deleted users in the response. bool show_deleted = 4 [(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. // Supports both numeric IDs and username strings: // - users/{id} (e.g., users/101) // - users/{username} (e.g., users/steven) // Format: users/{id_or_username} 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]; } // 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"} ]; } message ListAllUserStatsRequest { // This endpoint doesn't take any parameters. } message ListAllUserStatsResponse { // The list of user statistics. repeated UserStats stats = 1; } // User settings message message UserSetting { option (google.api.resource) = { type: "memos.api.v1/UserSetting" pattern: "users/{user}/settings/{setting}" singular: "userSetting" plural: "userSettings" }; // The name of the user setting. // Format: users/{user}/settings/{setting}, {setting} is the key for the setting. // For example, "users/123/settings/GENERAL" for general settings. string name = 1 [(google.api.field_behavior) = IDENTIFIER]; oneof value { GeneralSetting general_setting = 2; WebhooksSetting webhooks_setting = 5; } // Enumeration of user setting keys. enum Key { KEY_UNSPECIFIED = 0; // GENERAL is the key for general user settings. GENERAL = 1; // WEBHOOKS is the key for user webhooks. WEBHOOKS = 4; } // General user settings configuration. message GeneralSetting { // The preferred locale of the user. string locale = 1 [(google.api.field_behavior) = OPTIONAL]; // The default visibility of the memo. string memo_visibility = 3 [(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 = 4 [(google.api.field_behavior) = OPTIONAL]; } // User webhooks configuration. message WebhooksSetting { // List of user webhooks. repeated UserWebhook webhooks = 1; } } message GetUserSettingRequest { // Required. The resource name of the user setting. // Format: users/{user}/settings/{setting} string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/UserSetting"} ]; } 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]; } // Request message for ListUserSettings method. message ListUserSettingsRequest { // Required. The parent resource whose settings 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 settings to return. // The service may return fewer than this value. // If unspecified, at most 50 settings 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 `ListUserSettings` call. // Provide this to retrieve the subsequent page. string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; } // Response message for ListUserSettings method. message ListUserSettingsResponse { // The list of user settings. repeated UserSetting settings = 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 settings (may be approximate). int32 total_size = 3; } // PersonalAccessToken represents a long-lived token for API/script access. // PATs are distinct from short-lived JWT access tokens used for session authentication. message PersonalAccessToken { option (google.api.resource) = { type: "memos.api.v1/PersonalAccessToken" pattern: "users/{user}/personalAccessTokens/{personal_access_token}" singular: "personalAccessToken" plural: "personalAccessTokens" }; // The resource name of the personal access token. // Format: users/{user}/personalAccessTokens/{personal_access_token} string name = 1 [(google.api.field_behavior) = IDENTIFIER]; // The description of the token. string description = 2 [(google.api.field_behavior) = OPTIONAL]; // Output only. The creation timestamp. google.protobuf.Timestamp created_at = 3 [(google.api.field_behavior) = OUTPUT_ONLY]; // Optional. The expiration timestamp. google.protobuf.Timestamp expires_at = 4 [(google.api.field_behavior) = OPTIONAL]; // Output only. The last used timestamp. google.protobuf.Timestamp last_used_at = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; } message ListPersonalAccessTokensRequest { // Required. The parent resource whose personal 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 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 ListPersonalAccessTokensResponse { // The list of personal access tokens. repeated PersonalAccessToken personal_access_tokens = 1; // A token for the next page of results. string next_page_token = 2; // The total count of personal access tokens. int32 total_size = 3; } message CreatePersonalAccessTokenRequest { // Required. The parent resource where this token will be created. // Format: users/{user} string parent = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/User"} ]; // Optional. Description of the personal access token. string description = 2 [(google.api.field_behavior) = OPTIONAL]; // Optional. Expiration duration in days (0 = never expires). int32 expires_in_days = 3 [(google.api.field_behavior) = OPTIONAL]; } message CreatePersonalAccessTokenResponse { // The personal access token metadata. PersonalAccessToken personal_access_token = 1; // The actual token value - only returned on creation. // This is the only time the token value will be visible. string token = 2; } message DeletePersonalAccessTokenRequest { // Required. The resource name of the personal access token to delete. // Format: users/{user}/personalAccessTokens/{personal_access_token} string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/PersonalAccessToken"} ]; } // UserWebhook represents a webhook owned by a user. message UserWebhook { // The name of the webhook. // Format: users/{user}/webhooks/{webhook} string name = 1; // The URL to send the webhook to. string url = 2; // Optional. Human-readable name for the webhook. string display_name = 3; // The creation time of the webhook. google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; // The last update time of the webhook. google.protobuf.Timestamp update_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; } message ListUserWebhooksRequest { // The parent user resource. // Format: users/{user} string parent = 1 [(google.api.field_behavior) = REQUIRED]; } message ListUserWebhooksResponse { // The list of webhooks. repeated UserWebhook webhooks = 1; } message CreateUserWebhookRequest { // The parent user resource. // Format: users/{user} string parent = 1 [(google.api.field_behavior) = REQUIRED]; // The webhook to create. UserWebhook webhook = 2 [(google.api.field_behavior) = REQUIRED]; } message UpdateUserWebhookRequest { // The webhook to update. UserWebhook webhook = 1 [(google.api.field_behavior) = REQUIRED]; // The list of fields to update. google.protobuf.FieldMask update_mask = 2; } message DeleteUserWebhookRequest { // The name of the webhook to delete. // Format: users/{user}/webhooks/{webhook} string name = 1 [(google.api.field_behavior) = REQUIRED]; } message UserNotification { option (google.api.resource) = { type: "memos.api.v1/UserNotification" pattern: "users/{user}/notifications/{notification}" name_field: "name" singular: "notification" plural: "notifications" }; // The resource name of the notification. // Format: users/{user}/notifications/{notification} string name = 1 [ (google.api.field_behavior) = OUTPUT_ONLY, (google.api.field_behavior) = IDENTIFIER ]; // The sender of the notification. // Format: users/{user} string sender = 2 [ (google.api.field_behavior) = OUTPUT_ONLY, (google.api.resource_reference) = {type: "memos.api.v1/User"} ]; // The status of the notification. Status status = 3 [(google.api.field_behavior) = OPTIONAL]; // The creation timestamp. google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; // The type of the notification. Type type = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; oneof payload { MemoCommentPayload memo_comment = 6 [(google.api.field_behavior) = OUTPUT_ONLY]; } message MemoCommentPayload { // The memo name of comment. // Format: memos/{memo} string memo = 1; // The name of related memo. // Format: memos/{memo} string related_memo = 2; } enum Status { STATUS_UNSPECIFIED = 0; UNREAD = 1; ARCHIVED = 2; } enum Type { TYPE_UNSPECIFIED = 0; MEMO_COMMENT = 1; } } message ListUserNotificationsRequest { // The parent user resource. // Format: users/{user} string parent = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/User"} ]; int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]; string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; string filter = 4 [(google.api.field_behavior) = OPTIONAL]; } message ListUserNotificationsResponse { repeated UserNotification notifications = 1; string next_page_token = 2; } message UpdateUserNotificationRequest { UserNotification notification = 1 [(google.api.field_behavior) = REQUIRED]; google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; } message DeleteUserNotificationRequest { // Format: users/{user}/notifications/{notification} string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/UserNotification"} ]; } ================================================ FILE: proto/buf.gen.yaml ================================================ 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/connectrpc/go out: gen opt: paths=source_relative - remote: buf.build/grpc-ecosystem/gateway out: gen opt: paths=source_relative - remote: buf.build/community/google-gnostic-openapi out: gen opt: enum_type=string - remote: buf.build/bufbuild/es out: ../web/src/types/proto opt: - target=ts include_imports: true ================================================ FILE: proto/buf.yaml ================================================ 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 ================================================ FILE: proto/gen/api/v1/apiv1connect/attachment_service.connect.go ================================================ // Code generated by protoc-gen-connect-go. DO NOT EDIT. // // Source: api/v1/attachment_service.proto package apiv1connect import ( connect "connectrpc.com/connect" context "context" errors "errors" v1 "github.com/usememos/memos/proto/gen/api/v1" emptypb "google.golang.org/protobuf/types/known/emptypb" http "net/http" strings "strings" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // AttachmentServiceName is the fully-qualified name of the AttachmentService service. AttachmentServiceName = "memos.api.v1.AttachmentService" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // AttachmentServiceCreateAttachmentProcedure is the fully-qualified name of the AttachmentService's // CreateAttachment RPC. AttachmentServiceCreateAttachmentProcedure = "/memos.api.v1.AttachmentService/CreateAttachment" // AttachmentServiceListAttachmentsProcedure is the fully-qualified name of the AttachmentService's // ListAttachments RPC. AttachmentServiceListAttachmentsProcedure = "/memos.api.v1.AttachmentService/ListAttachments" // AttachmentServiceGetAttachmentProcedure is the fully-qualified name of the AttachmentService's // GetAttachment RPC. AttachmentServiceGetAttachmentProcedure = "/memos.api.v1.AttachmentService/GetAttachment" // AttachmentServiceUpdateAttachmentProcedure is the fully-qualified name of the AttachmentService's // UpdateAttachment RPC. AttachmentServiceUpdateAttachmentProcedure = "/memos.api.v1.AttachmentService/UpdateAttachment" // AttachmentServiceDeleteAttachmentProcedure is the fully-qualified name of the AttachmentService's // DeleteAttachment RPC. AttachmentServiceDeleteAttachmentProcedure = "/memos.api.v1.AttachmentService/DeleteAttachment" ) // AttachmentServiceClient is a client for the memos.api.v1.AttachmentService service. type AttachmentServiceClient interface { // CreateAttachment creates a new attachment. CreateAttachment(context.Context, *connect.Request[v1.CreateAttachmentRequest]) (*connect.Response[v1.Attachment], error) // ListAttachments lists all attachments. ListAttachments(context.Context, *connect.Request[v1.ListAttachmentsRequest]) (*connect.Response[v1.ListAttachmentsResponse], error) // GetAttachment returns an attachment by name. GetAttachment(context.Context, *connect.Request[v1.GetAttachmentRequest]) (*connect.Response[v1.Attachment], error) // UpdateAttachment updates an attachment. UpdateAttachment(context.Context, *connect.Request[v1.UpdateAttachmentRequest]) (*connect.Response[v1.Attachment], error) // DeleteAttachment deletes an attachment by name. DeleteAttachment(context.Context, *connect.Request[v1.DeleteAttachmentRequest]) (*connect.Response[emptypb.Empty], error) } // NewAttachmentServiceClient constructs a client for the memos.api.v1.AttachmentService service. By // default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, // and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the // connect.WithGRPC() or connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewAttachmentServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AttachmentServiceClient { baseURL = strings.TrimRight(baseURL, "/") attachmentServiceMethods := v1.File_api_v1_attachment_service_proto.Services().ByName("AttachmentService").Methods() return &attachmentServiceClient{ createAttachment: connect.NewClient[v1.CreateAttachmentRequest, v1.Attachment]( httpClient, baseURL+AttachmentServiceCreateAttachmentProcedure, connect.WithSchema(attachmentServiceMethods.ByName("CreateAttachment")), connect.WithClientOptions(opts...), ), listAttachments: connect.NewClient[v1.ListAttachmentsRequest, v1.ListAttachmentsResponse]( httpClient, baseURL+AttachmentServiceListAttachmentsProcedure, connect.WithSchema(attachmentServiceMethods.ByName("ListAttachments")), connect.WithClientOptions(opts...), ), getAttachment: connect.NewClient[v1.GetAttachmentRequest, v1.Attachment]( httpClient, baseURL+AttachmentServiceGetAttachmentProcedure, connect.WithSchema(attachmentServiceMethods.ByName("GetAttachment")), connect.WithClientOptions(opts...), ), updateAttachment: connect.NewClient[v1.UpdateAttachmentRequest, v1.Attachment]( httpClient, baseURL+AttachmentServiceUpdateAttachmentProcedure, connect.WithSchema(attachmentServiceMethods.ByName("UpdateAttachment")), connect.WithClientOptions(opts...), ), deleteAttachment: connect.NewClient[v1.DeleteAttachmentRequest, emptypb.Empty]( httpClient, baseURL+AttachmentServiceDeleteAttachmentProcedure, connect.WithSchema(attachmentServiceMethods.ByName("DeleteAttachment")), connect.WithClientOptions(opts...), ), } } // attachmentServiceClient implements AttachmentServiceClient. type attachmentServiceClient struct { createAttachment *connect.Client[v1.CreateAttachmentRequest, v1.Attachment] listAttachments *connect.Client[v1.ListAttachmentsRequest, v1.ListAttachmentsResponse] getAttachment *connect.Client[v1.GetAttachmentRequest, v1.Attachment] updateAttachment *connect.Client[v1.UpdateAttachmentRequest, v1.Attachment] deleteAttachment *connect.Client[v1.DeleteAttachmentRequest, emptypb.Empty] } // CreateAttachment calls memos.api.v1.AttachmentService.CreateAttachment. func (c *attachmentServiceClient) CreateAttachment(ctx context.Context, req *connect.Request[v1.CreateAttachmentRequest]) (*connect.Response[v1.Attachment], error) { return c.createAttachment.CallUnary(ctx, req) } // ListAttachments calls memos.api.v1.AttachmentService.ListAttachments. func (c *attachmentServiceClient) ListAttachments(ctx context.Context, req *connect.Request[v1.ListAttachmentsRequest]) (*connect.Response[v1.ListAttachmentsResponse], error) { return c.listAttachments.CallUnary(ctx, req) } // GetAttachment calls memos.api.v1.AttachmentService.GetAttachment. func (c *attachmentServiceClient) GetAttachment(ctx context.Context, req *connect.Request[v1.GetAttachmentRequest]) (*connect.Response[v1.Attachment], error) { return c.getAttachment.CallUnary(ctx, req) } // UpdateAttachment calls memos.api.v1.AttachmentService.UpdateAttachment. func (c *attachmentServiceClient) UpdateAttachment(ctx context.Context, req *connect.Request[v1.UpdateAttachmentRequest]) (*connect.Response[v1.Attachment], error) { return c.updateAttachment.CallUnary(ctx, req) } // DeleteAttachment calls memos.api.v1.AttachmentService.DeleteAttachment. func (c *attachmentServiceClient) DeleteAttachment(ctx context.Context, req *connect.Request[v1.DeleteAttachmentRequest]) (*connect.Response[emptypb.Empty], error) { return c.deleteAttachment.CallUnary(ctx, req) } // AttachmentServiceHandler is an implementation of the memos.api.v1.AttachmentService service. type AttachmentServiceHandler interface { // CreateAttachment creates a new attachment. CreateAttachment(context.Context, *connect.Request[v1.CreateAttachmentRequest]) (*connect.Response[v1.Attachment], error) // ListAttachments lists all attachments. ListAttachments(context.Context, *connect.Request[v1.ListAttachmentsRequest]) (*connect.Response[v1.ListAttachmentsResponse], error) // GetAttachment returns an attachment by name. GetAttachment(context.Context, *connect.Request[v1.GetAttachmentRequest]) (*connect.Response[v1.Attachment], error) // UpdateAttachment updates an attachment. UpdateAttachment(context.Context, *connect.Request[v1.UpdateAttachmentRequest]) (*connect.Response[v1.Attachment], error) // DeleteAttachment deletes an attachment by name. DeleteAttachment(context.Context, *connect.Request[v1.DeleteAttachmentRequest]) (*connect.Response[emptypb.Empty], error) } // NewAttachmentServiceHandler builds an HTTP handler from the service implementation. It returns // the path on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewAttachmentServiceHandler(svc AttachmentServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { attachmentServiceMethods := v1.File_api_v1_attachment_service_proto.Services().ByName("AttachmentService").Methods() attachmentServiceCreateAttachmentHandler := connect.NewUnaryHandler( AttachmentServiceCreateAttachmentProcedure, svc.CreateAttachment, connect.WithSchema(attachmentServiceMethods.ByName("CreateAttachment")), connect.WithHandlerOptions(opts...), ) attachmentServiceListAttachmentsHandler := connect.NewUnaryHandler( AttachmentServiceListAttachmentsProcedure, svc.ListAttachments, connect.WithSchema(attachmentServiceMethods.ByName("ListAttachments")), connect.WithHandlerOptions(opts...), ) attachmentServiceGetAttachmentHandler := connect.NewUnaryHandler( AttachmentServiceGetAttachmentProcedure, svc.GetAttachment, connect.WithSchema(attachmentServiceMethods.ByName("GetAttachment")), connect.WithHandlerOptions(opts...), ) attachmentServiceUpdateAttachmentHandler := connect.NewUnaryHandler( AttachmentServiceUpdateAttachmentProcedure, svc.UpdateAttachment, connect.WithSchema(attachmentServiceMethods.ByName("UpdateAttachment")), connect.WithHandlerOptions(opts...), ) attachmentServiceDeleteAttachmentHandler := connect.NewUnaryHandler( AttachmentServiceDeleteAttachmentProcedure, svc.DeleteAttachment, connect.WithSchema(attachmentServiceMethods.ByName("DeleteAttachment")), connect.WithHandlerOptions(opts...), ) return "/memos.api.v1.AttachmentService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case AttachmentServiceCreateAttachmentProcedure: attachmentServiceCreateAttachmentHandler.ServeHTTP(w, r) case AttachmentServiceListAttachmentsProcedure: attachmentServiceListAttachmentsHandler.ServeHTTP(w, r) case AttachmentServiceGetAttachmentProcedure: attachmentServiceGetAttachmentHandler.ServeHTTP(w, r) case AttachmentServiceUpdateAttachmentProcedure: attachmentServiceUpdateAttachmentHandler.ServeHTTP(w, r) case AttachmentServiceDeleteAttachmentProcedure: attachmentServiceDeleteAttachmentHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedAttachmentServiceHandler returns CodeUnimplemented from all methods. type UnimplementedAttachmentServiceHandler struct{} func (UnimplementedAttachmentServiceHandler) CreateAttachment(context.Context, *connect.Request[v1.CreateAttachmentRequest]) (*connect.Response[v1.Attachment], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AttachmentService.CreateAttachment is not implemented")) } func (UnimplementedAttachmentServiceHandler) ListAttachments(context.Context, *connect.Request[v1.ListAttachmentsRequest]) (*connect.Response[v1.ListAttachmentsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AttachmentService.ListAttachments is not implemented")) } func (UnimplementedAttachmentServiceHandler) GetAttachment(context.Context, *connect.Request[v1.GetAttachmentRequest]) (*connect.Response[v1.Attachment], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AttachmentService.GetAttachment is not implemented")) } func (UnimplementedAttachmentServiceHandler) UpdateAttachment(context.Context, *connect.Request[v1.UpdateAttachmentRequest]) (*connect.Response[v1.Attachment], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AttachmentService.UpdateAttachment is not implemented")) } func (UnimplementedAttachmentServiceHandler) DeleteAttachment(context.Context, *connect.Request[v1.DeleteAttachmentRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AttachmentService.DeleteAttachment is not implemented")) } ================================================ FILE: proto/gen/api/v1/apiv1connect/auth_service.connect.go ================================================ // Code generated by protoc-gen-connect-go. DO NOT EDIT. // // Source: api/v1/auth_service.proto package apiv1connect import ( connect "connectrpc.com/connect" context "context" errors "errors" v1 "github.com/usememos/memos/proto/gen/api/v1" emptypb "google.golang.org/protobuf/types/known/emptypb" http "net/http" strings "strings" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // AuthServiceName is the fully-qualified name of the AuthService service. AuthServiceName = "memos.api.v1.AuthService" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // AuthServiceGetCurrentUserProcedure is the fully-qualified name of the AuthService's // GetCurrentUser RPC. AuthServiceGetCurrentUserProcedure = "/memos.api.v1.AuthService/GetCurrentUser" // AuthServiceSignInProcedure is the fully-qualified name of the AuthService's SignIn RPC. AuthServiceSignInProcedure = "/memos.api.v1.AuthService/SignIn" // AuthServiceSignOutProcedure is the fully-qualified name of the AuthService's SignOut RPC. AuthServiceSignOutProcedure = "/memos.api.v1.AuthService/SignOut" // AuthServiceRefreshTokenProcedure is the fully-qualified name of the AuthService's RefreshToken // RPC. AuthServiceRefreshTokenProcedure = "/memos.api.v1.AuthService/RefreshToken" ) // AuthServiceClient is a client for the memos.api.v1.AuthService service. type AuthServiceClient interface { // GetCurrentUser returns the authenticated user's information. // Validates the access token and returns user details. // Similar to OIDC's /userinfo endpoint. GetCurrentUser(context.Context, *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error) // SignIn authenticates a user with credentials and returns tokens. // On success, returns an access token and sets a refresh token cookie. // Supports password-based and SSO authentication methods. SignIn(context.Context, *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error) // SignOut terminates the user's authentication. // Revokes the refresh token and clears the authentication cookie. SignOut(context.Context, *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error) // RefreshToken exchanges a valid refresh token for a new access token. // The refresh token is read from the HttpOnly cookie. // Returns a new short-lived access token. RefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error) } // NewAuthServiceClient constructs a client for the memos.api.v1.AuthService service. By default, it // uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or // connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewAuthServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AuthServiceClient { baseURL = strings.TrimRight(baseURL, "/") authServiceMethods := v1.File_api_v1_auth_service_proto.Services().ByName("AuthService").Methods() return &authServiceClient{ getCurrentUser: connect.NewClient[v1.GetCurrentUserRequest, v1.GetCurrentUserResponse]( httpClient, baseURL+AuthServiceGetCurrentUserProcedure, connect.WithSchema(authServiceMethods.ByName("GetCurrentUser")), connect.WithClientOptions(opts...), ), signIn: connect.NewClient[v1.SignInRequest, v1.SignInResponse]( httpClient, baseURL+AuthServiceSignInProcedure, connect.WithSchema(authServiceMethods.ByName("SignIn")), connect.WithClientOptions(opts...), ), signOut: connect.NewClient[v1.SignOutRequest, emptypb.Empty]( httpClient, baseURL+AuthServiceSignOutProcedure, connect.WithSchema(authServiceMethods.ByName("SignOut")), connect.WithClientOptions(opts...), ), refreshToken: connect.NewClient[v1.RefreshTokenRequest, v1.RefreshTokenResponse]( httpClient, baseURL+AuthServiceRefreshTokenProcedure, connect.WithSchema(authServiceMethods.ByName("RefreshToken")), connect.WithClientOptions(opts...), ), } } // authServiceClient implements AuthServiceClient. type authServiceClient struct { getCurrentUser *connect.Client[v1.GetCurrentUserRequest, v1.GetCurrentUserResponse] signIn *connect.Client[v1.SignInRequest, v1.SignInResponse] signOut *connect.Client[v1.SignOutRequest, emptypb.Empty] refreshToken *connect.Client[v1.RefreshTokenRequest, v1.RefreshTokenResponse] } // GetCurrentUser calls memos.api.v1.AuthService.GetCurrentUser. func (c *authServiceClient) GetCurrentUser(ctx context.Context, req *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error) { return c.getCurrentUser.CallUnary(ctx, req) } // SignIn calls memos.api.v1.AuthService.SignIn. func (c *authServiceClient) SignIn(ctx context.Context, req *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error) { return c.signIn.CallUnary(ctx, req) } // SignOut calls memos.api.v1.AuthService.SignOut. func (c *authServiceClient) SignOut(ctx context.Context, req *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error) { return c.signOut.CallUnary(ctx, req) } // RefreshToken calls memos.api.v1.AuthService.RefreshToken. func (c *authServiceClient) RefreshToken(ctx context.Context, req *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error) { return c.refreshToken.CallUnary(ctx, req) } // AuthServiceHandler is an implementation of the memos.api.v1.AuthService service. type AuthServiceHandler interface { // GetCurrentUser returns the authenticated user's information. // Validates the access token and returns user details. // Similar to OIDC's /userinfo endpoint. GetCurrentUser(context.Context, *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error) // SignIn authenticates a user with credentials and returns tokens. // On success, returns an access token and sets a refresh token cookie. // Supports password-based and SSO authentication methods. SignIn(context.Context, *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error) // SignOut terminates the user's authentication. // Revokes the refresh token and clears the authentication cookie. SignOut(context.Context, *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error) // RefreshToken exchanges a valid refresh token for a new access token. // The refresh token is read from the HttpOnly cookie. // Returns a new short-lived access token. RefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error) } // NewAuthServiceHandler builds an HTTP handler from the service implementation. It returns the path // on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewAuthServiceHandler(svc AuthServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { authServiceMethods := v1.File_api_v1_auth_service_proto.Services().ByName("AuthService").Methods() authServiceGetCurrentUserHandler := connect.NewUnaryHandler( AuthServiceGetCurrentUserProcedure, svc.GetCurrentUser, connect.WithSchema(authServiceMethods.ByName("GetCurrentUser")), connect.WithHandlerOptions(opts...), ) authServiceSignInHandler := connect.NewUnaryHandler( AuthServiceSignInProcedure, svc.SignIn, connect.WithSchema(authServiceMethods.ByName("SignIn")), connect.WithHandlerOptions(opts...), ) authServiceSignOutHandler := connect.NewUnaryHandler( AuthServiceSignOutProcedure, svc.SignOut, connect.WithSchema(authServiceMethods.ByName("SignOut")), connect.WithHandlerOptions(opts...), ) authServiceRefreshTokenHandler := connect.NewUnaryHandler( AuthServiceRefreshTokenProcedure, svc.RefreshToken, connect.WithSchema(authServiceMethods.ByName("RefreshToken")), connect.WithHandlerOptions(opts...), ) return "/memos.api.v1.AuthService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case AuthServiceGetCurrentUserProcedure: authServiceGetCurrentUserHandler.ServeHTTP(w, r) case AuthServiceSignInProcedure: authServiceSignInHandler.ServeHTTP(w, r) case AuthServiceSignOutProcedure: authServiceSignOutHandler.ServeHTTP(w, r) case AuthServiceRefreshTokenProcedure: authServiceRefreshTokenHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedAuthServiceHandler returns CodeUnimplemented from all methods. type UnimplementedAuthServiceHandler struct{} func (UnimplementedAuthServiceHandler) GetCurrentUser(context.Context, *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.GetCurrentUser is not implemented")) } func (UnimplementedAuthServiceHandler) SignIn(context.Context, *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.SignIn is not implemented")) } func (UnimplementedAuthServiceHandler) SignOut(context.Context, *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.SignOut is not implemented")) } func (UnimplementedAuthServiceHandler) RefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.RefreshToken is not implemented")) } ================================================ FILE: proto/gen/api/v1/apiv1connect/idp_service.connect.go ================================================ // Code generated by protoc-gen-connect-go. DO NOT EDIT. // // Source: api/v1/idp_service.proto package apiv1connect import ( connect "connectrpc.com/connect" context "context" errors "errors" v1 "github.com/usememos/memos/proto/gen/api/v1" emptypb "google.golang.org/protobuf/types/known/emptypb" http "net/http" strings "strings" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // IdentityProviderServiceName is the fully-qualified name of the IdentityProviderService service. IdentityProviderServiceName = "memos.api.v1.IdentityProviderService" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // IdentityProviderServiceListIdentityProvidersProcedure is the fully-qualified name of the // IdentityProviderService's ListIdentityProviders RPC. IdentityProviderServiceListIdentityProvidersProcedure = "/memos.api.v1.IdentityProviderService/ListIdentityProviders" // IdentityProviderServiceGetIdentityProviderProcedure is the fully-qualified name of the // IdentityProviderService's GetIdentityProvider RPC. IdentityProviderServiceGetIdentityProviderProcedure = "/memos.api.v1.IdentityProviderService/GetIdentityProvider" // IdentityProviderServiceCreateIdentityProviderProcedure is the fully-qualified name of the // IdentityProviderService's CreateIdentityProvider RPC. IdentityProviderServiceCreateIdentityProviderProcedure = "/memos.api.v1.IdentityProviderService/CreateIdentityProvider" // IdentityProviderServiceUpdateIdentityProviderProcedure is the fully-qualified name of the // IdentityProviderService's UpdateIdentityProvider RPC. IdentityProviderServiceUpdateIdentityProviderProcedure = "/memos.api.v1.IdentityProviderService/UpdateIdentityProvider" // IdentityProviderServiceDeleteIdentityProviderProcedure is the fully-qualified name of the // IdentityProviderService's DeleteIdentityProvider RPC. IdentityProviderServiceDeleteIdentityProviderProcedure = "/memos.api.v1.IdentityProviderService/DeleteIdentityProvider" ) // IdentityProviderServiceClient is a client for the memos.api.v1.IdentityProviderService service. type IdentityProviderServiceClient interface { // ListIdentityProviders lists identity providers. ListIdentityProviders(context.Context, *connect.Request[v1.ListIdentityProvidersRequest]) (*connect.Response[v1.ListIdentityProvidersResponse], error) // GetIdentityProvider gets an identity provider. GetIdentityProvider(context.Context, *connect.Request[v1.GetIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) // CreateIdentityProvider creates an identity provider. CreateIdentityProvider(context.Context, *connect.Request[v1.CreateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) // UpdateIdentityProvider updates an identity provider. UpdateIdentityProvider(context.Context, *connect.Request[v1.UpdateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) // DeleteIdentityProvider deletes an identity provider. DeleteIdentityProvider(context.Context, *connect.Request[v1.DeleteIdentityProviderRequest]) (*connect.Response[emptypb.Empty], error) } // NewIdentityProviderServiceClient constructs a client for the memos.api.v1.IdentityProviderService // service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for // gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply // the connect.WithGRPC() or connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewIdentityProviderServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) IdentityProviderServiceClient { baseURL = strings.TrimRight(baseURL, "/") identityProviderServiceMethods := v1.File_api_v1_idp_service_proto.Services().ByName("IdentityProviderService").Methods() return &identityProviderServiceClient{ listIdentityProviders: connect.NewClient[v1.ListIdentityProvidersRequest, v1.ListIdentityProvidersResponse]( httpClient, baseURL+IdentityProviderServiceListIdentityProvidersProcedure, connect.WithSchema(identityProviderServiceMethods.ByName("ListIdentityProviders")), connect.WithClientOptions(opts...), ), getIdentityProvider: connect.NewClient[v1.GetIdentityProviderRequest, v1.IdentityProvider]( httpClient, baseURL+IdentityProviderServiceGetIdentityProviderProcedure, connect.WithSchema(identityProviderServiceMethods.ByName("GetIdentityProvider")), connect.WithClientOptions(opts...), ), createIdentityProvider: connect.NewClient[v1.CreateIdentityProviderRequest, v1.IdentityProvider]( httpClient, baseURL+IdentityProviderServiceCreateIdentityProviderProcedure, connect.WithSchema(identityProviderServiceMethods.ByName("CreateIdentityProvider")), connect.WithClientOptions(opts...), ), updateIdentityProvider: connect.NewClient[v1.UpdateIdentityProviderRequest, v1.IdentityProvider]( httpClient, baseURL+IdentityProviderServiceUpdateIdentityProviderProcedure, connect.WithSchema(identityProviderServiceMethods.ByName("UpdateIdentityProvider")), connect.WithClientOptions(opts...), ), deleteIdentityProvider: connect.NewClient[v1.DeleteIdentityProviderRequest, emptypb.Empty]( httpClient, baseURL+IdentityProviderServiceDeleteIdentityProviderProcedure, connect.WithSchema(identityProviderServiceMethods.ByName("DeleteIdentityProvider")), connect.WithClientOptions(opts...), ), } } // identityProviderServiceClient implements IdentityProviderServiceClient. type identityProviderServiceClient struct { listIdentityProviders *connect.Client[v1.ListIdentityProvidersRequest, v1.ListIdentityProvidersResponse] getIdentityProvider *connect.Client[v1.GetIdentityProviderRequest, v1.IdentityProvider] createIdentityProvider *connect.Client[v1.CreateIdentityProviderRequest, v1.IdentityProvider] updateIdentityProvider *connect.Client[v1.UpdateIdentityProviderRequest, v1.IdentityProvider] deleteIdentityProvider *connect.Client[v1.DeleteIdentityProviderRequest, emptypb.Empty] } // ListIdentityProviders calls memos.api.v1.IdentityProviderService.ListIdentityProviders. func (c *identityProviderServiceClient) ListIdentityProviders(ctx context.Context, req *connect.Request[v1.ListIdentityProvidersRequest]) (*connect.Response[v1.ListIdentityProvidersResponse], error) { return c.listIdentityProviders.CallUnary(ctx, req) } // GetIdentityProvider calls memos.api.v1.IdentityProviderService.GetIdentityProvider. func (c *identityProviderServiceClient) GetIdentityProvider(ctx context.Context, req *connect.Request[v1.GetIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) { return c.getIdentityProvider.CallUnary(ctx, req) } // CreateIdentityProvider calls memos.api.v1.IdentityProviderService.CreateIdentityProvider. func (c *identityProviderServiceClient) CreateIdentityProvider(ctx context.Context, req *connect.Request[v1.CreateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) { return c.createIdentityProvider.CallUnary(ctx, req) } // UpdateIdentityProvider calls memos.api.v1.IdentityProviderService.UpdateIdentityProvider. func (c *identityProviderServiceClient) UpdateIdentityProvider(ctx context.Context, req *connect.Request[v1.UpdateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) { return c.updateIdentityProvider.CallUnary(ctx, req) } // DeleteIdentityProvider calls memos.api.v1.IdentityProviderService.DeleteIdentityProvider. func (c *identityProviderServiceClient) DeleteIdentityProvider(ctx context.Context, req *connect.Request[v1.DeleteIdentityProviderRequest]) (*connect.Response[emptypb.Empty], error) { return c.deleteIdentityProvider.CallUnary(ctx, req) } // IdentityProviderServiceHandler is an implementation of the memos.api.v1.IdentityProviderService // service. type IdentityProviderServiceHandler interface { // ListIdentityProviders lists identity providers. ListIdentityProviders(context.Context, *connect.Request[v1.ListIdentityProvidersRequest]) (*connect.Response[v1.ListIdentityProvidersResponse], error) // GetIdentityProvider gets an identity provider. GetIdentityProvider(context.Context, *connect.Request[v1.GetIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) // CreateIdentityProvider creates an identity provider. CreateIdentityProvider(context.Context, *connect.Request[v1.CreateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) // UpdateIdentityProvider updates an identity provider. UpdateIdentityProvider(context.Context, *connect.Request[v1.UpdateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) // DeleteIdentityProvider deletes an identity provider. DeleteIdentityProvider(context.Context, *connect.Request[v1.DeleteIdentityProviderRequest]) (*connect.Response[emptypb.Empty], error) } // NewIdentityProviderServiceHandler builds an HTTP handler from the service implementation. It // returns the path on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewIdentityProviderServiceHandler(svc IdentityProviderServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { identityProviderServiceMethods := v1.File_api_v1_idp_service_proto.Services().ByName("IdentityProviderService").Methods() identityProviderServiceListIdentityProvidersHandler := connect.NewUnaryHandler( IdentityProviderServiceListIdentityProvidersProcedure, svc.ListIdentityProviders, connect.WithSchema(identityProviderServiceMethods.ByName("ListIdentityProviders")), connect.WithHandlerOptions(opts...), ) identityProviderServiceGetIdentityProviderHandler := connect.NewUnaryHandler( IdentityProviderServiceGetIdentityProviderProcedure, svc.GetIdentityProvider, connect.WithSchema(identityProviderServiceMethods.ByName("GetIdentityProvider")), connect.WithHandlerOptions(opts...), ) identityProviderServiceCreateIdentityProviderHandler := connect.NewUnaryHandler( IdentityProviderServiceCreateIdentityProviderProcedure, svc.CreateIdentityProvider, connect.WithSchema(identityProviderServiceMethods.ByName("CreateIdentityProvider")), connect.WithHandlerOptions(opts...), ) identityProviderServiceUpdateIdentityProviderHandler := connect.NewUnaryHandler( IdentityProviderServiceUpdateIdentityProviderProcedure, svc.UpdateIdentityProvider, connect.WithSchema(identityProviderServiceMethods.ByName("UpdateIdentityProvider")), connect.WithHandlerOptions(opts...), ) identityProviderServiceDeleteIdentityProviderHandler := connect.NewUnaryHandler( IdentityProviderServiceDeleteIdentityProviderProcedure, svc.DeleteIdentityProvider, connect.WithSchema(identityProviderServiceMethods.ByName("DeleteIdentityProvider")), connect.WithHandlerOptions(opts...), ) return "/memos.api.v1.IdentityProviderService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case IdentityProviderServiceListIdentityProvidersProcedure: identityProviderServiceListIdentityProvidersHandler.ServeHTTP(w, r) case IdentityProviderServiceGetIdentityProviderProcedure: identityProviderServiceGetIdentityProviderHandler.ServeHTTP(w, r) case IdentityProviderServiceCreateIdentityProviderProcedure: identityProviderServiceCreateIdentityProviderHandler.ServeHTTP(w, r) case IdentityProviderServiceUpdateIdentityProviderProcedure: identityProviderServiceUpdateIdentityProviderHandler.ServeHTTP(w, r) case IdentityProviderServiceDeleteIdentityProviderProcedure: identityProviderServiceDeleteIdentityProviderHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedIdentityProviderServiceHandler returns CodeUnimplemented from all methods. type UnimplementedIdentityProviderServiceHandler struct{} func (UnimplementedIdentityProviderServiceHandler) ListIdentityProviders(context.Context, *connect.Request[v1.ListIdentityProvidersRequest]) (*connect.Response[v1.ListIdentityProvidersResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.IdentityProviderService.ListIdentityProviders is not implemented")) } func (UnimplementedIdentityProviderServiceHandler) GetIdentityProvider(context.Context, *connect.Request[v1.GetIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.IdentityProviderService.GetIdentityProvider is not implemented")) } func (UnimplementedIdentityProviderServiceHandler) CreateIdentityProvider(context.Context, *connect.Request[v1.CreateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.IdentityProviderService.CreateIdentityProvider is not implemented")) } func (UnimplementedIdentityProviderServiceHandler) UpdateIdentityProvider(context.Context, *connect.Request[v1.UpdateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.IdentityProviderService.UpdateIdentityProvider is not implemented")) } func (UnimplementedIdentityProviderServiceHandler) DeleteIdentityProvider(context.Context, *connect.Request[v1.DeleteIdentityProviderRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.IdentityProviderService.DeleteIdentityProvider is not implemented")) } ================================================ FILE: proto/gen/api/v1/apiv1connect/instance_service.connect.go ================================================ // Code generated by protoc-gen-connect-go. DO NOT EDIT. // // Source: api/v1/instance_service.proto package apiv1connect import ( connect "connectrpc.com/connect" context "context" errors "errors" v1 "github.com/usememos/memos/proto/gen/api/v1" http "net/http" strings "strings" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // InstanceServiceName is the fully-qualified name of the InstanceService service. InstanceServiceName = "memos.api.v1.InstanceService" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // InstanceServiceGetInstanceProfileProcedure is the fully-qualified name of the InstanceService's // GetInstanceProfile RPC. InstanceServiceGetInstanceProfileProcedure = "/memos.api.v1.InstanceService/GetInstanceProfile" // InstanceServiceGetInstanceSettingProcedure is the fully-qualified name of the InstanceService's // GetInstanceSetting RPC. InstanceServiceGetInstanceSettingProcedure = "/memos.api.v1.InstanceService/GetInstanceSetting" // InstanceServiceUpdateInstanceSettingProcedure is the fully-qualified name of the // InstanceService's UpdateInstanceSetting RPC. InstanceServiceUpdateInstanceSettingProcedure = "/memos.api.v1.InstanceService/UpdateInstanceSetting" ) // InstanceServiceClient is a client for the memos.api.v1.InstanceService service. type InstanceServiceClient interface { // Gets the instance profile. GetInstanceProfile(context.Context, *connect.Request[v1.GetInstanceProfileRequest]) (*connect.Response[v1.InstanceProfile], error) // Gets an instance setting. GetInstanceSetting(context.Context, *connect.Request[v1.GetInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) // Updates an instance setting. UpdateInstanceSetting(context.Context, *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) } // NewInstanceServiceClient constructs a client for the memos.api.v1.InstanceService service. By // default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, // and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the // connect.WithGRPC() or connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewInstanceServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) InstanceServiceClient { baseURL = strings.TrimRight(baseURL, "/") instanceServiceMethods := v1.File_api_v1_instance_service_proto.Services().ByName("InstanceService").Methods() return &instanceServiceClient{ getInstanceProfile: connect.NewClient[v1.GetInstanceProfileRequest, v1.InstanceProfile]( httpClient, baseURL+InstanceServiceGetInstanceProfileProcedure, connect.WithSchema(instanceServiceMethods.ByName("GetInstanceProfile")), connect.WithClientOptions(opts...), ), getInstanceSetting: connect.NewClient[v1.GetInstanceSettingRequest, v1.InstanceSetting]( httpClient, baseURL+InstanceServiceGetInstanceSettingProcedure, connect.WithSchema(instanceServiceMethods.ByName("GetInstanceSetting")), connect.WithClientOptions(opts...), ), updateInstanceSetting: connect.NewClient[v1.UpdateInstanceSettingRequest, v1.InstanceSetting]( httpClient, baseURL+InstanceServiceUpdateInstanceSettingProcedure, connect.WithSchema(instanceServiceMethods.ByName("UpdateInstanceSetting")), connect.WithClientOptions(opts...), ), } } // instanceServiceClient implements InstanceServiceClient. type instanceServiceClient struct { getInstanceProfile *connect.Client[v1.GetInstanceProfileRequest, v1.InstanceProfile] getInstanceSetting *connect.Client[v1.GetInstanceSettingRequest, v1.InstanceSetting] updateInstanceSetting *connect.Client[v1.UpdateInstanceSettingRequest, v1.InstanceSetting] } // GetInstanceProfile calls memos.api.v1.InstanceService.GetInstanceProfile. func (c *instanceServiceClient) GetInstanceProfile(ctx context.Context, req *connect.Request[v1.GetInstanceProfileRequest]) (*connect.Response[v1.InstanceProfile], error) { return c.getInstanceProfile.CallUnary(ctx, req) } // GetInstanceSetting calls memos.api.v1.InstanceService.GetInstanceSetting. func (c *instanceServiceClient) GetInstanceSetting(ctx context.Context, req *connect.Request[v1.GetInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) { return c.getInstanceSetting.CallUnary(ctx, req) } // UpdateInstanceSetting calls memos.api.v1.InstanceService.UpdateInstanceSetting. func (c *instanceServiceClient) UpdateInstanceSetting(ctx context.Context, req *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) { return c.updateInstanceSetting.CallUnary(ctx, req) } // InstanceServiceHandler is an implementation of the memos.api.v1.InstanceService service. type InstanceServiceHandler interface { // Gets the instance profile. GetInstanceProfile(context.Context, *connect.Request[v1.GetInstanceProfileRequest]) (*connect.Response[v1.InstanceProfile], error) // Gets an instance setting. GetInstanceSetting(context.Context, *connect.Request[v1.GetInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) // Updates an instance setting. UpdateInstanceSetting(context.Context, *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) } // NewInstanceServiceHandler builds an HTTP handler from the service implementation. It returns the // path on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewInstanceServiceHandler(svc InstanceServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { instanceServiceMethods := v1.File_api_v1_instance_service_proto.Services().ByName("InstanceService").Methods() instanceServiceGetInstanceProfileHandler := connect.NewUnaryHandler( InstanceServiceGetInstanceProfileProcedure, svc.GetInstanceProfile, connect.WithSchema(instanceServiceMethods.ByName("GetInstanceProfile")), connect.WithHandlerOptions(opts...), ) instanceServiceGetInstanceSettingHandler := connect.NewUnaryHandler( InstanceServiceGetInstanceSettingProcedure, svc.GetInstanceSetting, connect.WithSchema(instanceServiceMethods.ByName("GetInstanceSetting")), connect.WithHandlerOptions(opts...), ) instanceServiceUpdateInstanceSettingHandler := connect.NewUnaryHandler( InstanceServiceUpdateInstanceSettingProcedure, svc.UpdateInstanceSetting, connect.WithSchema(instanceServiceMethods.ByName("UpdateInstanceSetting")), connect.WithHandlerOptions(opts...), ) return "/memos.api.v1.InstanceService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case InstanceServiceGetInstanceProfileProcedure: instanceServiceGetInstanceProfileHandler.ServeHTTP(w, r) case InstanceServiceGetInstanceSettingProcedure: instanceServiceGetInstanceSettingHandler.ServeHTTP(w, r) case InstanceServiceUpdateInstanceSettingProcedure: instanceServiceUpdateInstanceSettingHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedInstanceServiceHandler returns CodeUnimplemented from all methods. type UnimplementedInstanceServiceHandler struct{} func (UnimplementedInstanceServiceHandler) GetInstanceProfile(context.Context, *connect.Request[v1.GetInstanceProfileRequest]) (*connect.Response[v1.InstanceProfile], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.InstanceService.GetInstanceProfile is not implemented")) } func (UnimplementedInstanceServiceHandler) GetInstanceSetting(context.Context, *connect.Request[v1.GetInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.InstanceService.GetInstanceSetting is not implemented")) } func (UnimplementedInstanceServiceHandler) UpdateInstanceSetting(context.Context, *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.InstanceService.UpdateInstanceSetting is not implemented")) } ================================================ FILE: proto/gen/api/v1/apiv1connect/memo_service.connect.go ================================================ // Code generated by protoc-gen-connect-go. DO NOT EDIT. // // Source: api/v1/memo_service.proto package apiv1connect import ( connect "connectrpc.com/connect" context "context" errors "errors" v1 "github.com/usememos/memos/proto/gen/api/v1" emptypb "google.golang.org/protobuf/types/known/emptypb" http "net/http" strings "strings" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // MemoServiceName is the fully-qualified name of the MemoService service. MemoServiceName = "memos.api.v1.MemoService" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // MemoServiceCreateMemoProcedure is the fully-qualified name of the MemoService's CreateMemo RPC. MemoServiceCreateMemoProcedure = "/memos.api.v1.MemoService/CreateMemo" // MemoServiceListMemosProcedure is the fully-qualified name of the MemoService's ListMemos RPC. MemoServiceListMemosProcedure = "/memos.api.v1.MemoService/ListMemos" // MemoServiceGetMemoProcedure is the fully-qualified name of the MemoService's GetMemo RPC. MemoServiceGetMemoProcedure = "/memos.api.v1.MemoService/GetMemo" // MemoServiceUpdateMemoProcedure is the fully-qualified name of the MemoService's UpdateMemo RPC. MemoServiceUpdateMemoProcedure = "/memos.api.v1.MemoService/UpdateMemo" // MemoServiceDeleteMemoProcedure is the fully-qualified name of the MemoService's DeleteMemo RPC. MemoServiceDeleteMemoProcedure = "/memos.api.v1.MemoService/DeleteMemo" // MemoServiceSetMemoAttachmentsProcedure is the fully-qualified name of the MemoService's // SetMemoAttachments RPC. MemoServiceSetMemoAttachmentsProcedure = "/memos.api.v1.MemoService/SetMemoAttachments" // MemoServiceListMemoAttachmentsProcedure is the fully-qualified name of the MemoService's // ListMemoAttachments RPC. MemoServiceListMemoAttachmentsProcedure = "/memos.api.v1.MemoService/ListMemoAttachments" // MemoServiceSetMemoRelationsProcedure is the fully-qualified name of the MemoService's // SetMemoRelations RPC. MemoServiceSetMemoRelationsProcedure = "/memos.api.v1.MemoService/SetMemoRelations" // MemoServiceListMemoRelationsProcedure is the fully-qualified name of the MemoService's // ListMemoRelations RPC. MemoServiceListMemoRelationsProcedure = "/memos.api.v1.MemoService/ListMemoRelations" // MemoServiceCreateMemoCommentProcedure is the fully-qualified name of the MemoService's // CreateMemoComment RPC. MemoServiceCreateMemoCommentProcedure = "/memos.api.v1.MemoService/CreateMemoComment" // MemoServiceListMemoCommentsProcedure is the fully-qualified name of the MemoService's // ListMemoComments RPC. MemoServiceListMemoCommentsProcedure = "/memos.api.v1.MemoService/ListMemoComments" // MemoServiceListMemoReactionsProcedure is the fully-qualified name of the MemoService's // ListMemoReactions RPC. MemoServiceListMemoReactionsProcedure = "/memos.api.v1.MemoService/ListMemoReactions" // MemoServiceUpsertMemoReactionProcedure is the fully-qualified name of the MemoService's // UpsertMemoReaction RPC. MemoServiceUpsertMemoReactionProcedure = "/memos.api.v1.MemoService/UpsertMemoReaction" // MemoServiceDeleteMemoReactionProcedure is the fully-qualified name of the MemoService's // DeleteMemoReaction RPC. MemoServiceDeleteMemoReactionProcedure = "/memos.api.v1.MemoService/DeleteMemoReaction" // MemoServiceCreateMemoShareProcedure is the fully-qualified name of the MemoService's // CreateMemoShare RPC. MemoServiceCreateMemoShareProcedure = "/memos.api.v1.MemoService/CreateMemoShare" // MemoServiceListMemoSharesProcedure is the fully-qualified name of the MemoService's // ListMemoShares RPC. MemoServiceListMemoSharesProcedure = "/memos.api.v1.MemoService/ListMemoShares" // MemoServiceDeleteMemoShareProcedure is the fully-qualified name of the MemoService's // DeleteMemoShare RPC. MemoServiceDeleteMemoShareProcedure = "/memos.api.v1.MemoService/DeleteMemoShare" // MemoServiceGetMemoByShareProcedure is the fully-qualified name of the MemoService's // GetMemoByShare RPC. MemoServiceGetMemoByShareProcedure = "/memos.api.v1.MemoService/GetMemoByShare" ) // MemoServiceClient is a client for the memos.api.v1.MemoService service. type MemoServiceClient interface { // CreateMemo creates a memo. CreateMemo(context.Context, *connect.Request[v1.CreateMemoRequest]) (*connect.Response[v1.Memo], error) // ListMemos lists memos with pagination and filter. ListMemos(context.Context, *connect.Request[v1.ListMemosRequest]) (*connect.Response[v1.ListMemosResponse], error) // GetMemo gets a memo. GetMemo(context.Context, *connect.Request[v1.GetMemoRequest]) (*connect.Response[v1.Memo], error) // UpdateMemo updates a memo. UpdateMemo(context.Context, *connect.Request[v1.UpdateMemoRequest]) (*connect.Response[v1.Memo], error) // DeleteMemo deletes a memo. DeleteMemo(context.Context, *connect.Request[v1.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error) // SetMemoAttachments sets attachments for a memo. SetMemoAttachments(context.Context, *connect.Request[v1.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error) // ListMemoAttachments lists attachments for a memo. ListMemoAttachments(context.Context, *connect.Request[v1.ListMemoAttachmentsRequest]) (*connect.Response[v1.ListMemoAttachmentsResponse], error) // SetMemoRelations sets relations for a memo. SetMemoRelations(context.Context, *connect.Request[v1.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error) // ListMemoRelations lists relations for a memo. ListMemoRelations(context.Context, *connect.Request[v1.ListMemoRelationsRequest]) (*connect.Response[v1.ListMemoRelationsResponse], error) // CreateMemoComment creates a comment for a memo. CreateMemoComment(context.Context, *connect.Request[v1.CreateMemoCommentRequest]) (*connect.Response[v1.Memo], error) // ListMemoComments lists comments for a memo. ListMemoComments(context.Context, *connect.Request[v1.ListMemoCommentsRequest]) (*connect.Response[v1.ListMemoCommentsResponse], error) // ListMemoReactions lists reactions for a memo. ListMemoReactions(context.Context, *connect.Request[v1.ListMemoReactionsRequest]) (*connect.Response[v1.ListMemoReactionsResponse], error) // UpsertMemoReaction upserts a reaction for a memo. UpsertMemoReaction(context.Context, *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error) // DeleteMemoReaction deletes a reaction for a memo. DeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error) // CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator. CreateMemoShare(context.Context, *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error) // ListMemoShares lists all share links for a memo. Requires authentication as the memo creator. ListMemoShares(context.Context, *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error) // DeleteMemoShare revokes a share link. Requires authentication as the memo creator. DeleteMemoShare(context.Context, *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) // GetMemoByShare resolves a share token to its memo. No authentication required. // Returns NOT_FOUND if the token is invalid or expired. GetMemoByShare(context.Context, *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error) } // NewMemoServiceClient constructs a client for the memos.api.v1.MemoService service. By default, it // uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or // connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewMemoServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) MemoServiceClient { baseURL = strings.TrimRight(baseURL, "/") memoServiceMethods := v1.File_api_v1_memo_service_proto.Services().ByName("MemoService").Methods() return &memoServiceClient{ createMemo: connect.NewClient[v1.CreateMemoRequest, v1.Memo]( httpClient, baseURL+MemoServiceCreateMemoProcedure, connect.WithSchema(memoServiceMethods.ByName("CreateMemo")), connect.WithClientOptions(opts...), ), listMemos: connect.NewClient[v1.ListMemosRequest, v1.ListMemosResponse]( httpClient, baseURL+MemoServiceListMemosProcedure, connect.WithSchema(memoServiceMethods.ByName("ListMemos")), connect.WithClientOptions(opts...), ), getMemo: connect.NewClient[v1.GetMemoRequest, v1.Memo]( httpClient, baseURL+MemoServiceGetMemoProcedure, connect.WithSchema(memoServiceMethods.ByName("GetMemo")), connect.WithClientOptions(opts...), ), updateMemo: connect.NewClient[v1.UpdateMemoRequest, v1.Memo]( httpClient, baseURL+MemoServiceUpdateMemoProcedure, connect.WithSchema(memoServiceMethods.ByName("UpdateMemo")), connect.WithClientOptions(opts...), ), deleteMemo: connect.NewClient[v1.DeleteMemoRequest, emptypb.Empty]( httpClient, baseURL+MemoServiceDeleteMemoProcedure, connect.WithSchema(memoServiceMethods.ByName("DeleteMemo")), connect.WithClientOptions(opts...), ), setMemoAttachments: connect.NewClient[v1.SetMemoAttachmentsRequest, emptypb.Empty]( httpClient, baseURL+MemoServiceSetMemoAttachmentsProcedure, connect.WithSchema(memoServiceMethods.ByName("SetMemoAttachments")), connect.WithClientOptions(opts...), ), listMemoAttachments: connect.NewClient[v1.ListMemoAttachmentsRequest, v1.ListMemoAttachmentsResponse]( httpClient, baseURL+MemoServiceListMemoAttachmentsProcedure, connect.WithSchema(memoServiceMethods.ByName("ListMemoAttachments")), connect.WithClientOptions(opts...), ), setMemoRelations: connect.NewClient[v1.SetMemoRelationsRequest, emptypb.Empty]( httpClient, baseURL+MemoServiceSetMemoRelationsProcedure, connect.WithSchema(memoServiceMethods.ByName("SetMemoRelations")), connect.WithClientOptions(opts...), ), listMemoRelations: connect.NewClient[v1.ListMemoRelationsRequest, v1.ListMemoRelationsResponse]( httpClient, baseURL+MemoServiceListMemoRelationsProcedure, connect.WithSchema(memoServiceMethods.ByName("ListMemoRelations")), connect.WithClientOptions(opts...), ), createMemoComment: connect.NewClient[v1.CreateMemoCommentRequest, v1.Memo]( httpClient, baseURL+MemoServiceCreateMemoCommentProcedure, connect.WithSchema(memoServiceMethods.ByName("CreateMemoComment")), connect.WithClientOptions(opts...), ), listMemoComments: connect.NewClient[v1.ListMemoCommentsRequest, v1.ListMemoCommentsResponse]( httpClient, baseURL+MemoServiceListMemoCommentsProcedure, connect.WithSchema(memoServiceMethods.ByName("ListMemoComments")), connect.WithClientOptions(opts...), ), listMemoReactions: connect.NewClient[v1.ListMemoReactionsRequest, v1.ListMemoReactionsResponse]( httpClient, baseURL+MemoServiceListMemoReactionsProcedure, connect.WithSchema(memoServiceMethods.ByName("ListMemoReactions")), connect.WithClientOptions(opts...), ), upsertMemoReaction: connect.NewClient[v1.UpsertMemoReactionRequest, v1.Reaction]( httpClient, baseURL+MemoServiceUpsertMemoReactionProcedure, connect.WithSchema(memoServiceMethods.ByName("UpsertMemoReaction")), connect.WithClientOptions(opts...), ), deleteMemoReaction: connect.NewClient[v1.DeleteMemoReactionRequest, emptypb.Empty]( httpClient, baseURL+MemoServiceDeleteMemoReactionProcedure, connect.WithSchema(memoServiceMethods.ByName("DeleteMemoReaction")), connect.WithClientOptions(opts...), ), createMemoShare: connect.NewClient[v1.CreateMemoShareRequest, v1.MemoShare]( httpClient, baseURL+MemoServiceCreateMemoShareProcedure, connect.WithSchema(memoServiceMethods.ByName("CreateMemoShare")), connect.WithClientOptions(opts...), ), listMemoShares: connect.NewClient[v1.ListMemoSharesRequest, v1.ListMemoSharesResponse]( httpClient, baseURL+MemoServiceListMemoSharesProcedure, connect.WithSchema(memoServiceMethods.ByName("ListMemoShares")), connect.WithClientOptions(opts...), ), deleteMemoShare: connect.NewClient[v1.DeleteMemoShareRequest, emptypb.Empty]( httpClient, baseURL+MemoServiceDeleteMemoShareProcedure, connect.WithSchema(memoServiceMethods.ByName("DeleteMemoShare")), connect.WithClientOptions(opts...), ), getMemoByShare: connect.NewClient[v1.GetMemoByShareRequest, v1.Memo]( httpClient, baseURL+MemoServiceGetMemoByShareProcedure, connect.WithSchema(memoServiceMethods.ByName("GetMemoByShare")), connect.WithClientOptions(opts...), ), } } // memoServiceClient implements MemoServiceClient. type memoServiceClient struct { createMemo *connect.Client[v1.CreateMemoRequest, v1.Memo] listMemos *connect.Client[v1.ListMemosRequest, v1.ListMemosResponse] getMemo *connect.Client[v1.GetMemoRequest, v1.Memo] updateMemo *connect.Client[v1.UpdateMemoRequest, v1.Memo] deleteMemo *connect.Client[v1.DeleteMemoRequest, emptypb.Empty] setMemoAttachments *connect.Client[v1.SetMemoAttachmentsRequest, emptypb.Empty] listMemoAttachments *connect.Client[v1.ListMemoAttachmentsRequest, v1.ListMemoAttachmentsResponse] setMemoRelations *connect.Client[v1.SetMemoRelationsRequest, emptypb.Empty] listMemoRelations *connect.Client[v1.ListMemoRelationsRequest, v1.ListMemoRelationsResponse] createMemoComment *connect.Client[v1.CreateMemoCommentRequest, v1.Memo] listMemoComments *connect.Client[v1.ListMemoCommentsRequest, v1.ListMemoCommentsResponse] listMemoReactions *connect.Client[v1.ListMemoReactionsRequest, v1.ListMemoReactionsResponse] upsertMemoReaction *connect.Client[v1.UpsertMemoReactionRequest, v1.Reaction] deleteMemoReaction *connect.Client[v1.DeleteMemoReactionRequest, emptypb.Empty] createMemoShare *connect.Client[v1.CreateMemoShareRequest, v1.MemoShare] listMemoShares *connect.Client[v1.ListMemoSharesRequest, v1.ListMemoSharesResponse] deleteMemoShare *connect.Client[v1.DeleteMemoShareRequest, emptypb.Empty] getMemoByShare *connect.Client[v1.GetMemoByShareRequest, v1.Memo] } // CreateMemo calls memos.api.v1.MemoService.CreateMemo. func (c *memoServiceClient) CreateMemo(ctx context.Context, req *connect.Request[v1.CreateMemoRequest]) (*connect.Response[v1.Memo], error) { return c.createMemo.CallUnary(ctx, req) } // ListMemos calls memos.api.v1.MemoService.ListMemos. func (c *memoServiceClient) ListMemos(ctx context.Context, req *connect.Request[v1.ListMemosRequest]) (*connect.Response[v1.ListMemosResponse], error) { return c.listMemos.CallUnary(ctx, req) } // GetMemo calls memos.api.v1.MemoService.GetMemo. func (c *memoServiceClient) GetMemo(ctx context.Context, req *connect.Request[v1.GetMemoRequest]) (*connect.Response[v1.Memo], error) { return c.getMemo.CallUnary(ctx, req) } // UpdateMemo calls memos.api.v1.MemoService.UpdateMemo. func (c *memoServiceClient) UpdateMemo(ctx context.Context, req *connect.Request[v1.UpdateMemoRequest]) (*connect.Response[v1.Memo], error) { return c.updateMemo.CallUnary(ctx, req) } // DeleteMemo calls memos.api.v1.MemoService.DeleteMemo. func (c *memoServiceClient) DeleteMemo(ctx context.Context, req *connect.Request[v1.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error) { return c.deleteMemo.CallUnary(ctx, req) } // SetMemoAttachments calls memos.api.v1.MemoService.SetMemoAttachments. func (c *memoServiceClient) SetMemoAttachments(ctx context.Context, req *connect.Request[v1.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error) { return c.setMemoAttachments.CallUnary(ctx, req) } // ListMemoAttachments calls memos.api.v1.MemoService.ListMemoAttachments. func (c *memoServiceClient) ListMemoAttachments(ctx context.Context, req *connect.Request[v1.ListMemoAttachmentsRequest]) (*connect.Response[v1.ListMemoAttachmentsResponse], error) { return c.listMemoAttachments.CallUnary(ctx, req) } // SetMemoRelations calls memos.api.v1.MemoService.SetMemoRelations. func (c *memoServiceClient) SetMemoRelations(ctx context.Context, req *connect.Request[v1.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error) { return c.setMemoRelations.CallUnary(ctx, req) } // ListMemoRelations calls memos.api.v1.MemoService.ListMemoRelations. func (c *memoServiceClient) ListMemoRelations(ctx context.Context, req *connect.Request[v1.ListMemoRelationsRequest]) (*connect.Response[v1.ListMemoRelationsResponse], error) { return c.listMemoRelations.CallUnary(ctx, req) } // CreateMemoComment calls memos.api.v1.MemoService.CreateMemoComment. func (c *memoServiceClient) CreateMemoComment(ctx context.Context, req *connect.Request[v1.CreateMemoCommentRequest]) (*connect.Response[v1.Memo], error) { return c.createMemoComment.CallUnary(ctx, req) } // ListMemoComments calls memos.api.v1.MemoService.ListMemoComments. func (c *memoServiceClient) ListMemoComments(ctx context.Context, req *connect.Request[v1.ListMemoCommentsRequest]) (*connect.Response[v1.ListMemoCommentsResponse], error) { return c.listMemoComments.CallUnary(ctx, req) } // ListMemoReactions calls memos.api.v1.MemoService.ListMemoReactions. func (c *memoServiceClient) ListMemoReactions(ctx context.Context, req *connect.Request[v1.ListMemoReactionsRequest]) (*connect.Response[v1.ListMemoReactionsResponse], error) { return c.listMemoReactions.CallUnary(ctx, req) } // UpsertMemoReaction calls memos.api.v1.MemoService.UpsertMemoReaction. func (c *memoServiceClient) UpsertMemoReaction(ctx context.Context, req *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error) { return c.upsertMemoReaction.CallUnary(ctx, req) } // DeleteMemoReaction calls memos.api.v1.MemoService.DeleteMemoReaction. func (c *memoServiceClient) DeleteMemoReaction(ctx context.Context, req *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error) { return c.deleteMemoReaction.CallUnary(ctx, req) } // CreateMemoShare calls memos.api.v1.MemoService.CreateMemoShare. func (c *memoServiceClient) CreateMemoShare(ctx context.Context, req *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error) { return c.createMemoShare.CallUnary(ctx, req) } // ListMemoShares calls memos.api.v1.MemoService.ListMemoShares. func (c *memoServiceClient) ListMemoShares(ctx context.Context, req *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error) { return c.listMemoShares.CallUnary(ctx, req) } // DeleteMemoShare calls memos.api.v1.MemoService.DeleteMemoShare. func (c *memoServiceClient) DeleteMemoShare(ctx context.Context, req *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) { return c.deleteMemoShare.CallUnary(ctx, req) } // GetMemoByShare calls memos.api.v1.MemoService.GetMemoByShare. func (c *memoServiceClient) GetMemoByShare(ctx context.Context, req *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error) { return c.getMemoByShare.CallUnary(ctx, req) } // MemoServiceHandler is an implementation of the memos.api.v1.MemoService service. type MemoServiceHandler interface { // CreateMemo creates a memo. CreateMemo(context.Context, *connect.Request[v1.CreateMemoRequest]) (*connect.Response[v1.Memo], error) // ListMemos lists memos with pagination and filter. ListMemos(context.Context, *connect.Request[v1.ListMemosRequest]) (*connect.Response[v1.ListMemosResponse], error) // GetMemo gets a memo. GetMemo(context.Context, *connect.Request[v1.GetMemoRequest]) (*connect.Response[v1.Memo], error) // UpdateMemo updates a memo. UpdateMemo(context.Context, *connect.Request[v1.UpdateMemoRequest]) (*connect.Response[v1.Memo], error) // DeleteMemo deletes a memo. DeleteMemo(context.Context, *connect.Request[v1.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error) // SetMemoAttachments sets attachments for a memo. SetMemoAttachments(context.Context, *connect.Request[v1.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error) // ListMemoAttachments lists attachments for a memo. ListMemoAttachments(context.Context, *connect.Request[v1.ListMemoAttachmentsRequest]) (*connect.Response[v1.ListMemoAttachmentsResponse], error) // SetMemoRelations sets relations for a memo. SetMemoRelations(context.Context, *connect.Request[v1.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error) // ListMemoRelations lists relations for a memo. ListMemoRelations(context.Context, *connect.Request[v1.ListMemoRelationsRequest]) (*connect.Response[v1.ListMemoRelationsResponse], error) // CreateMemoComment creates a comment for a memo. CreateMemoComment(context.Context, *connect.Request[v1.CreateMemoCommentRequest]) (*connect.Response[v1.Memo], error) // ListMemoComments lists comments for a memo. ListMemoComments(context.Context, *connect.Request[v1.ListMemoCommentsRequest]) (*connect.Response[v1.ListMemoCommentsResponse], error) // ListMemoReactions lists reactions for a memo. ListMemoReactions(context.Context, *connect.Request[v1.ListMemoReactionsRequest]) (*connect.Response[v1.ListMemoReactionsResponse], error) // UpsertMemoReaction upserts a reaction for a memo. UpsertMemoReaction(context.Context, *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error) // DeleteMemoReaction deletes a reaction for a memo. DeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error) // CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator. CreateMemoShare(context.Context, *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error) // ListMemoShares lists all share links for a memo. Requires authentication as the memo creator. ListMemoShares(context.Context, *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error) // DeleteMemoShare revokes a share link. Requires authentication as the memo creator. DeleteMemoShare(context.Context, *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) // GetMemoByShare resolves a share token to its memo. No authentication required. // Returns NOT_FOUND if the token is invalid or expired. GetMemoByShare(context.Context, *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error) } // NewMemoServiceHandler builds an HTTP handler from the service implementation. It returns the path // on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewMemoServiceHandler(svc MemoServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { memoServiceMethods := v1.File_api_v1_memo_service_proto.Services().ByName("MemoService").Methods() memoServiceCreateMemoHandler := connect.NewUnaryHandler( MemoServiceCreateMemoProcedure, svc.CreateMemo, connect.WithSchema(memoServiceMethods.ByName("CreateMemo")), connect.WithHandlerOptions(opts...), ) memoServiceListMemosHandler := connect.NewUnaryHandler( MemoServiceListMemosProcedure, svc.ListMemos, connect.WithSchema(memoServiceMethods.ByName("ListMemos")), connect.WithHandlerOptions(opts...), ) memoServiceGetMemoHandler := connect.NewUnaryHandler( MemoServiceGetMemoProcedure, svc.GetMemo, connect.WithSchema(memoServiceMethods.ByName("GetMemo")), connect.WithHandlerOptions(opts...), ) memoServiceUpdateMemoHandler := connect.NewUnaryHandler( MemoServiceUpdateMemoProcedure, svc.UpdateMemo, connect.WithSchema(memoServiceMethods.ByName("UpdateMemo")), connect.WithHandlerOptions(opts...), ) memoServiceDeleteMemoHandler := connect.NewUnaryHandler( MemoServiceDeleteMemoProcedure, svc.DeleteMemo, connect.WithSchema(memoServiceMethods.ByName("DeleteMemo")), connect.WithHandlerOptions(opts...), ) memoServiceSetMemoAttachmentsHandler := connect.NewUnaryHandler( MemoServiceSetMemoAttachmentsProcedure, svc.SetMemoAttachments, connect.WithSchema(memoServiceMethods.ByName("SetMemoAttachments")), connect.WithHandlerOptions(opts...), ) memoServiceListMemoAttachmentsHandler := connect.NewUnaryHandler( MemoServiceListMemoAttachmentsProcedure, svc.ListMemoAttachments, connect.WithSchema(memoServiceMethods.ByName("ListMemoAttachments")), connect.WithHandlerOptions(opts...), ) memoServiceSetMemoRelationsHandler := connect.NewUnaryHandler( MemoServiceSetMemoRelationsProcedure, svc.SetMemoRelations, connect.WithSchema(memoServiceMethods.ByName("SetMemoRelations")), connect.WithHandlerOptions(opts...), ) memoServiceListMemoRelationsHandler := connect.NewUnaryHandler( MemoServiceListMemoRelationsProcedure, svc.ListMemoRelations, connect.WithSchema(memoServiceMethods.ByName("ListMemoRelations")), connect.WithHandlerOptions(opts...), ) memoServiceCreateMemoCommentHandler := connect.NewUnaryHandler( MemoServiceCreateMemoCommentProcedure, svc.CreateMemoComment, connect.WithSchema(memoServiceMethods.ByName("CreateMemoComment")), connect.WithHandlerOptions(opts...), ) memoServiceListMemoCommentsHandler := connect.NewUnaryHandler( MemoServiceListMemoCommentsProcedure, svc.ListMemoComments, connect.WithSchema(memoServiceMethods.ByName("ListMemoComments")), connect.WithHandlerOptions(opts...), ) memoServiceListMemoReactionsHandler := connect.NewUnaryHandler( MemoServiceListMemoReactionsProcedure, svc.ListMemoReactions, connect.WithSchema(memoServiceMethods.ByName("ListMemoReactions")), connect.WithHandlerOptions(opts...), ) memoServiceUpsertMemoReactionHandler := connect.NewUnaryHandler( MemoServiceUpsertMemoReactionProcedure, svc.UpsertMemoReaction, connect.WithSchema(memoServiceMethods.ByName("UpsertMemoReaction")), connect.WithHandlerOptions(opts...), ) memoServiceDeleteMemoReactionHandler := connect.NewUnaryHandler( MemoServiceDeleteMemoReactionProcedure, svc.DeleteMemoReaction, connect.WithSchema(memoServiceMethods.ByName("DeleteMemoReaction")), connect.WithHandlerOptions(opts...), ) memoServiceCreateMemoShareHandler := connect.NewUnaryHandler( MemoServiceCreateMemoShareProcedure, svc.CreateMemoShare, connect.WithSchema(memoServiceMethods.ByName("CreateMemoShare")), connect.WithHandlerOptions(opts...), ) memoServiceListMemoSharesHandler := connect.NewUnaryHandler( MemoServiceListMemoSharesProcedure, svc.ListMemoShares, connect.WithSchema(memoServiceMethods.ByName("ListMemoShares")), connect.WithHandlerOptions(opts...), ) memoServiceDeleteMemoShareHandler := connect.NewUnaryHandler( MemoServiceDeleteMemoShareProcedure, svc.DeleteMemoShare, connect.WithSchema(memoServiceMethods.ByName("DeleteMemoShare")), connect.WithHandlerOptions(opts...), ) memoServiceGetMemoByShareHandler := connect.NewUnaryHandler( MemoServiceGetMemoByShareProcedure, svc.GetMemoByShare, connect.WithSchema(memoServiceMethods.ByName("GetMemoByShare")), connect.WithHandlerOptions(opts...), ) return "/memos.api.v1.MemoService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case MemoServiceCreateMemoProcedure: memoServiceCreateMemoHandler.ServeHTTP(w, r) case MemoServiceListMemosProcedure: memoServiceListMemosHandler.ServeHTTP(w, r) case MemoServiceGetMemoProcedure: memoServiceGetMemoHandler.ServeHTTP(w, r) case MemoServiceUpdateMemoProcedure: memoServiceUpdateMemoHandler.ServeHTTP(w, r) case MemoServiceDeleteMemoProcedure: memoServiceDeleteMemoHandler.ServeHTTP(w, r) case MemoServiceSetMemoAttachmentsProcedure: memoServiceSetMemoAttachmentsHandler.ServeHTTP(w, r) case MemoServiceListMemoAttachmentsProcedure: memoServiceListMemoAttachmentsHandler.ServeHTTP(w, r) case MemoServiceSetMemoRelationsProcedure: memoServiceSetMemoRelationsHandler.ServeHTTP(w, r) case MemoServiceListMemoRelationsProcedure: memoServiceListMemoRelationsHandler.ServeHTTP(w, r) case MemoServiceCreateMemoCommentProcedure: memoServiceCreateMemoCommentHandler.ServeHTTP(w, r) case MemoServiceListMemoCommentsProcedure: memoServiceListMemoCommentsHandler.ServeHTTP(w, r) case MemoServiceListMemoReactionsProcedure: memoServiceListMemoReactionsHandler.ServeHTTP(w, r) case MemoServiceUpsertMemoReactionProcedure: memoServiceUpsertMemoReactionHandler.ServeHTTP(w, r) case MemoServiceDeleteMemoReactionProcedure: memoServiceDeleteMemoReactionHandler.ServeHTTP(w, r) case MemoServiceCreateMemoShareProcedure: memoServiceCreateMemoShareHandler.ServeHTTP(w, r) case MemoServiceListMemoSharesProcedure: memoServiceListMemoSharesHandler.ServeHTTP(w, r) case MemoServiceDeleteMemoShareProcedure: memoServiceDeleteMemoShareHandler.ServeHTTP(w, r) case MemoServiceGetMemoByShareProcedure: memoServiceGetMemoByShareHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedMemoServiceHandler returns CodeUnimplemented from all methods. type UnimplementedMemoServiceHandler struct{} func (UnimplementedMemoServiceHandler) CreateMemo(context.Context, *connect.Request[v1.CreateMemoRequest]) (*connect.Response[v1.Memo], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.CreateMemo is not implemented")) } func (UnimplementedMemoServiceHandler) ListMemos(context.Context, *connect.Request[v1.ListMemosRequest]) (*connect.Response[v1.ListMemosResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.ListMemos is not implemented")) } func (UnimplementedMemoServiceHandler) GetMemo(context.Context, *connect.Request[v1.GetMemoRequest]) (*connect.Response[v1.Memo], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.GetMemo is not implemented")) } func (UnimplementedMemoServiceHandler) UpdateMemo(context.Context, *connect.Request[v1.UpdateMemoRequest]) (*connect.Response[v1.Memo], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.UpdateMemo is not implemented")) } func (UnimplementedMemoServiceHandler) DeleteMemo(context.Context, *connect.Request[v1.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.DeleteMemo is not implemented")) } func (UnimplementedMemoServiceHandler) SetMemoAttachments(context.Context, *connect.Request[v1.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.SetMemoAttachments is not implemented")) } func (UnimplementedMemoServiceHandler) ListMemoAttachments(context.Context, *connect.Request[v1.ListMemoAttachmentsRequest]) (*connect.Response[v1.ListMemoAttachmentsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.ListMemoAttachments is not implemented")) } func (UnimplementedMemoServiceHandler) SetMemoRelations(context.Context, *connect.Request[v1.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.SetMemoRelations is not implemented")) } func (UnimplementedMemoServiceHandler) ListMemoRelations(context.Context, *connect.Request[v1.ListMemoRelationsRequest]) (*connect.Response[v1.ListMemoRelationsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.ListMemoRelations is not implemented")) } func (UnimplementedMemoServiceHandler) CreateMemoComment(context.Context, *connect.Request[v1.CreateMemoCommentRequest]) (*connect.Response[v1.Memo], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.CreateMemoComment is not implemented")) } func (UnimplementedMemoServiceHandler) ListMemoComments(context.Context, *connect.Request[v1.ListMemoCommentsRequest]) (*connect.Response[v1.ListMemoCommentsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.ListMemoComments is not implemented")) } func (UnimplementedMemoServiceHandler) ListMemoReactions(context.Context, *connect.Request[v1.ListMemoReactionsRequest]) (*connect.Response[v1.ListMemoReactionsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.ListMemoReactions is not implemented")) } func (UnimplementedMemoServiceHandler) UpsertMemoReaction(context.Context, *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.UpsertMemoReaction is not implemented")) } func (UnimplementedMemoServiceHandler) DeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.DeleteMemoReaction is not implemented")) } func (UnimplementedMemoServiceHandler) CreateMemoShare(context.Context, *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.CreateMemoShare is not implemented")) } func (UnimplementedMemoServiceHandler) ListMemoShares(context.Context, *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.ListMemoShares is not implemented")) } func (UnimplementedMemoServiceHandler) DeleteMemoShare(context.Context, *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.DeleteMemoShare is not implemented")) } func (UnimplementedMemoServiceHandler) GetMemoByShare(context.Context, *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.GetMemoByShare is not implemented")) } ================================================ FILE: proto/gen/api/v1/apiv1connect/shortcut_service.connect.go ================================================ // Code generated by protoc-gen-connect-go. DO NOT EDIT. // // Source: api/v1/shortcut_service.proto package apiv1connect import ( connect "connectrpc.com/connect" context "context" errors "errors" v1 "github.com/usememos/memos/proto/gen/api/v1" emptypb "google.golang.org/protobuf/types/known/emptypb" http "net/http" strings "strings" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // ShortcutServiceName is the fully-qualified name of the ShortcutService service. ShortcutServiceName = "memos.api.v1.ShortcutService" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // ShortcutServiceListShortcutsProcedure is the fully-qualified name of the ShortcutService's // ListShortcuts RPC. ShortcutServiceListShortcutsProcedure = "/memos.api.v1.ShortcutService/ListShortcuts" // ShortcutServiceGetShortcutProcedure is the fully-qualified name of the ShortcutService's // GetShortcut RPC. ShortcutServiceGetShortcutProcedure = "/memos.api.v1.ShortcutService/GetShortcut" // ShortcutServiceCreateShortcutProcedure is the fully-qualified name of the ShortcutService's // CreateShortcut RPC. ShortcutServiceCreateShortcutProcedure = "/memos.api.v1.ShortcutService/CreateShortcut" // ShortcutServiceUpdateShortcutProcedure is the fully-qualified name of the ShortcutService's // UpdateShortcut RPC. ShortcutServiceUpdateShortcutProcedure = "/memos.api.v1.ShortcutService/UpdateShortcut" // ShortcutServiceDeleteShortcutProcedure is the fully-qualified name of the ShortcutService's // DeleteShortcut RPC. ShortcutServiceDeleteShortcutProcedure = "/memos.api.v1.ShortcutService/DeleteShortcut" ) // ShortcutServiceClient is a client for the memos.api.v1.ShortcutService service. type ShortcutServiceClient interface { // ListShortcuts returns a list of shortcuts for a user. ListShortcuts(context.Context, *connect.Request[v1.ListShortcutsRequest]) (*connect.Response[v1.ListShortcutsResponse], error) // GetShortcut gets a shortcut by name. GetShortcut(context.Context, *connect.Request[v1.GetShortcutRequest]) (*connect.Response[v1.Shortcut], error) // CreateShortcut creates a new shortcut for a user. CreateShortcut(context.Context, *connect.Request[v1.CreateShortcutRequest]) (*connect.Response[v1.Shortcut], error) // UpdateShortcut updates a shortcut for a user. UpdateShortcut(context.Context, *connect.Request[v1.UpdateShortcutRequest]) (*connect.Response[v1.Shortcut], error) // DeleteShortcut deletes a shortcut for a user. DeleteShortcut(context.Context, *connect.Request[v1.DeleteShortcutRequest]) (*connect.Response[emptypb.Empty], error) } // NewShortcutServiceClient constructs a client for the memos.api.v1.ShortcutService service. By // default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, // and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the // connect.WithGRPC() or connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewShortcutServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ShortcutServiceClient { baseURL = strings.TrimRight(baseURL, "/") shortcutServiceMethods := v1.File_api_v1_shortcut_service_proto.Services().ByName("ShortcutService").Methods() return &shortcutServiceClient{ listShortcuts: connect.NewClient[v1.ListShortcutsRequest, v1.ListShortcutsResponse]( httpClient, baseURL+ShortcutServiceListShortcutsProcedure, connect.WithSchema(shortcutServiceMethods.ByName("ListShortcuts")), connect.WithClientOptions(opts...), ), getShortcut: connect.NewClient[v1.GetShortcutRequest, v1.Shortcut]( httpClient, baseURL+ShortcutServiceGetShortcutProcedure, connect.WithSchema(shortcutServiceMethods.ByName("GetShortcut")), connect.WithClientOptions(opts...), ), createShortcut: connect.NewClient[v1.CreateShortcutRequest, v1.Shortcut]( httpClient, baseURL+ShortcutServiceCreateShortcutProcedure, connect.WithSchema(shortcutServiceMethods.ByName("CreateShortcut")), connect.WithClientOptions(opts...), ), updateShortcut: connect.NewClient[v1.UpdateShortcutRequest, v1.Shortcut]( httpClient, baseURL+ShortcutServiceUpdateShortcutProcedure, connect.WithSchema(shortcutServiceMethods.ByName("UpdateShortcut")), connect.WithClientOptions(opts...), ), deleteShortcut: connect.NewClient[v1.DeleteShortcutRequest, emptypb.Empty]( httpClient, baseURL+ShortcutServiceDeleteShortcutProcedure, connect.WithSchema(shortcutServiceMethods.ByName("DeleteShortcut")), connect.WithClientOptions(opts...), ), } } // shortcutServiceClient implements ShortcutServiceClient. type shortcutServiceClient struct { listShortcuts *connect.Client[v1.ListShortcutsRequest, v1.ListShortcutsResponse] getShortcut *connect.Client[v1.GetShortcutRequest, v1.Shortcut] createShortcut *connect.Client[v1.CreateShortcutRequest, v1.Shortcut] updateShortcut *connect.Client[v1.UpdateShortcutRequest, v1.Shortcut] deleteShortcut *connect.Client[v1.DeleteShortcutRequest, emptypb.Empty] } // ListShortcuts calls memos.api.v1.ShortcutService.ListShortcuts. func (c *shortcutServiceClient) ListShortcuts(ctx context.Context, req *connect.Request[v1.ListShortcutsRequest]) (*connect.Response[v1.ListShortcutsResponse], error) { return c.listShortcuts.CallUnary(ctx, req) } // GetShortcut calls memos.api.v1.ShortcutService.GetShortcut. func (c *shortcutServiceClient) GetShortcut(ctx context.Context, req *connect.Request[v1.GetShortcutRequest]) (*connect.Response[v1.Shortcut], error) { return c.getShortcut.CallUnary(ctx, req) } // CreateShortcut calls memos.api.v1.ShortcutService.CreateShortcut. func (c *shortcutServiceClient) CreateShortcut(ctx context.Context, req *connect.Request[v1.CreateShortcutRequest]) (*connect.Response[v1.Shortcut], error) { return c.createShortcut.CallUnary(ctx, req) } // UpdateShortcut calls memos.api.v1.ShortcutService.UpdateShortcut. func (c *shortcutServiceClient) UpdateShortcut(ctx context.Context, req *connect.Request[v1.UpdateShortcutRequest]) (*connect.Response[v1.Shortcut], error) { return c.updateShortcut.CallUnary(ctx, req) } // DeleteShortcut calls memos.api.v1.ShortcutService.DeleteShortcut. func (c *shortcutServiceClient) DeleteShortcut(ctx context.Context, req *connect.Request[v1.DeleteShortcutRequest]) (*connect.Response[emptypb.Empty], error) { return c.deleteShortcut.CallUnary(ctx, req) } // ShortcutServiceHandler is an implementation of the memos.api.v1.ShortcutService service. type ShortcutServiceHandler interface { // ListShortcuts returns a list of shortcuts for a user. ListShortcuts(context.Context, *connect.Request[v1.ListShortcutsRequest]) (*connect.Response[v1.ListShortcutsResponse], error) // GetShortcut gets a shortcut by name. GetShortcut(context.Context, *connect.Request[v1.GetShortcutRequest]) (*connect.Response[v1.Shortcut], error) // CreateShortcut creates a new shortcut for a user. CreateShortcut(context.Context, *connect.Request[v1.CreateShortcutRequest]) (*connect.Response[v1.Shortcut], error) // UpdateShortcut updates a shortcut for a user. UpdateShortcut(context.Context, *connect.Request[v1.UpdateShortcutRequest]) (*connect.Response[v1.Shortcut], error) // DeleteShortcut deletes a shortcut for a user. DeleteShortcut(context.Context, *connect.Request[v1.DeleteShortcutRequest]) (*connect.Response[emptypb.Empty], error) } // NewShortcutServiceHandler builds an HTTP handler from the service implementation. It returns the // path on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewShortcutServiceHandler(svc ShortcutServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { shortcutServiceMethods := v1.File_api_v1_shortcut_service_proto.Services().ByName("ShortcutService").Methods() shortcutServiceListShortcutsHandler := connect.NewUnaryHandler( ShortcutServiceListShortcutsProcedure, svc.ListShortcuts, connect.WithSchema(shortcutServiceMethods.ByName("ListShortcuts")), connect.WithHandlerOptions(opts...), ) shortcutServiceGetShortcutHandler := connect.NewUnaryHandler( ShortcutServiceGetShortcutProcedure, svc.GetShortcut, connect.WithSchema(shortcutServiceMethods.ByName("GetShortcut")), connect.WithHandlerOptions(opts...), ) shortcutServiceCreateShortcutHandler := connect.NewUnaryHandler( ShortcutServiceCreateShortcutProcedure, svc.CreateShortcut, connect.WithSchema(shortcutServiceMethods.ByName("CreateShortcut")), connect.WithHandlerOptions(opts...), ) shortcutServiceUpdateShortcutHandler := connect.NewUnaryHandler( ShortcutServiceUpdateShortcutProcedure, svc.UpdateShortcut, connect.WithSchema(shortcutServiceMethods.ByName("UpdateShortcut")), connect.WithHandlerOptions(opts...), ) shortcutServiceDeleteShortcutHandler := connect.NewUnaryHandler( ShortcutServiceDeleteShortcutProcedure, svc.DeleteShortcut, connect.WithSchema(shortcutServiceMethods.ByName("DeleteShortcut")), connect.WithHandlerOptions(opts...), ) return "/memos.api.v1.ShortcutService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case ShortcutServiceListShortcutsProcedure: shortcutServiceListShortcutsHandler.ServeHTTP(w, r) case ShortcutServiceGetShortcutProcedure: shortcutServiceGetShortcutHandler.ServeHTTP(w, r) case ShortcutServiceCreateShortcutProcedure: shortcutServiceCreateShortcutHandler.ServeHTTP(w, r) case ShortcutServiceUpdateShortcutProcedure: shortcutServiceUpdateShortcutHandler.ServeHTTP(w, r) case ShortcutServiceDeleteShortcutProcedure: shortcutServiceDeleteShortcutHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedShortcutServiceHandler returns CodeUnimplemented from all methods. type UnimplementedShortcutServiceHandler struct{} func (UnimplementedShortcutServiceHandler) ListShortcuts(context.Context, *connect.Request[v1.ListShortcutsRequest]) (*connect.Response[v1.ListShortcutsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.ShortcutService.ListShortcuts is not implemented")) } func (UnimplementedShortcutServiceHandler) GetShortcut(context.Context, *connect.Request[v1.GetShortcutRequest]) (*connect.Response[v1.Shortcut], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.ShortcutService.GetShortcut is not implemented")) } func (UnimplementedShortcutServiceHandler) CreateShortcut(context.Context, *connect.Request[v1.CreateShortcutRequest]) (*connect.Response[v1.Shortcut], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.ShortcutService.CreateShortcut is not implemented")) } func (UnimplementedShortcutServiceHandler) UpdateShortcut(context.Context, *connect.Request[v1.UpdateShortcutRequest]) (*connect.Response[v1.Shortcut], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.ShortcutService.UpdateShortcut is not implemented")) } func (UnimplementedShortcutServiceHandler) DeleteShortcut(context.Context, *connect.Request[v1.DeleteShortcutRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.ShortcutService.DeleteShortcut is not implemented")) } ================================================ FILE: proto/gen/api/v1/apiv1connect/user_service.connect.go ================================================ // Code generated by protoc-gen-connect-go. DO NOT EDIT. // // Source: api/v1/user_service.proto package apiv1connect import ( connect "connectrpc.com/connect" context "context" errors "errors" v1 "github.com/usememos/memos/proto/gen/api/v1" emptypb "google.golang.org/protobuf/types/known/emptypb" http "net/http" strings "strings" ) // This is a compile-time assertion to ensure that this generated file and the connect package are // compatible. If you get a compiler error that this constant is not defined, this code was // generated with a version of connect newer than the one compiled into your binary. You can fix the // problem by either regenerating this code with an older version of connect or updating the connect // version compiled into your binary. const _ = connect.IsAtLeastVersion1_13_0 const ( // UserServiceName is the fully-qualified name of the UserService service. UserServiceName = "memos.api.v1.UserService" ) // These constants are the fully-qualified names of the RPCs defined in this package. They're // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. // // Note that these are different from the fully-qualified method names used by // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( // UserServiceListUsersProcedure is the fully-qualified name of the UserService's ListUsers RPC. UserServiceListUsersProcedure = "/memos.api.v1.UserService/ListUsers" // UserServiceGetUserProcedure is the fully-qualified name of the UserService's GetUser RPC. UserServiceGetUserProcedure = "/memos.api.v1.UserService/GetUser" // UserServiceCreateUserProcedure is the fully-qualified name of the UserService's CreateUser RPC. UserServiceCreateUserProcedure = "/memos.api.v1.UserService/CreateUser" // UserServiceUpdateUserProcedure is the fully-qualified name of the UserService's UpdateUser RPC. UserServiceUpdateUserProcedure = "/memos.api.v1.UserService/UpdateUser" // UserServiceDeleteUserProcedure is the fully-qualified name of the UserService's DeleteUser RPC. UserServiceDeleteUserProcedure = "/memos.api.v1.UserService/DeleteUser" // UserServiceListAllUserStatsProcedure is the fully-qualified name of the UserService's // ListAllUserStats RPC. UserServiceListAllUserStatsProcedure = "/memos.api.v1.UserService/ListAllUserStats" // UserServiceGetUserStatsProcedure is the fully-qualified name of the UserService's GetUserStats // RPC. UserServiceGetUserStatsProcedure = "/memos.api.v1.UserService/GetUserStats" // UserServiceGetUserSettingProcedure is the fully-qualified name of the UserService's // GetUserSetting RPC. UserServiceGetUserSettingProcedure = "/memos.api.v1.UserService/GetUserSetting" // UserServiceUpdateUserSettingProcedure is the fully-qualified name of the UserService's // UpdateUserSetting RPC. UserServiceUpdateUserSettingProcedure = "/memos.api.v1.UserService/UpdateUserSetting" // UserServiceListUserSettingsProcedure is the fully-qualified name of the UserService's // ListUserSettings RPC. UserServiceListUserSettingsProcedure = "/memos.api.v1.UserService/ListUserSettings" // UserServiceListPersonalAccessTokensProcedure is the fully-qualified name of the UserService's // ListPersonalAccessTokens RPC. UserServiceListPersonalAccessTokensProcedure = "/memos.api.v1.UserService/ListPersonalAccessTokens" // UserServiceCreatePersonalAccessTokenProcedure is the fully-qualified name of the UserService's // CreatePersonalAccessToken RPC. UserServiceCreatePersonalAccessTokenProcedure = "/memos.api.v1.UserService/CreatePersonalAccessToken" // UserServiceDeletePersonalAccessTokenProcedure is the fully-qualified name of the UserService's // DeletePersonalAccessToken RPC. UserServiceDeletePersonalAccessTokenProcedure = "/memos.api.v1.UserService/DeletePersonalAccessToken" // UserServiceListUserWebhooksProcedure is the fully-qualified name of the UserService's // ListUserWebhooks RPC. UserServiceListUserWebhooksProcedure = "/memos.api.v1.UserService/ListUserWebhooks" // UserServiceCreateUserWebhookProcedure is the fully-qualified name of the UserService's // CreateUserWebhook RPC. UserServiceCreateUserWebhookProcedure = "/memos.api.v1.UserService/CreateUserWebhook" // UserServiceUpdateUserWebhookProcedure is the fully-qualified name of the UserService's // UpdateUserWebhook RPC. UserServiceUpdateUserWebhookProcedure = "/memos.api.v1.UserService/UpdateUserWebhook" // UserServiceDeleteUserWebhookProcedure is the fully-qualified name of the UserService's // DeleteUserWebhook RPC. UserServiceDeleteUserWebhookProcedure = "/memos.api.v1.UserService/DeleteUserWebhook" // UserServiceListUserNotificationsProcedure is the fully-qualified name of the UserService's // ListUserNotifications RPC. UserServiceListUserNotificationsProcedure = "/memos.api.v1.UserService/ListUserNotifications" // UserServiceUpdateUserNotificationProcedure is the fully-qualified name of the UserService's // UpdateUserNotification RPC. UserServiceUpdateUserNotificationProcedure = "/memos.api.v1.UserService/UpdateUserNotification" // UserServiceDeleteUserNotificationProcedure is the fully-qualified name of the UserService's // DeleteUserNotification RPC. UserServiceDeleteUserNotificationProcedure = "/memos.api.v1.UserService/DeleteUserNotification" ) // UserServiceClient is a client for the memos.api.v1.UserService service. type UserServiceClient interface { // ListUsers returns a list of users. ListUsers(context.Context, *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error) // GetUser gets a user by ID or username. // Supports both numeric IDs and username strings: // - users/{id} (e.g., users/101) // - users/{username} (e.g., users/steven) GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error) // CreateUser creates a new user. CreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.User], error) // UpdateUser updates a user. UpdateUser(context.Context, *connect.Request[v1.UpdateUserRequest]) (*connect.Response[v1.User], error) // DeleteUser deletes a user. DeleteUser(context.Context, *connect.Request[v1.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) // ListAllUserStats returns statistics for all users. ListAllUserStats(context.Context, *connect.Request[v1.ListAllUserStatsRequest]) (*connect.Response[v1.ListAllUserStatsResponse], error) // GetUserStats returns statistics for a specific user. GetUserStats(context.Context, *connect.Request[v1.GetUserStatsRequest]) (*connect.Response[v1.UserStats], error) // GetUserSetting returns the user setting. GetUserSetting(context.Context, *connect.Request[v1.GetUserSettingRequest]) (*connect.Response[v1.UserSetting], error) // UpdateUserSetting updates the user setting. UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error) // ListUserSettings returns a list of user settings. ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error) // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) // CreatePersonalAccessToken creates a new Personal Access Token for a user. // The token value is only returned once upon creation. CreatePersonalAccessToken(context.Context, *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error) // DeletePersonalAccessToken deletes a Personal Access Token. DeletePersonalAccessToken(context.Context, *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) // ListUserWebhooks returns a list of webhooks for a user. ListUserWebhooks(context.Context, *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error) // CreateUserWebhook creates a new webhook for a user. CreateUserWebhook(context.Context, *connect.Request[v1.CreateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) // UpdateUserWebhook updates an existing webhook for a user. UpdateUserWebhook(context.Context, *connect.Request[v1.UpdateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) // DeleteUserWebhook deletes a webhook for a user. DeleteUserWebhook(context.Context, *connect.Request[v1.DeleteUserWebhookRequest]) (*connect.Response[emptypb.Empty], error) // ListUserNotifications lists notifications for a user. ListUserNotifications(context.Context, *connect.Request[v1.ListUserNotificationsRequest]) (*connect.Response[v1.ListUserNotificationsResponse], error) // UpdateUserNotification updates a notification. UpdateUserNotification(context.Context, *connect.Request[v1.UpdateUserNotificationRequest]) (*connect.Response[v1.UserNotification], error) // DeleteUserNotification deletes a notification. DeleteUserNotification(context.Context, *connect.Request[v1.DeleteUserNotificationRequest]) (*connect.Response[emptypb.Empty], error) } // NewUserServiceClient constructs a client for the memos.api.v1.UserService service. By default, it // uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or // connect.WithGRPCWeb() options. // // The URL supplied here should be the base URL for the Connect or gRPC server (for example, // http://api.acme.com or https://acme.com/grpc). func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) UserServiceClient { baseURL = strings.TrimRight(baseURL, "/") userServiceMethods := v1.File_api_v1_user_service_proto.Services().ByName("UserService").Methods() return &userServiceClient{ listUsers: connect.NewClient[v1.ListUsersRequest, v1.ListUsersResponse]( httpClient, baseURL+UserServiceListUsersProcedure, connect.WithSchema(userServiceMethods.ByName("ListUsers")), connect.WithClientOptions(opts...), ), getUser: connect.NewClient[v1.GetUserRequest, v1.User]( httpClient, baseURL+UserServiceGetUserProcedure, connect.WithSchema(userServiceMethods.ByName("GetUser")), connect.WithClientOptions(opts...), ), createUser: connect.NewClient[v1.CreateUserRequest, v1.User]( httpClient, baseURL+UserServiceCreateUserProcedure, connect.WithSchema(userServiceMethods.ByName("CreateUser")), connect.WithClientOptions(opts...), ), updateUser: connect.NewClient[v1.UpdateUserRequest, v1.User]( httpClient, baseURL+UserServiceUpdateUserProcedure, connect.WithSchema(userServiceMethods.ByName("UpdateUser")), connect.WithClientOptions(opts...), ), deleteUser: connect.NewClient[v1.DeleteUserRequest, emptypb.Empty]( httpClient, baseURL+UserServiceDeleteUserProcedure, connect.WithSchema(userServiceMethods.ByName("DeleteUser")), connect.WithClientOptions(opts...), ), listAllUserStats: connect.NewClient[v1.ListAllUserStatsRequest, v1.ListAllUserStatsResponse]( httpClient, baseURL+UserServiceListAllUserStatsProcedure, connect.WithSchema(userServiceMethods.ByName("ListAllUserStats")), connect.WithClientOptions(opts...), ), getUserStats: connect.NewClient[v1.GetUserStatsRequest, v1.UserStats]( httpClient, baseURL+UserServiceGetUserStatsProcedure, connect.WithSchema(userServiceMethods.ByName("GetUserStats")), connect.WithClientOptions(opts...), ), getUserSetting: connect.NewClient[v1.GetUserSettingRequest, v1.UserSetting]( httpClient, baseURL+UserServiceGetUserSettingProcedure, connect.WithSchema(userServiceMethods.ByName("GetUserSetting")), connect.WithClientOptions(opts...), ), updateUserSetting: connect.NewClient[v1.UpdateUserSettingRequest, v1.UserSetting]( httpClient, baseURL+UserServiceUpdateUserSettingProcedure, connect.WithSchema(userServiceMethods.ByName("UpdateUserSetting")), connect.WithClientOptions(opts...), ), listUserSettings: connect.NewClient[v1.ListUserSettingsRequest, v1.ListUserSettingsResponse]( httpClient, baseURL+UserServiceListUserSettingsProcedure, connect.WithSchema(userServiceMethods.ByName("ListUserSettings")), connect.WithClientOptions(opts...), ), listPersonalAccessTokens: connect.NewClient[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse]( httpClient, baseURL+UserServiceListPersonalAccessTokensProcedure, connect.WithSchema(userServiceMethods.ByName("ListPersonalAccessTokens")), connect.WithClientOptions(opts...), ), createPersonalAccessToken: connect.NewClient[v1.CreatePersonalAccessTokenRequest, v1.CreatePersonalAccessTokenResponse]( httpClient, baseURL+UserServiceCreatePersonalAccessTokenProcedure, connect.WithSchema(userServiceMethods.ByName("CreatePersonalAccessToken")), connect.WithClientOptions(opts...), ), deletePersonalAccessToken: connect.NewClient[v1.DeletePersonalAccessTokenRequest, emptypb.Empty]( httpClient, baseURL+UserServiceDeletePersonalAccessTokenProcedure, connect.WithSchema(userServiceMethods.ByName("DeletePersonalAccessToken")), connect.WithClientOptions(opts...), ), listUserWebhooks: connect.NewClient[v1.ListUserWebhooksRequest, v1.ListUserWebhooksResponse]( httpClient, baseURL+UserServiceListUserWebhooksProcedure, connect.WithSchema(userServiceMethods.ByName("ListUserWebhooks")), connect.WithClientOptions(opts...), ), createUserWebhook: connect.NewClient[v1.CreateUserWebhookRequest, v1.UserWebhook]( httpClient, baseURL+UserServiceCreateUserWebhookProcedure, connect.WithSchema(userServiceMethods.ByName("CreateUserWebhook")), connect.WithClientOptions(opts...), ), updateUserWebhook: connect.NewClient[v1.UpdateUserWebhookRequest, v1.UserWebhook]( httpClient, baseURL+UserServiceUpdateUserWebhookProcedure, connect.WithSchema(userServiceMethods.ByName("UpdateUserWebhook")), connect.WithClientOptions(opts...), ), deleteUserWebhook: connect.NewClient[v1.DeleteUserWebhookRequest, emptypb.Empty]( httpClient, baseURL+UserServiceDeleteUserWebhookProcedure, connect.WithSchema(userServiceMethods.ByName("DeleteUserWebhook")), connect.WithClientOptions(opts...), ), listUserNotifications: connect.NewClient[v1.ListUserNotificationsRequest, v1.ListUserNotificationsResponse]( httpClient, baseURL+UserServiceListUserNotificationsProcedure, connect.WithSchema(userServiceMethods.ByName("ListUserNotifications")), connect.WithClientOptions(opts...), ), updateUserNotification: connect.NewClient[v1.UpdateUserNotificationRequest, v1.UserNotification]( httpClient, baseURL+UserServiceUpdateUserNotificationProcedure, connect.WithSchema(userServiceMethods.ByName("UpdateUserNotification")), connect.WithClientOptions(opts...), ), deleteUserNotification: connect.NewClient[v1.DeleteUserNotificationRequest, emptypb.Empty]( httpClient, baseURL+UserServiceDeleteUserNotificationProcedure, connect.WithSchema(userServiceMethods.ByName("DeleteUserNotification")), connect.WithClientOptions(opts...), ), } } // userServiceClient implements UserServiceClient. type userServiceClient struct { listUsers *connect.Client[v1.ListUsersRequest, v1.ListUsersResponse] getUser *connect.Client[v1.GetUserRequest, v1.User] createUser *connect.Client[v1.CreateUserRequest, v1.User] updateUser *connect.Client[v1.UpdateUserRequest, v1.User] deleteUser *connect.Client[v1.DeleteUserRequest, emptypb.Empty] listAllUserStats *connect.Client[v1.ListAllUserStatsRequest, v1.ListAllUserStatsResponse] getUserStats *connect.Client[v1.GetUserStatsRequest, v1.UserStats] getUserSetting *connect.Client[v1.GetUserSettingRequest, v1.UserSetting] updateUserSetting *connect.Client[v1.UpdateUserSettingRequest, v1.UserSetting] listUserSettings *connect.Client[v1.ListUserSettingsRequest, v1.ListUserSettingsResponse] listPersonalAccessTokens *connect.Client[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse] createPersonalAccessToken *connect.Client[v1.CreatePersonalAccessTokenRequest, v1.CreatePersonalAccessTokenResponse] deletePersonalAccessToken *connect.Client[v1.DeletePersonalAccessTokenRequest, emptypb.Empty] listUserWebhooks *connect.Client[v1.ListUserWebhooksRequest, v1.ListUserWebhooksResponse] createUserWebhook *connect.Client[v1.CreateUserWebhookRequest, v1.UserWebhook] updateUserWebhook *connect.Client[v1.UpdateUserWebhookRequest, v1.UserWebhook] deleteUserWebhook *connect.Client[v1.DeleteUserWebhookRequest, emptypb.Empty] listUserNotifications *connect.Client[v1.ListUserNotificationsRequest, v1.ListUserNotificationsResponse] updateUserNotification *connect.Client[v1.UpdateUserNotificationRequest, v1.UserNotification] deleteUserNotification *connect.Client[v1.DeleteUserNotificationRequest, emptypb.Empty] } // ListUsers calls memos.api.v1.UserService.ListUsers. func (c *userServiceClient) ListUsers(ctx context.Context, req *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error) { return c.listUsers.CallUnary(ctx, req) } // GetUser calls memos.api.v1.UserService.GetUser. func (c *userServiceClient) GetUser(ctx context.Context, req *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error) { return c.getUser.CallUnary(ctx, req) } // CreateUser calls memos.api.v1.UserService.CreateUser. func (c *userServiceClient) CreateUser(ctx context.Context, req *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.User], error) { return c.createUser.CallUnary(ctx, req) } // UpdateUser calls memos.api.v1.UserService.UpdateUser. func (c *userServiceClient) UpdateUser(ctx context.Context, req *connect.Request[v1.UpdateUserRequest]) (*connect.Response[v1.User], error) { return c.updateUser.CallUnary(ctx, req) } // DeleteUser calls memos.api.v1.UserService.DeleteUser. func (c *userServiceClient) DeleteUser(ctx context.Context, req *connect.Request[v1.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) { return c.deleteUser.CallUnary(ctx, req) } // ListAllUserStats calls memos.api.v1.UserService.ListAllUserStats. func (c *userServiceClient) ListAllUserStats(ctx context.Context, req *connect.Request[v1.ListAllUserStatsRequest]) (*connect.Response[v1.ListAllUserStatsResponse], error) { return c.listAllUserStats.CallUnary(ctx, req) } // GetUserStats calls memos.api.v1.UserService.GetUserStats. func (c *userServiceClient) GetUserStats(ctx context.Context, req *connect.Request[v1.GetUserStatsRequest]) (*connect.Response[v1.UserStats], error) { return c.getUserStats.CallUnary(ctx, req) } // GetUserSetting calls memos.api.v1.UserService.GetUserSetting. func (c *userServiceClient) GetUserSetting(ctx context.Context, req *connect.Request[v1.GetUserSettingRequest]) (*connect.Response[v1.UserSetting], error) { return c.getUserSetting.CallUnary(ctx, req) } // UpdateUserSetting calls memos.api.v1.UserService.UpdateUserSetting. func (c *userServiceClient) UpdateUserSetting(ctx context.Context, req *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error) { return c.updateUserSetting.CallUnary(ctx, req) } // ListUserSettings calls memos.api.v1.UserService.ListUserSettings. func (c *userServiceClient) ListUserSettings(ctx context.Context, req *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error) { return c.listUserSettings.CallUnary(ctx, req) } // ListPersonalAccessTokens calls memos.api.v1.UserService.ListPersonalAccessTokens. func (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) { return c.listPersonalAccessTokens.CallUnary(ctx, req) } // CreatePersonalAccessToken calls memos.api.v1.UserService.CreatePersonalAccessToken. func (c *userServiceClient) CreatePersonalAccessToken(ctx context.Context, req *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error) { return c.createPersonalAccessToken.CallUnary(ctx, req) } // DeletePersonalAccessToken calls memos.api.v1.UserService.DeletePersonalAccessToken. func (c *userServiceClient) DeletePersonalAccessToken(ctx context.Context, req *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) { return c.deletePersonalAccessToken.CallUnary(ctx, req) } // ListUserWebhooks calls memos.api.v1.UserService.ListUserWebhooks. func (c *userServiceClient) ListUserWebhooks(ctx context.Context, req *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error) { return c.listUserWebhooks.CallUnary(ctx, req) } // CreateUserWebhook calls memos.api.v1.UserService.CreateUserWebhook. func (c *userServiceClient) CreateUserWebhook(ctx context.Context, req *connect.Request[v1.CreateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) { return c.createUserWebhook.CallUnary(ctx, req) } // UpdateUserWebhook calls memos.api.v1.UserService.UpdateUserWebhook. func (c *userServiceClient) UpdateUserWebhook(ctx context.Context, req *connect.Request[v1.UpdateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) { return c.updateUserWebhook.CallUnary(ctx, req) } // DeleteUserWebhook calls memos.api.v1.UserService.DeleteUserWebhook. func (c *userServiceClient) DeleteUserWebhook(ctx context.Context, req *connect.Request[v1.DeleteUserWebhookRequest]) (*connect.Response[emptypb.Empty], error) { return c.deleteUserWebhook.CallUnary(ctx, req) } // ListUserNotifications calls memos.api.v1.UserService.ListUserNotifications. func (c *userServiceClient) ListUserNotifications(ctx context.Context, req *connect.Request[v1.ListUserNotificationsRequest]) (*connect.Response[v1.ListUserNotificationsResponse], error) { return c.listUserNotifications.CallUnary(ctx, req) } // UpdateUserNotification calls memos.api.v1.UserService.UpdateUserNotification. func (c *userServiceClient) UpdateUserNotification(ctx context.Context, req *connect.Request[v1.UpdateUserNotificationRequest]) (*connect.Response[v1.UserNotification], error) { return c.updateUserNotification.CallUnary(ctx, req) } // DeleteUserNotification calls memos.api.v1.UserService.DeleteUserNotification. func (c *userServiceClient) DeleteUserNotification(ctx context.Context, req *connect.Request[v1.DeleteUserNotificationRequest]) (*connect.Response[emptypb.Empty], error) { return c.deleteUserNotification.CallUnary(ctx, req) } // UserServiceHandler is an implementation of the memos.api.v1.UserService service. type UserServiceHandler interface { // ListUsers returns a list of users. ListUsers(context.Context, *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error) // GetUser gets a user by ID or username. // Supports both numeric IDs and username strings: // - users/{id} (e.g., users/101) // - users/{username} (e.g., users/steven) GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error) // CreateUser creates a new user. CreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.User], error) // UpdateUser updates a user. UpdateUser(context.Context, *connect.Request[v1.UpdateUserRequest]) (*connect.Response[v1.User], error) // DeleteUser deletes a user. DeleteUser(context.Context, *connect.Request[v1.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) // ListAllUserStats returns statistics for all users. ListAllUserStats(context.Context, *connect.Request[v1.ListAllUserStatsRequest]) (*connect.Response[v1.ListAllUserStatsResponse], error) // GetUserStats returns statistics for a specific user. GetUserStats(context.Context, *connect.Request[v1.GetUserStatsRequest]) (*connect.Response[v1.UserStats], error) // GetUserSetting returns the user setting. GetUserSetting(context.Context, *connect.Request[v1.GetUserSettingRequest]) (*connect.Response[v1.UserSetting], error) // UpdateUserSetting updates the user setting. UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error) // ListUserSettings returns a list of user settings. ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error) // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) // CreatePersonalAccessToken creates a new Personal Access Token for a user. // The token value is only returned once upon creation. CreatePersonalAccessToken(context.Context, *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error) // DeletePersonalAccessToken deletes a Personal Access Token. DeletePersonalAccessToken(context.Context, *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) // ListUserWebhooks returns a list of webhooks for a user. ListUserWebhooks(context.Context, *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error) // CreateUserWebhook creates a new webhook for a user. CreateUserWebhook(context.Context, *connect.Request[v1.CreateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) // UpdateUserWebhook updates an existing webhook for a user. UpdateUserWebhook(context.Context, *connect.Request[v1.UpdateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) // DeleteUserWebhook deletes a webhook for a user. DeleteUserWebhook(context.Context, *connect.Request[v1.DeleteUserWebhookRequest]) (*connect.Response[emptypb.Empty], error) // ListUserNotifications lists notifications for a user. ListUserNotifications(context.Context, *connect.Request[v1.ListUserNotificationsRequest]) (*connect.Response[v1.ListUserNotificationsResponse], error) // UpdateUserNotification updates a notification. UpdateUserNotification(context.Context, *connect.Request[v1.UpdateUserNotificationRequest]) (*connect.Response[v1.UserNotification], error) // DeleteUserNotification deletes a notification. DeleteUserNotification(context.Context, *connect.Request[v1.DeleteUserNotificationRequest]) (*connect.Response[emptypb.Empty], error) } // NewUserServiceHandler builds an HTTP handler from the service implementation. It returns the path // on which to mount the handler and the handler itself. // // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { userServiceMethods := v1.File_api_v1_user_service_proto.Services().ByName("UserService").Methods() userServiceListUsersHandler := connect.NewUnaryHandler( UserServiceListUsersProcedure, svc.ListUsers, connect.WithSchema(userServiceMethods.ByName("ListUsers")), connect.WithHandlerOptions(opts...), ) userServiceGetUserHandler := connect.NewUnaryHandler( UserServiceGetUserProcedure, svc.GetUser, connect.WithSchema(userServiceMethods.ByName("GetUser")), connect.WithHandlerOptions(opts...), ) userServiceCreateUserHandler := connect.NewUnaryHandler( UserServiceCreateUserProcedure, svc.CreateUser, connect.WithSchema(userServiceMethods.ByName("CreateUser")), connect.WithHandlerOptions(opts...), ) userServiceUpdateUserHandler := connect.NewUnaryHandler( UserServiceUpdateUserProcedure, svc.UpdateUser, connect.WithSchema(userServiceMethods.ByName("UpdateUser")), connect.WithHandlerOptions(opts...), ) userServiceDeleteUserHandler := connect.NewUnaryHandler( UserServiceDeleteUserProcedure, svc.DeleteUser, connect.WithSchema(userServiceMethods.ByName("DeleteUser")), connect.WithHandlerOptions(opts...), ) userServiceListAllUserStatsHandler := connect.NewUnaryHandler( UserServiceListAllUserStatsProcedure, svc.ListAllUserStats, connect.WithSchema(userServiceMethods.ByName("ListAllUserStats")), connect.WithHandlerOptions(opts...), ) userServiceGetUserStatsHandler := connect.NewUnaryHandler( UserServiceGetUserStatsProcedure, svc.GetUserStats, connect.WithSchema(userServiceMethods.ByName("GetUserStats")), connect.WithHandlerOptions(opts...), ) userServiceGetUserSettingHandler := connect.NewUnaryHandler( UserServiceGetUserSettingProcedure, svc.GetUserSetting, connect.WithSchema(userServiceMethods.ByName("GetUserSetting")), connect.WithHandlerOptions(opts...), ) userServiceUpdateUserSettingHandler := connect.NewUnaryHandler( UserServiceUpdateUserSettingProcedure, svc.UpdateUserSetting, connect.WithSchema(userServiceMethods.ByName("UpdateUserSetting")), connect.WithHandlerOptions(opts...), ) userServiceListUserSettingsHandler := connect.NewUnaryHandler( UserServiceListUserSettingsProcedure, svc.ListUserSettings, connect.WithSchema(userServiceMethods.ByName("ListUserSettings")), connect.WithHandlerOptions(opts...), ) userServiceListPersonalAccessTokensHandler := connect.NewUnaryHandler( UserServiceListPersonalAccessTokensProcedure, svc.ListPersonalAccessTokens, connect.WithSchema(userServiceMethods.ByName("ListPersonalAccessTokens")), connect.WithHandlerOptions(opts...), ) userServiceCreatePersonalAccessTokenHandler := connect.NewUnaryHandler( UserServiceCreatePersonalAccessTokenProcedure, svc.CreatePersonalAccessToken, connect.WithSchema(userServiceMethods.ByName("CreatePersonalAccessToken")), connect.WithHandlerOptions(opts...), ) userServiceDeletePersonalAccessTokenHandler := connect.NewUnaryHandler( UserServiceDeletePersonalAccessTokenProcedure, svc.DeletePersonalAccessToken, connect.WithSchema(userServiceMethods.ByName("DeletePersonalAccessToken")), connect.WithHandlerOptions(opts...), ) userServiceListUserWebhooksHandler := connect.NewUnaryHandler( UserServiceListUserWebhooksProcedure, svc.ListUserWebhooks, connect.WithSchema(userServiceMethods.ByName("ListUserWebhooks")), connect.WithHandlerOptions(opts...), ) userServiceCreateUserWebhookHandler := connect.NewUnaryHandler( UserServiceCreateUserWebhookProcedure, svc.CreateUserWebhook, connect.WithSchema(userServiceMethods.ByName("CreateUserWebhook")), connect.WithHandlerOptions(opts...), ) userServiceUpdateUserWebhookHandler := connect.NewUnaryHandler( UserServiceUpdateUserWebhookProcedure, svc.UpdateUserWebhook, connect.WithSchema(userServiceMethods.ByName("UpdateUserWebhook")), connect.WithHandlerOptions(opts...), ) userServiceDeleteUserWebhookHandler := connect.NewUnaryHandler( UserServiceDeleteUserWebhookProcedure, svc.DeleteUserWebhook, connect.WithSchema(userServiceMethods.ByName("DeleteUserWebhook")), connect.WithHandlerOptions(opts...), ) userServiceListUserNotificationsHandler := connect.NewUnaryHandler( UserServiceListUserNotificationsProcedure, svc.ListUserNotifications, connect.WithSchema(userServiceMethods.ByName("ListUserNotifications")), connect.WithHandlerOptions(opts...), ) userServiceUpdateUserNotificationHandler := connect.NewUnaryHandler( UserServiceUpdateUserNotificationProcedure, svc.UpdateUserNotification, connect.WithSchema(userServiceMethods.ByName("UpdateUserNotification")), connect.WithHandlerOptions(opts...), ) userServiceDeleteUserNotificationHandler := connect.NewUnaryHandler( UserServiceDeleteUserNotificationProcedure, svc.DeleteUserNotification, connect.WithSchema(userServiceMethods.ByName("DeleteUserNotification")), connect.WithHandlerOptions(opts...), ) return "/memos.api.v1.UserService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case UserServiceListUsersProcedure: userServiceListUsersHandler.ServeHTTP(w, r) case UserServiceGetUserProcedure: userServiceGetUserHandler.ServeHTTP(w, r) case UserServiceCreateUserProcedure: userServiceCreateUserHandler.ServeHTTP(w, r) case UserServiceUpdateUserProcedure: userServiceUpdateUserHandler.ServeHTTP(w, r) case UserServiceDeleteUserProcedure: userServiceDeleteUserHandler.ServeHTTP(w, r) case UserServiceListAllUserStatsProcedure: userServiceListAllUserStatsHandler.ServeHTTP(w, r) case UserServiceGetUserStatsProcedure: userServiceGetUserStatsHandler.ServeHTTP(w, r) case UserServiceGetUserSettingProcedure: userServiceGetUserSettingHandler.ServeHTTP(w, r) case UserServiceUpdateUserSettingProcedure: userServiceUpdateUserSettingHandler.ServeHTTP(w, r) case UserServiceListUserSettingsProcedure: userServiceListUserSettingsHandler.ServeHTTP(w, r) case UserServiceListPersonalAccessTokensProcedure: userServiceListPersonalAccessTokensHandler.ServeHTTP(w, r) case UserServiceCreatePersonalAccessTokenProcedure: userServiceCreatePersonalAccessTokenHandler.ServeHTTP(w, r) case UserServiceDeletePersonalAccessTokenProcedure: userServiceDeletePersonalAccessTokenHandler.ServeHTTP(w, r) case UserServiceListUserWebhooksProcedure: userServiceListUserWebhooksHandler.ServeHTTP(w, r) case UserServiceCreateUserWebhookProcedure: userServiceCreateUserWebhookHandler.ServeHTTP(w, r) case UserServiceUpdateUserWebhookProcedure: userServiceUpdateUserWebhookHandler.ServeHTTP(w, r) case UserServiceDeleteUserWebhookProcedure: userServiceDeleteUserWebhookHandler.ServeHTTP(w, r) case UserServiceListUserNotificationsProcedure: userServiceListUserNotificationsHandler.ServeHTTP(w, r) case UserServiceUpdateUserNotificationProcedure: userServiceUpdateUserNotificationHandler.ServeHTTP(w, r) case UserServiceDeleteUserNotificationProcedure: userServiceDeleteUserNotificationHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } }) } // UnimplementedUserServiceHandler returns CodeUnimplemented from all methods. type UnimplementedUserServiceHandler struct{} func (UnimplementedUserServiceHandler) ListUsers(context.Context, *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUsers is not implemented")) } func (UnimplementedUserServiceHandler) GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.GetUser is not implemented")) } func (UnimplementedUserServiceHandler) CreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.User], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.CreateUser is not implemented")) } func (UnimplementedUserServiceHandler) UpdateUser(context.Context, *connect.Request[v1.UpdateUserRequest]) (*connect.Response[v1.User], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.UpdateUser is not implemented")) } func (UnimplementedUserServiceHandler) DeleteUser(context.Context, *connect.Request[v1.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.DeleteUser is not implemented")) } func (UnimplementedUserServiceHandler) ListAllUserStats(context.Context, *connect.Request[v1.ListAllUserStatsRequest]) (*connect.Response[v1.ListAllUserStatsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListAllUserStats is not implemented")) } func (UnimplementedUserServiceHandler) GetUserStats(context.Context, *connect.Request[v1.GetUserStatsRequest]) (*connect.Response[v1.UserStats], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.GetUserStats is not implemented")) } func (UnimplementedUserServiceHandler) GetUserSetting(context.Context, *connect.Request[v1.GetUserSettingRequest]) (*connect.Response[v1.UserSetting], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.GetUserSetting is not implemented")) } func (UnimplementedUserServiceHandler) UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.UpdateUserSetting is not implemented")) } func (UnimplementedUserServiceHandler) ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUserSettings is not implemented")) } func (UnimplementedUserServiceHandler) ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListPersonalAccessTokens is not implemented")) } func (UnimplementedUserServiceHandler) CreatePersonalAccessToken(context.Context, *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.CreatePersonalAccessToken is not implemented")) } func (UnimplementedUserServiceHandler) DeletePersonalAccessToken(context.Context, *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.DeletePersonalAccessToken is not implemented")) } func (UnimplementedUserServiceHandler) ListUserWebhooks(context.Context, *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUserWebhooks is not implemented")) } func (UnimplementedUserServiceHandler) CreateUserWebhook(context.Context, *connect.Request[v1.CreateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.CreateUserWebhook is not implemented")) } func (UnimplementedUserServiceHandler) UpdateUserWebhook(context.Context, *connect.Request[v1.UpdateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.UpdateUserWebhook is not implemented")) } func (UnimplementedUserServiceHandler) DeleteUserWebhook(context.Context, *connect.Request[v1.DeleteUserWebhookRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.DeleteUserWebhook is not implemented")) } func (UnimplementedUserServiceHandler) ListUserNotifications(context.Context, *connect.Request[v1.ListUserNotificationsRequest]) (*connect.Response[v1.ListUserNotificationsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUserNotifications is not implemented")) } func (UnimplementedUserServiceHandler) UpdateUserNotification(context.Context, *connect.Request[v1.UpdateUserNotificationRequest]) (*connect.Response[v1.UserNotification], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.UpdateUserNotification is not implemented")) } func (UnimplementedUserServiceHandler) DeleteUserNotification(context.Context, *connect.Request[v1.DeleteUserNotificationRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.DeleteUserNotification is not implemented")) } ================================================ FILE: proto/gen/api/v1/attachment_service.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc (unknown) // source: api/v1/attachment_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 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: "mime_type==\"image/png\"" or "filename.contains(\"test\")" // Supported operators: =, !=, <, <=, >, >=, : (contains), in // Supported fields: filename, mime_type, 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 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[5] 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[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 UpdateAttachmentRequest.ProtoReflect.Descriptor instead. func (*UpdateAttachmentRequest) Descriptor() ([]byte, []int) { return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{5} } 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[6] 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[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 DeleteAttachmentRequest.ProtoReflect.Descriptor instead. func (*DeleteAttachmentRequest) Descriptor() ([]byte, []int) { return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{6} } 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/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\"\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\xc4\x05\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\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, 7) 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 (*UpdateAttachmentRequest)(nil), // 5: memos.api.v1.UpdateAttachmentRequest (*DeleteAttachmentRequest)(nil), // 6: memos.api.v1.DeleteAttachmentRequest (*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_attachment_service_proto_depIdxs = []int32{ 7, // 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 8, // 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.UpdateAttachment:input_type -> memos.api.v1.UpdateAttachmentRequest 6, // 9: memos.api.v1.AttachmentService.DeleteAttachment:input_type -> memos.api.v1.DeleteAttachmentRequest 0, // 10: memos.api.v1.AttachmentService.CreateAttachment:output_type -> memos.api.v1.Attachment 3, // 11: memos.api.v1.AttachmentService.ListAttachments:output_type -> memos.api.v1.ListAttachmentsResponse 0, // 12: memos.api.v1.AttachmentService.GetAttachment:output_type -> memos.api.v1.Attachment 0, // 13: memos.api.v1.AttachmentService.UpdateAttachment:output_type -> memos.api.v1.Attachment 9, // 14: memos.api.v1.AttachmentService.DeleteAttachment:output_type -> google.protobuf.Empty 10, // [10:15] is the sub-list for method output_type 5, // [5:10] 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: 7, 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 } ================================================ FILE: proto/gen/api/v1/attachment_service.pb.gw.go ================================================ // 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_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.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.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_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_UpdateAttachment_0 = runtime.ForwardResponseMessage forward_AttachmentService_DeleteAttachment_0 = runtime.ForwardResponseMessage ) ================================================ FILE: proto/gen/api/v1/attachment_service_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 // - protoc (unknown) // source: api/v1/attachment_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 ( 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_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 an attachment by name. GetAttachment(ctx context.Context, in *GetAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) // UpdateAttachment updates an attachment. UpdateAttachment(ctx context.Context, in *UpdateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) // DeleteAttachment deletes an 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) 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 an attachment by name. GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error) // UpdateAttachment updates an attachment. UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error) // DeleteAttachment deletes an 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.Error(codes.Unimplemented, "method CreateAttachment not implemented") } func (UnimplementedAttachmentServiceServer) ListAttachments(context.Context, *ListAttachmentsRequest) (*ListAttachmentsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListAttachments not implemented") } func (UnimplementedAttachmentServiceServer) GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error) { return nil, status.Error(codes.Unimplemented, "method GetAttachment not implemented") } func (UnimplementedAttachmentServiceServer) UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error) { return nil, status.Error(codes.Unimplemented, "method UpdateAttachment not implemented") } func (UnimplementedAttachmentServiceServer) DeleteAttachment(context.Context, *DeleteAttachmentRequest) (*emptypb.Empty, error) { return nil, status.Error(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 panics, 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_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: "UpdateAttachment", Handler: _AttachmentService_UpdateAttachment_Handler, }, { MethodName: "DeleteAttachment", Handler: _AttachmentService_DeleteAttachment_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "api/v1/attachment_service.proto", } ================================================ FILE: proto/gen/api/v1/auth_service.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // 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 GetCurrentUserRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetCurrentUserRequest) Reset() { *x = GetCurrentUserRequest{} mi := &file_api_v1_auth_service_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GetCurrentUserRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetCurrentUserRequest) ProtoMessage() {} func (x *GetCurrentUserRequest) 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 GetCurrentUserRequest.ProtoReflect.Descriptor instead. func (*GetCurrentUserRequest) Descriptor() ([]byte, []int) { return file_api_v1_auth_service_proto_rawDescGZIP(), []int{0} } type GetCurrentUserResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The authenticated user's information. User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetCurrentUserResponse) Reset() { *x = GetCurrentUserResponse{} mi := &file_api_v1_auth_service_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GetCurrentUserResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetCurrentUserResponse) ProtoMessage() {} func (x *GetCurrentUserResponse) 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 GetCurrentUserResponse.ProtoReflect.Descriptor instead. func (*GetCurrentUserResponse) Descriptor() ([]byte, []int) { return file_api_v1_auth_service_proto_rawDescGZIP(), []int{1} } func (x *GetCurrentUserResponse) GetUser() *User { if x != nil { return x.User } return nil } type SignInRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Authentication credentials. Provide one method. // // Types that are valid to be assigned to Credentials: // // *SignInRequest_PasswordCredentials_ // *SignInRequest_SsoCredentials Credentials isSignInRequest_Credentials `protobuf_oneof:"credentials"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SignInRequest) Reset() { *x = SignInRequest{} mi := &file_api_v1_auth_service_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SignInRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SignInRequest) ProtoMessage() {} func (x *SignInRequest) 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 SignInRequest.ProtoReflect.Descriptor instead. func (*SignInRequest) Descriptor() ([]byte, []int) { return file_api_v1_auth_service_proto_rawDescGZIP(), []int{2} } func (x *SignInRequest) GetCredentials() isSignInRequest_Credentials { if x != nil { return x.Credentials } return nil } func (x *SignInRequest) GetPasswordCredentials() *SignInRequest_PasswordCredentials { if x != nil { if x, ok := x.Credentials.(*SignInRequest_PasswordCredentials_); ok { return x.PasswordCredentials } } return nil } func (x *SignInRequest) GetSsoCredentials() *SignInRequest_SSOCredentials { if x != nil { if x, ok := x.Credentials.(*SignInRequest_SsoCredentials); ok { return x.SsoCredentials } } return nil } type isSignInRequest_Credentials interface { isSignInRequest_Credentials() } type SignInRequest_PasswordCredentials_ struct { // Username and password authentication. PasswordCredentials *SignInRequest_PasswordCredentials `protobuf:"bytes,1,opt,name=password_credentials,json=passwordCredentials,proto3,oneof"` } type SignInRequest_SsoCredentials struct { // SSO provider authentication. SsoCredentials *SignInRequest_SSOCredentials `protobuf:"bytes,2,opt,name=sso_credentials,json=ssoCredentials,proto3,oneof"` } func (*SignInRequest_PasswordCredentials_) isSignInRequest_Credentials() {} func (*SignInRequest_SsoCredentials) isSignInRequest_Credentials() {} type SignInResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The authenticated user's information. User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` // The short-lived access token for API requests. // Store in memory only, not in localStorage. AccessToken string `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` // When the access token expires. // Client should call RefreshToken before this time. AccessTokenExpiresAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=access_token_expires_at,json=accessTokenExpiresAt,proto3" json:"access_token_expires_at,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SignInResponse) Reset() { *x = SignInResponse{} mi := &file_api_v1_auth_service_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SignInResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*SignInResponse) ProtoMessage() {} func (x *SignInResponse) 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 SignInResponse.ProtoReflect.Descriptor instead. func (*SignInResponse) Descriptor() ([]byte, []int) { return file_api_v1_auth_service_proto_rawDescGZIP(), []int{3} } func (x *SignInResponse) GetUser() *User { if x != nil { return x.User } return nil } func (x *SignInResponse) GetAccessToken() string { if x != nil { return x.AccessToken } return "" } func (x *SignInResponse) GetAccessTokenExpiresAt() *timestamppb.Timestamp { if x != nil { return x.AccessTokenExpiresAt } return nil } type SignOutRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SignOutRequest) Reset() { *x = SignOutRequest{} mi := &file_api_v1_auth_service_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SignOutRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SignOutRequest) ProtoMessage() {} func (x *SignOutRequest) 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 SignOutRequest.ProtoReflect.Descriptor instead. func (*SignOutRequest) Descriptor() ([]byte, []int) { return file_api_v1_auth_service_proto_rawDescGZIP(), []int{4} } type RefreshTokenRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RefreshTokenRequest) Reset() { *x = RefreshTokenRequest{} mi := &file_api_v1_auth_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RefreshTokenRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*RefreshTokenRequest) ProtoMessage() {} func (x *RefreshTokenRequest) 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 RefreshTokenRequest.ProtoReflect.Descriptor instead. func (*RefreshTokenRequest) Descriptor() ([]byte, []int) { return file_api_v1_auth_service_proto_rawDescGZIP(), []int{5} } type RefreshTokenResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The new short-lived access token. AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` // When the access token expires. ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RefreshTokenResponse) Reset() { *x = RefreshTokenResponse{} mi := &file_api_v1_auth_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RefreshTokenResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*RefreshTokenResponse) ProtoMessage() {} func (x *RefreshTokenResponse) 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 RefreshTokenResponse.ProtoReflect.Descriptor instead. func (*RefreshTokenResponse) Descriptor() ([]byte, []int) { return file_api_v1_auth_service_proto_rawDescGZIP(), []int{6} } func (x *RefreshTokenResponse) GetAccessToken() string { if x != nil { return x.AccessToken } return "" } func (x *RefreshTokenResponse) GetExpiresAt() *timestamppb.Timestamp { if x != nil { return x.ExpiresAt } return nil } // Nested message for password-based authentication credentials. type SignInRequest_PasswordCredentials struct { state protoimpl.MessageState `protogen:"open.v1"` // The username to sign in with. Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` // The password to sign in with. Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SignInRequest_PasswordCredentials) Reset() { *x = SignInRequest_PasswordCredentials{} mi := &file_api_v1_auth_service_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SignInRequest_PasswordCredentials) String() string { return protoimpl.X.MessageStringOf(x) } func (*SignInRequest_PasswordCredentials) ProtoMessage() {} func (x *SignInRequest_PasswordCredentials) ProtoReflect() protoreflect.Message { mi := &file_api_v1_auth_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 SignInRequest_PasswordCredentials.ProtoReflect.Descriptor instead. func (*SignInRequest_PasswordCredentials) Descriptor() ([]byte, []int) { return file_api_v1_auth_service_proto_rawDescGZIP(), []int{2, 0} } func (x *SignInRequest_PasswordCredentials) GetUsername() string { if x != nil { return x.Username } return "" } func (x *SignInRequest_PasswordCredentials) GetPassword() string { if x != nil { return x.Password } return "" } // Nested message for SSO authentication credentials. type SignInRequest_SSOCredentials struct { state protoimpl.MessageState `protogen:"open.v1"` // The resource name of the SSO provider. // Format: identity-providers/{uid} IdpName string `protobuf:"bytes,1,opt,name=idp_name,json=idpName,proto3" json:"idp_name,omitempty"` // The authorization code from the SSO provider. Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"` // The redirect URI used in the SSO flow. RedirectUri string `protobuf:"bytes,3,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"` // The PKCE code verifier for enhanced security (RFC 7636). // Optional - enables PKCE flow protection against authorization code interception. CodeVerifier string `protobuf:"bytes,4,opt,name=code_verifier,json=codeVerifier,proto3" json:"code_verifier,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SignInRequest_SSOCredentials) Reset() { *x = SignInRequest_SSOCredentials{} mi := &file_api_v1_auth_service_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SignInRequest_SSOCredentials) String() string { return protoimpl.X.MessageStringOf(x) } func (*SignInRequest_SSOCredentials) ProtoMessage() {} func (x *SignInRequest_SSOCredentials) ProtoReflect() protoreflect.Message { mi := &file_api_v1_auth_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 SignInRequest_SSOCredentials.ProtoReflect.Descriptor instead. func (*SignInRequest_SSOCredentials) Descriptor() ([]byte, []int) { return file_api_v1_auth_service_proto_rawDescGZIP(), []int{2, 1} } func (x *SignInRequest_SSOCredentials) GetIdpName() string { if x != nil { return x.IdpName } return "" } func (x *SignInRequest_SSOCredentials) GetCode() string { if x != nil { return x.Code } return "" } func (x *SignInRequest_SSOCredentials) GetRedirectUri() string { if x != nil { return x.RedirectUri } return "" } func (x *SignInRequest_SSOCredentials) GetCodeVerifier() string { if x != nil { return x.CodeVerifier } 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\"\x17\n" + "\x15GetCurrentUserRequest\"@\n" + "\x16GetCurrentUserResponse\x12&\n" + "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\"\xd2\x03\n" + "\rSignInRequest\x12d\n" + "\x14password_credentials\x18\x01 \x01(\v2/.memos.api.v1.SignInRequest.PasswordCredentialsH\x00R\x13passwordCredentials\x12U\n" + "\x0fsso_credentials\x18\x02 \x01(\v2*.memos.api.v1.SignInRequest.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\x1a\x9b\x01\n" + "\x0eSSOCredentials\x12\x1e\n" + "\bidp_name\x18\x01 \x01(\tB\x03\xe0A\x02R\aidpName\x12\x17\n" + "\x04code\x18\x02 \x01(\tB\x03\xe0A\x02R\x04code\x12&\n" + "\fredirect_uri\x18\x03 \x01(\tB\x03\xe0A\x02R\vredirectUri\x12(\n" + "\rcode_verifier\x18\x04 \x01(\tB\x03\xe0A\x01R\fcodeVerifierB\r\n" + "\vcredentials\"\xae\x01\n" + "\x0eSignInResponse\x12&\n" + "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\x12!\n" + "\faccess_token\x18\x02 \x01(\tR\vaccessToken\x12Q\n" + "\x17access_token_expires_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x14accessTokenExpiresAt\"\x10\n" + "\x0eSignOutRequest\"\x15\n" + "\x13RefreshTokenRequest\"t\n" + "\x14RefreshTokenResponse\x12!\n" + "\faccess_token\x18\x01 \x01(\tR\vaccessToken\x129\n" + "\n" + "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt2\xbf\x03\n" + "\vAuthService\x12t\n" + "\x0eGetCurrentUser\x12#.memos.api.v1.GetCurrentUserRequest\x1a$.memos.api.v1.GetCurrentUserResponse\"\x17\x82\xd3\xe4\x93\x02\x11\x12\x0f/api/v1/auth/me\x12c\n" + "\x06SignIn\x12\x1b.memos.api.v1.SignInRequest\x1a\x1c.memos.api.v1.SignInResponse\"\x1e\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/api/v1/auth/signin\x12]\n" + "\aSignOut\x12\x1c.memos.api.v1.SignOutRequest\x1a\x16.google.protobuf.Empty\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x14/api/v1/auth/signout\x12v\n" + "\fRefreshToken\x12!.memos.api.v1.RefreshTokenRequest\x1a\".memos.api.v1.RefreshTokenResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/v1/auth/refreshB\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, 9) var file_api_v1_auth_service_proto_goTypes = []any{ (*GetCurrentUserRequest)(nil), // 0: memos.api.v1.GetCurrentUserRequest (*GetCurrentUserResponse)(nil), // 1: memos.api.v1.GetCurrentUserResponse (*SignInRequest)(nil), // 2: memos.api.v1.SignInRequest (*SignInResponse)(nil), // 3: memos.api.v1.SignInResponse (*SignOutRequest)(nil), // 4: memos.api.v1.SignOutRequest (*RefreshTokenRequest)(nil), // 5: memos.api.v1.RefreshTokenRequest (*RefreshTokenResponse)(nil), // 6: memos.api.v1.RefreshTokenResponse (*SignInRequest_PasswordCredentials)(nil), // 7: memos.api.v1.SignInRequest.PasswordCredentials (*SignInRequest_SSOCredentials)(nil), // 8: memos.api.v1.SignInRequest.SSOCredentials (*User)(nil), // 9: memos.api.v1.User (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp (*emptypb.Empty)(nil), // 11: google.protobuf.Empty } var file_api_v1_auth_service_proto_depIdxs = []int32{ 9, // 0: memos.api.v1.GetCurrentUserResponse.user:type_name -> memos.api.v1.User 7, // 1: memos.api.v1.SignInRequest.password_credentials:type_name -> memos.api.v1.SignInRequest.PasswordCredentials 8, // 2: memos.api.v1.SignInRequest.sso_credentials:type_name -> memos.api.v1.SignInRequest.SSOCredentials 9, // 3: memos.api.v1.SignInResponse.user:type_name -> memos.api.v1.User 10, // 4: memos.api.v1.SignInResponse.access_token_expires_at:type_name -> google.protobuf.Timestamp 10, // 5: memos.api.v1.RefreshTokenResponse.expires_at:type_name -> google.protobuf.Timestamp 0, // 6: memos.api.v1.AuthService.GetCurrentUser:input_type -> memos.api.v1.GetCurrentUserRequest 2, // 7: memos.api.v1.AuthService.SignIn:input_type -> memos.api.v1.SignInRequest 4, // 8: memos.api.v1.AuthService.SignOut:input_type -> memos.api.v1.SignOutRequest 5, // 9: memos.api.v1.AuthService.RefreshToken:input_type -> memos.api.v1.RefreshTokenRequest 1, // 10: memos.api.v1.AuthService.GetCurrentUser:output_type -> memos.api.v1.GetCurrentUserResponse 3, // 11: memos.api.v1.AuthService.SignIn:output_type -> memos.api.v1.SignInResponse 11, // 12: memos.api.v1.AuthService.SignOut:output_type -> google.protobuf.Empty 6, // 13: memos.api.v1.AuthService.RefreshToken:output_type -> memos.api.v1.RefreshTokenResponse 10, // [10:14] is the sub-list for method output_type 6, // [6:10] 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{ (*SignInRequest_PasswordCredentials_)(nil), (*SignInRequest_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: 9, 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 } ================================================ FILE: proto/gen/api/v1/auth_service.pb.gw.go ================================================ // 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_GetCurrentUser_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetCurrentUserRequest metadata runtime.ServerMetadata ) if req.Body != nil { _, _ = io.Copy(io.Discard, req.Body) } msg, err := client.GetCurrentUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_AuthService_GetCurrentUser_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetCurrentUserRequest metadata runtime.ServerMetadata ) msg, err := server.GetCurrentUser(ctx, &protoReq) return msg, metadata, err } func request_AuthService_SignIn_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq SignInRequest 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.SignIn(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_AuthService_SignIn_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq SignInRequest 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.SignIn(ctx, &protoReq) return msg, metadata, err } func request_AuthService_SignOut_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq SignOutRequest metadata runtime.ServerMetadata ) if req.Body != nil { _, _ = io.Copy(io.Discard, req.Body) } msg, err := client.SignOut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_AuthService_SignOut_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq SignOutRequest metadata runtime.ServerMetadata ) msg, err := server.SignOut(ctx, &protoReq) return msg, metadata, err } func request_AuthService_RefreshToken_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq RefreshTokenRequest 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.RefreshToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_AuthService_RefreshToken_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq RefreshTokenRequest 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.RefreshToken(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_GetCurrentUser_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/GetCurrentUser", runtime.WithHTTPPathPattern("/api/v1/auth/me")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_AuthService_GetCurrentUser_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_GetCurrentUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPost, pattern_AuthService_SignIn_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/SignIn", runtime.WithHTTPPathPattern("/api/v1/auth/signin")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_AuthService_SignIn_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_SignIn_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPost, pattern_AuthService_SignOut_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/SignOut", runtime.WithHTTPPathPattern("/api/v1/auth/signout")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_AuthService_SignOut_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_SignOut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPost, pattern_AuthService_RefreshToken_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/RefreshToken", runtime.WithHTTPPathPattern("/api/v1/auth/refresh")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_AuthService_RefreshToken_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_RefreshToken_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_GetCurrentUser_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/GetCurrentUser", runtime.WithHTTPPathPattern("/api/v1/auth/me")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_AuthService_GetCurrentUser_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_GetCurrentUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPost, pattern_AuthService_SignIn_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/SignIn", runtime.WithHTTPPathPattern("/api/v1/auth/signin")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_AuthService_SignIn_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_SignIn_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPost, pattern_AuthService_SignOut_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/SignOut", runtime.WithHTTPPathPattern("/api/v1/auth/signout")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_AuthService_SignOut_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_SignOut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPost, pattern_AuthService_RefreshToken_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/RefreshToken", runtime.WithHTTPPathPattern("/api/v1/auth/refresh")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_AuthService_RefreshToken_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_RefreshToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) return nil } var ( pattern_AuthService_GetCurrentUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "me"}, "")) pattern_AuthService_SignIn_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "signin"}, "")) pattern_AuthService_SignOut_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "signout"}, "")) pattern_AuthService_RefreshToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "refresh"}, "")) ) var ( forward_AuthService_GetCurrentUser_0 = runtime.ForwardResponseMessage forward_AuthService_SignIn_0 = runtime.ForwardResponseMessage forward_AuthService_SignOut_0 = runtime.ForwardResponseMessage forward_AuthService_RefreshToken_0 = runtime.ForwardResponseMessage ) ================================================ FILE: proto/gen/api/v1/auth_service_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.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_GetCurrentUser_FullMethodName = "/memos.api.v1.AuthService/GetCurrentUser" AuthService_SignIn_FullMethodName = "/memos.api.v1.AuthService/SignIn" AuthService_SignOut_FullMethodName = "/memos.api.v1.AuthService/SignOut" AuthService_RefreshToken_FullMethodName = "/memos.api.v1.AuthService/RefreshToken" ) // 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 { // GetCurrentUser returns the authenticated user's information. // Validates the access token and returns user details. // Similar to OIDC's /userinfo endpoint. GetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error) // SignIn authenticates a user with credentials and returns tokens. // On success, returns an access token and sets a refresh token cookie. // Supports password-based and SSO authentication methods. SignIn(ctx context.Context, in *SignInRequest, opts ...grpc.CallOption) (*SignInResponse, error) // SignOut terminates the user's authentication. // Revokes the refresh token and clears the authentication cookie. SignOut(ctx context.Context, in *SignOutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // RefreshToken exchanges a valid refresh token for a new access token. // The refresh token is read from the HttpOnly cookie. // Returns a new short-lived access token. RefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error) } type authServiceClient struct { cc grpc.ClientConnInterface } func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient { return &authServiceClient{cc} } func (c *authServiceClient) GetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetCurrentUserResponse) err := c.cc.Invoke(ctx, AuthService_GetCurrentUser_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *authServiceClient) SignIn(ctx context.Context, in *SignInRequest, opts ...grpc.CallOption) (*SignInResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SignInResponse) err := c.cc.Invoke(ctx, AuthService_SignIn_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *authServiceClient) SignOut(ctx context.Context, in *SignOutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, AuthService_SignOut_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *authServiceClient) RefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(RefreshTokenResponse) err := c.cc.Invoke(ctx, AuthService_RefreshToken_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 { // GetCurrentUser returns the authenticated user's information. // Validates the access token and returns user details. // Similar to OIDC's /userinfo endpoint. GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) // SignIn authenticates a user with credentials and returns tokens. // On success, returns an access token and sets a refresh token cookie. // Supports password-based and SSO authentication methods. SignIn(context.Context, *SignInRequest) (*SignInResponse, error) // SignOut terminates the user's authentication. // Revokes the refresh token and clears the authentication cookie. SignOut(context.Context, *SignOutRequest) (*emptypb.Empty, error) // RefreshToken exchanges a valid refresh token for a new access token. // The refresh token is read from the HttpOnly cookie. // Returns a new short-lived access token. RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, 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) GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetCurrentUser not implemented") } func (UnimplementedAuthServiceServer) SignIn(context.Context, *SignInRequest) (*SignInResponse, error) { return nil, status.Error(codes.Unimplemented, "method SignIn not implemented") } func (UnimplementedAuthServiceServer) SignOut(context.Context, *SignOutRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method SignOut not implemented") } func (UnimplementedAuthServiceServer) RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error) { return nil, status.Error(codes.Unimplemented, "method RefreshToken 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 panics, 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_GetCurrentUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetCurrentUserRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(AuthServiceServer).GetCurrentUser(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: AuthService_GetCurrentUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(AuthServiceServer).GetCurrentUser(ctx, req.(*GetCurrentUserRequest)) } return interceptor(ctx, in, info, handler) } func _AuthService_SignIn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SignInRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(AuthServiceServer).SignIn(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: AuthService_SignIn_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(AuthServiceServer).SignIn(ctx, req.(*SignInRequest)) } return interceptor(ctx, in, info, handler) } func _AuthService_SignOut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SignOutRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(AuthServiceServer).SignOut(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: AuthService_SignOut_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(AuthServiceServer).SignOut(ctx, req.(*SignOutRequest)) } return interceptor(ctx, in, info, handler) } func _AuthService_RefreshToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RefreshTokenRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(AuthServiceServer).RefreshToken(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: AuthService_RefreshToken_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(AuthServiceServer).RefreshToken(ctx, req.(*RefreshTokenRequest)) } 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: "GetCurrentUser", Handler: _AuthService_GetCurrentUser_Handler, }, { MethodName: "SignIn", Handler: _AuthService_SignIn_Handler, }, { MethodName: "SignOut", Handler: _AuthService_SignOut_Handler, }, { MethodName: "RefreshToken", Handler: _AuthService_RefreshToken_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "api/v1/auth_service.proto", } ================================================ FILE: proto/gen/api/v1/common.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // 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 } ================================================ FILE: proto/gen/api/v1/idp_service.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // 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: identity-providers/{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: identity-providers/{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: identity-providers/{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\"\x8c\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:g\xeaAd\n" + "\x1dmemos.api.v1/IdentityProvider\x12\x18identity-providers/{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\xe7\x06\n" + "\x17IdentityProviderService\x12\x94\x01\n" + "\x15ListIdentityProviders\x12*.memos.api.v1.ListIdentityProvidersRequest\x1a+.memos.api.v1.ListIdentityProvidersResponse\"\"\x82\xd3\xe4\x93\x02\x1c\x12\x1a/api/v1/identity-providers\x12\x93\x01\n" + "\x13GetIdentityProvider\x12(.memos.api.v1.GetIdentityProviderRequest\x1a\x1e.memos.api.v1.IdentityProvider\"2\xdaA\x04name\x82\xd3\xe4\x93\x02%\x12#/api/v1/{name=identity-providers/*}\x12\xb0\x01\n" + "\x16CreateIdentityProvider\x12+.memos.api.v1.CreateIdentityProviderRequest\x1a\x1e.memos.api.v1.IdentityProvider\"I\xdaA\x11identity_provider\x82\xd3\xe4\x93\x02/:\x11identity_provider\"\x1a/api/v1/identity-providers\x12\xd7\x01\n" + "\x16UpdateIdentityProvider\x12+.memos.api.v1.UpdateIdentityProviderRequest\x1a\x1e.memos.api.v1.IdentityProvider\"p\xdaA\x1didentity_provider,update_mask\x82\xd3\xe4\x93\x02J:\x11identity_provider25/api/v1/{identity_provider.name=identity-providers/*}\x12\x91\x01\n" + "\x16DeleteIdentityProvider\x12+.memos.api.v1.DeleteIdentityProviderRequest\x1a\x16.google.protobuf.Empty\"2\xdaA\x04name\x82\xd3\xe4\x93\x02%*#/api/v1/{name=identity-providers/*}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 } ================================================ FILE: proto/gen/api/v1/idp_service.pb.gw.go ================================================ // 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/identity-providers")) 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=identity-providers/*}")) 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/identity-providers")) 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=identity-providers/*}")) 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=identity-providers/*}")) 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/identity-providers")) 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=identity-providers/*}")) 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/identity-providers")) 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=identity-providers/*}")) 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=identity-providers/*}")) 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", "identity-providers"}, "")) 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", "identity-providers", "name"}, "")) pattern_IdentityProviderService_CreateIdentityProvider_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "identity-providers"}, "")) 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", "identity-providers", "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", "identity-providers", "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 ) ================================================ FILE: proto/gen/api/v1/idp_service_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.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.Error(codes.Unimplemented, "method ListIdentityProviders not implemented") } func (UnimplementedIdentityProviderServiceServer) GetIdentityProvider(context.Context, *GetIdentityProviderRequest) (*IdentityProvider, error) { return nil, status.Error(codes.Unimplemented, "method GetIdentityProvider not implemented") } func (UnimplementedIdentityProviderServiceServer) CreateIdentityProvider(context.Context, *CreateIdentityProviderRequest) (*IdentityProvider, error) { return nil, status.Error(codes.Unimplemented, "method CreateIdentityProvider not implemented") } func (UnimplementedIdentityProviderServiceServer) UpdateIdentityProvider(context.Context, *UpdateIdentityProviderRequest) (*IdentityProvider, error) { return nil, status.Error(codes.Unimplemented, "method UpdateIdentityProvider not implemented") } func (UnimplementedIdentityProviderServiceServer) DeleteIdentityProvider(context.Context, *DeleteIdentityProviderRequest) (*emptypb.Empty, error) { return nil, status.Error(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 panics, 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", } ================================================ FILE: proto/gen/api/v1/instance_service.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc (unknown) // source: api/v1/instance_service.proto package apiv1 import ( _ "google.golang.org/genproto/googleapis/api/annotations" color "google.golang.org/genproto/googleapis/type/color" 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) ) // Enumeration of instance setting keys. type InstanceSetting_Key int32 const ( InstanceSetting_KEY_UNSPECIFIED InstanceSetting_Key = 0 // GENERAL is the key for general settings. InstanceSetting_GENERAL InstanceSetting_Key = 1 // STORAGE is the key for storage settings. InstanceSetting_STORAGE InstanceSetting_Key = 2 // MEMO_RELATED is the key for memo related settings. InstanceSetting_MEMO_RELATED InstanceSetting_Key = 3 // TAGS is the key for tag metadata. InstanceSetting_TAGS InstanceSetting_Key = 4 // NOTIFICATION is the key for notification transport settings. InstanceSetting_NOTIFICATION InstanceSetting_Key = 5 ) // Enum value maps for InstanceSetting_Key. var ( InstanceSetting_Key_name = map[int32]string{ 0: "KEY_UNSPECIFIED", 1: "GENERAL", 2: "STORAGE", 3: "MEMO_RELATED", 4: "TAGS", 5: "NOTIFICATION", } InstanceSetting_Key_value = map[string]int32{ "KEY_UNSPECIFIED": 0, "GENERAL": 1, "STORAGE": 2, "MEMO_RELATED": 3, "TAGS": 4, "NOTIFICATION": 5, } ) func (x InstanceSetting_Key) Enum() *InstanceSetting_Key { p := new(InstanceSetting_Key) *p = x return p } func (x InstanceSetting_Key) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (InstanceSetting_Key) Descriptor() protoreflect.EnumDescriptor { return file_api_v1_instance_service_proto_enumTypes[0].Descriptor() } func (InstanceSetting_Key) Type() protoreflect.EnumType { return &file_api_v1_instance_service_proto_enumTypes[0] } func (x InstanceSetting_Key) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use InstanceSetting_Key.Descriptor instead. func (InstanceSetting_Key) EnumDescriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 0} } // Storage type enumeration for different storage backends. type InstanceSetting_StorageSetting_StorageType int32 const ( InstanceSetting_StorageSetting_STORAGE_TYPE_UNSPECIFIED InstanceSetting_StorageSetting_StorageType = 0 // DATABASE is the database storage type. InstanceSetting_StorageSetting_DATABASE InstanceSetting_StorageSetting_StorageType = 1 // LOCAL is the local storage type. InstanceSetting_StorageSetting_LOCAL InstanceSetting_StorageSetting_StorageType = 2 // S3 is the S3 storage type. InstanceSetting_StorageSetting_S3 InstanceSetting_StorageSetting_StorageType = 3 ) // Enum value maps for InstanceSetting_StorageSetting_StorageType. var ( InstanceSetting_StorageSetting_StorageType_name = map[int32]string{ 0: "STORAGE_TYPE_UNSPECIFIED", 1: "DATABASE", 2: "LOCAL", 3: "S3", } InstanceSetting_StorageSetting_StorageType_value = map[string]int32{ "STORAGE_TYPE_UNSPECIFIED": 0, "DATABASE": 1, "LOCAL": 2, "S3": 3, } ) func (x InstanceSetting_StorageSetting_StorageType) Enum() *InstanceSetting_StorageSetting_StorageType { p := new(InstanceSetting_StorageSetting_StorageType) *p = x return p } func (x InstanceSetting_StorageSetting_StorageType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (InstanceSetting_StorageSetting_StorageType) Descriptor() protoreflect.EnumDescriptor { return file_api_v1_instance_service_proto_enumTypes[1].Descriptor() } func (InstanceSetting_StorageSetting_StorageType) Type() protoreflect.EnumType { return &file_api_v1_instance_service_proto_enumTypes[1] } func (x InstanceSetting_StorageSetting_StorageType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use InstanceSetting_StorageSetting_StorageType.Descriptor instead. func (InstanceSetting_StorageSetting_StorageType) EnumDescriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 1, 0} } // Instance profile message containing basic instance information. type InstanceProfile struct { state protoimpl.MessageState `protogen:"open.v1"` // Version is the current version of instance. Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` // Demo indicates if the instance is in demo mode. Demo bool `protobuf:"varint,3,opt,name=demo,proto3" json:"demo,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"` // The first administrator who set up this instance. // When null, instance requires initial setup (creating the first admin account). Admin *User `protobuf:"bytes,7,opt,name=admin,proto3" json:"admin,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceProfile) Reset() { *x = InstanceProfile{} mi := &file_api_v1_instance_service_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceProfile) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceProfile) ProtoMessage() {} func (x *InstanceProfile) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceProfile.ProtoReflect.Descriptor instead. func (*InstanceProfile) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{0} } func (x *InstanceProfile) GetVersion() string { if x != nil { return x.Version } return "" } func (x *InstanceProfile) GetDemo() bool { if x != nil { return x.Demo } return false } func (x *InstanceProfile) GetInstanceUrl() string { if x != nil { return x.InstanceUrl } return "" } func (x *InstanceProfile) GetAdmin() *User { if x != nil { return x.Admin } return nil } // Request for instance profile. type GetInstanceProfileRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetInstanceProfileRequest) Reset() { *x = GetInstanceProfileRequest{} mi := &file_api_v1_instance_service_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GetInstanceProfileRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetInstanceProfileRequest) ProtoMessage() {} func (x *GetInstanceProfileRequest) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 GetInstanceProfileRequest.ProtoReflect.Descriptor instead. func (*GetInstanceProfileRequest) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{1} } // An instance setting resource. type InstanceSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // The name of the instance setting. // Format: instance/settings/{setting} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Types that are valid to be assigned to Value: // // *InstanceSetting_GeneralSetting_ // *InstanceSetting_StorageSetting_ // *InstanceSetting_MemoRelatedSetting_ // *InstanceSetting_TagsSetting_ // *InstanceSetting_NotificationSetting_ Value isInstanceSetting_Value `protobuf_oneof:"value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceSetting) Reset() { *x = InstanceSetting{} mi := &file_api_v1_instance_service_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting) ProtoMessage() {} func (x *InstanceSetting) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceSetting.ProtoReflect.Descriptor instead. func (*InstanceSetting) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2} } func (x *InstanceSetting) GetName() string { if x != nil { return x.Name } return "" } func (x *InstanceSetting) GetValue() isInstanceSetting_Value { if x != nil { return x.Value } return nil } func (x *InstanceSetting) GetGeneralSetting() *InstanceSetting_GeneralSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_GeneralSetting_); ok { return x.GeneralSetting } } return nil } func (x *InstanceSetting) GetStorageSetting() *InstanceSetting_StorageSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_StorageSetting_); ok { return x.StorageSetting } } return nil } func (x *InstanceSetting) GetMemoRelatedSetting() *InstanceSetting_MemoRelatedSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_MemoRelatedSetting_); ok { return x.MemoRelatedSetting } } return nil } func (x *InstanceSetting) GetTagsSetting() *InstanceSetting_TagsSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_TagsSetting_); ok { return x.TagsSetting } } return nil } func (x *InstanceSetting) GetNotificationSetting() *InstanceSetting_NotificationSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_NotificationSetting_); ok { return x.NotificationSetting } } return nil } type isInstanceSetting_Value interface { isInstanceSetting_Value() } type InstanceSetting_GeneralSetting_ struct { GeneralSetting *InstanceSetting_GeneralSetting `protobuf:"bytes,2,opt,name=general_setting,json=generalSetting,proto3,oneof"` } type InstanceSetting_StorageSetting_ struct { StorageSetting *InstanceSetting_StorageSetting `protobuf:"bytes,3,opt,name=storage_setting,json=storageSetting,proto3,oneof"` } type InstanceSetting_MemoRelatedSetting_ struct { MemoRelatedSetting *InstanceSetting_MemoRelatedSetting `protobuf:"bytes,4,opt,name=memo_related_setting,json=memoRelatedSetting,proto3,oneof"` } type InstanceSetting_TagsSetting_ struct { TagsSetting *InstanceSetting_TagsSetting `protobuf:"bytes,5,opt,name=tags_setting,json=tagsSetting,proto3,oneof"` } type InstanceSetting_NotificationSetting_ struct { NotificationSetting *InstanceSetting_NotificationSetting `protobuf:"bytes,6,opt,name=notification_setting,json=notificationSetting,proto3,oneof"` } func (*InstanceSetting_GeneralSetting_) isInstanceSetting_Value() {} func (*InstanceSetting_StorageSetting_) isInstanceSetting_Value() {} func (*InstanceSetting_MemoRelatedSetting_) isInstanceSetting_Value() {} func (*InstanceSetting_TagsSetting_) isInstanceSetting_Value() {} func (*InstanceSetting_NotificationSetting_) isInstanceSetting_Value() {} // Request message for GetInstanceSetting method. type GetInstanceSettingRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The resource name of the instance setting. // Format: instance/settings/{setting} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetInstanceSettingRequest) Reset() { *x = GetInstanceSettingRequest{} mi := &file_api_v1_instance_service_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GetInstanceSettingRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetInstanceSettingRequest) ProtoMessage() {} func (x *GetInstanceSettingRequest) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 GetInstanceSettingRequest.ProtoReflect.Descriptor instead. func (*GetInstanceSettingRequest) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{3} } func (x *GetInstanceSettingRequest) GetName() string { if x != nil { return x.Name } return "" } // Request message for UpdateInstanceSetting method. type UpdateInstanceSettingRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The instance setting resource which replaces the resource on the server. Setting *InstanceSetting `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 *UpdateInstanceSettingRequest) Reset() { *x = UpdateInstanceSettingRequest{} mi := &file_api_v1_instance_service_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *UpdateInstanceSettingRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*UpdateInstanceSettingRequest) ProtoMessage() {} func (x *UpdateInstanceSettingRequest) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 UpdateInstanceSettingRequest.ProtoReflect.Descriptor instead. func (*UpdateInstanceSettingRequest) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{4} } func (x *UpdateInstanceSettingRequest) GetSetting() *InstanceSetting { if x != nil { return x.Setting } return nil } func (x *UpdateInstanceSettingRequest) GetUpdateMask() *fieldmaskpb.FieldMask { if x != nil { return x.UpdateMask } return nil } // General instance settings configuration. type InstanceSetting_GeneralSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // 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 *InstanceSetting_GeneralSetting_CustomProfile `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 *InstanceSetting_GeneralSetting) Reset() { *x = InstanceSetting_GeneralSetting{} mi := &file_api_v1_instance_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting_GeneralSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting_GeneralSetting) ProtoMessage() {} func (x *InstanceSetting_GeneralSetting) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceSetting_GeneralSetting.ProtoReflect.Descriptor instead. func (*InstanceSetting_GeneralSetting) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 0} } func (x *InstanceSetting_GeneralSetting) GetDisallowUserRegistration() bool { if x != nil { return x.DisallowUserRegistration } return false } func (x *InstanceSetting_GeneralSetting) GetDisallowPasswordAuth() bool { if x != nil { return x.DisallowPasswordAuth } return false } func (x *InstanceSetting_GeneralSetting) GetAdditionalScript() string { if x != nil { return x.AdditionalScript } return "" } func (x *InstanceSetting_GeneralSetting) GetAdditionalStyle() string { if x != nil { return x.AdditionalStyle } return "" } func (x *InstanceSetting_GeneralSetting) GetCustomProfile() *InstanceSetting_GeneralSetting_CustomProfile { if x != nil { return x.CustomProfile } return nil } func (x *InstanceSetting_GeneralSetting) GetWeekStartDayOffset() int32 { if x != nil { return x.WeekStartDayOffset } return 0 } func (x *InstanceSetting_GeneralSetting) GetDisallowChangeUsername() bool { if x != nil { return x.DisallowChangeUsername } return false } func (x *InstanceSetting_GeneralSetting) GetDisallowChangeNickname() bool { if x != nil { return x.DisallowChangeNickname } return false } // Storage configuration settings for instance attachments. type InstanceSetting_StorageSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // storage_type is the storage type. StorageType InstanceSetting_StorageSetting_StorageType `protobuf:"varint,1,opt,name=storage_type,json=storageType,proto3,enum=memos.api.v1.InstanceSetting_StorageSetting_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 *InstanceSetting_StorageSetting_S3Config `protobuf:"bytes,4,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceSetting_StorageSetting) Reset() { *x = InstanceSetting_StorageSetting{} mi := &file_api_v1_instance_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting_StorageSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting_StorageSetting) ProtoMessage() {} func (x *InstanceSetting_StorageSetting) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceSetting_StorageSetting.ProtoReflect.Descriptor instead. func (*InstanceSetting_StorageSetting) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 1} } func (x *InstanceSetting_StorageSetting) GetStorageType() InstanceSetting_StorageSetting_StorageType { if x != nil { return x.StorageType } return InstanceSetting_StorageSetting_STORAGE_TYPE_UNSPECIFIED } func (x *InstanceSetting_StorageSetting) GetFilepathTemplate() string { if x != nil { return x.FilepathTemplate } return "" } func (x *InstanceSetting_StorageSetting) GetUploadSizeLimitMb() int64 { if x != nil { return x.UploadSizeLimitMb } return 0 } func (x *InstanceSetting_StorageSetting) GetS3Config() *InstanceSetting_StorageSetting_S3Config { if x != nil { return x.S3Config } return nil } // Memo-related instance settings and policies. type InstanceSetting_MemoRelatedSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // 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"` // reactions is the list of reactions. Reactions []string `protobuf:"bytes,7,rep,name=reactions,proto3" json:"reactions,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceSetting_MemoRelatedSetting) Reset() { *x = InstanceSetting_MemoRelatedSetting{} mi := &file_api_v1_instance_service_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting_MemoRelatedSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting_MemoRelatedSetting) ProtoMessage() {} func (x *InstanceSetting_MemoRelatedSetting) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceSetting_MemoRelatedSetting.ProtoReflect.Descriptor instead. func (*InstanceSetting_MemoRelatedSetting) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 2} } func (x *InstanceSetting_MemoRelatedSetting) GetDisplayWithUpdateTime() bool { if x != nil { return x.DisplayWithUpdateTime } return false } func (x *InstanceSetting_MemoRelatedSetting) GetContentLengthLimit() int32 { if x != nil { return x.ContentLengthLimit } return 0 } func (x *InstanceSetting_MemoRelatedSetting) GetEnableDoubleClickEdit() bool { if x != nil { return x.EnableDoubleClickEdit } return false } func (x *InstanceSetting_MemoRelatedSetting) GetReactions() []string { if x != nil { return x.Reactions } return nil } // Metadata for a tag. type InstanceSetting_TagMetadata struct { state protoimpl.MessageState `protogen:"open.v1"` // Background color for the tag label. BackgroundColor *color.Color `protobuf:"bytes,1,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceSetting_TagMetadata) Reset() { *x = InstanceSetting_TagMetadata{} mi := &file_api_v1_instance_service_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting_TagMetadata) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting_TagMetadata) ProtoMessage() {} func (x *InstanceSetting_TagMetadata) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceSetting_TagMetadata.ProtoReflect.Descriptor instead. func (*InstanceSetting_TagMetadata) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 3} } func (x *InstanceSetting_TagMetadata) GetBackgroundColor() *color.Color { if x != nil { return x.BackgroundColor } return nil } // Tag metadata configuration. type InstanceSetting_TagsSetting struct { state protoimpl.MessageState `protogen:"open.v1"` Tags map[string]*InstanceSetting_TagMetadata `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceSetting_TagsSetting) Reset() { *x = InstanceSetting_TagsSetting{} mi := &file_api_v1_instance_service_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting_TagsSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting_TagsSetting) ProtoMessage() {} func (x *InstanceSetting_TagsSetting) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceSetting_TagsSetting.ProtoReflect.Descriptor instead. func (*InstanceSetting_TagsSetting) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 4} } func (x *InstanceSetting_TagsSetting) GetTags() map[string]*InstanceSetting_TagMetadata { if x != nil { return x.Tags } return nil } // Notification transport configuration. type InstanceSetting_NotificationSetting struct { state protoimpl.MessageState `protogen:"open.v1"` Email *InstanceSetting_NotificationSetting_EmailSetting `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceSetting_NotificationSetting) Reset() { *x = InstanceSetting_NotificationSetting{} mi := &file_api_v1_instance_service_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting_NotificationSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting_NotificationSetting) ProtoMessage() {} func (x *InstanceSetting_NotificationSetting) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceSetting_NotificationSetting.ProtoReflect.Descriptor instead. func (*InstanceSetting_NotificationSetting) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 5} } func (x *InstanceSetting_NotificationSetting) GetEmail() *InstanceSetting_NotificationSetting_EmailSetting { if x != nil { return x.Email } return nil } // Custom profile configuration for instance branding. type InstanceSetting_GeneralSetting_CustomProfile 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"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceSetting_GeneralSetting_CustomProfile) Reset() { *x = InstanceSetting_GeneralSetting_CustomProfile{} mi := &file_api_v1_instance_service_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting_GeneralSetting_CustomProfile) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting_GeneralSetting_CustomProfile) ProtoMessage() {} func (x *InstanceSetting_GeneralSetting_CustomProfile) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceSetting_GeneralSetting_CustomProfile.ProtoReflect.Descriptor instead. func (*InstanceSetting_GeneralSetting_CustomProfile) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 0, 0} } func (x *InstanceSetting_GeneralSetting_CustomProfile) GetTitle() string { if x != nil { return x.Title } return "" } func (x *InstanceSetting_GeneralSetting_CustomProfile) GetDescription() string { if x != nil { return x.Description } return "" } func (x *InstanceSetting_GeneralSetting_CustomProfile) GetLogoUrl() string { if x != nil { return x.LogoUrl } return "" } // S3 configuration for cloud storage backend. // Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/ type InstanceSetting_StorageSetting_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 *InstanceSetting_StorageSetting_S3Config) Reset() { *x = InstanceSetting_StorageSetting_S3Config{} mi := &file_api_v1_instance_service_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting_StorageSetting_S3Config) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting_StorageSetting_S3Config) ProtoMessage() {} func (x *InstanceSetting_StorageSetting_S3Config) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceSetting_StorageSetting_S3Config.ProtoReflect.Descriptor instead. func (*InstanceSetting_StorageSetting_S3Config) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 1, 0} } func (x *InstanceSetting_StorageSetting_S3Config) GetAccessKeyId() string { if x != nil { return x.AccessKeyId } return "" } func (x *InstanceSetting_StorageSetting_S3Config) GetAccessKeySecret() string { if x != nil { return x.AccessKeySecret } return "" } func (x *InstanceSetting_StorageSetting_S3Config) GetEndpoint() string { if x != nil { return x.Endpoint } return "" } func (x *InstanceSetting_StorageSetting_S3Config) GetRegion() string { if x != nil { return x.Region } return "" } func (x *InstanceSetting_StorageSetting_S3Config) GetBucket() string { if x != nil { return x.Bucket } return "" } func (x *InstanceSetting_StorageSetting_S3Config) GetUsePathStyle() bool { if x != nil { return x.UsePathStyle } return false } // Email delivery configuration for notifications. type InstanceSetting_NotificationSetting_EmailSetting struct { state protoimpl.MessageState `protogen:"open.v1"` Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` SmtpHost string `protobuf:"bytes,2,opt,name=smtp_host,json=smtpHost,proto3" json:"smtp_host,omitempty"` SmtpPort int32 `protobuf:"varint,3,opt,name=smtp_port,json=smtpPort,proto3" json:"smtp_port,omitempty"` SmtpUsername string `protobuf:"bytes,4,opt,name=smtp_username,json=smtpUsername,proto3" json:"smtp_username,omitempty"` SmtpPassword string `protobuf:"bytes,5,opt,name=smtp_password,json=smtpPassword,proto3" json:"smtp_password,omitempty"` FromEmail string `protobuf:"bytes,6,opt,name=from_email,json=fromEmail,proto3" json:"from_email,omitempty"` FromName string `protobuf:"bytes,7,opt,name=from_name,json=fromName,proto3" json:"from_name,omitempty"` ReplyTo string `protobuf:"bytes,8,opt,name=reply_to,json=replyTo,proto3" json:"reply_to,omitempty"` UseTls bool `protobuf:"varint,9,opt,name=use_tls,json=useTls,proto3" json:"use_tls,omitempty"` UseSsl bool `protobuf:"varint,10,opt,name=use_ssl,json=useSsl,proto3" json:"use_ssl,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceSetting_NotificationSetting_EmailSetting) Reset() { *x = InstanceSetting_NotificationSetting_EmailSetting{} mi := &file_api_v1_instance_service_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting_NotificationSetting_EmailSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting_NotificationSetting_EmailSetting) ProtoMessage() {} func (x *InstanceSetting_NotificationSetting_EmailSetting) ProtoReflect() protoreflect.Message { mi := &file_api_v1_instance_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 InstanceSetting_NotificationSetting_EmailSetting.ProtoReflect.Descriptor instead. func (*InstanceSetting_NotificationSetting_EmailSetting) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 5, 0} } func (x *InstanceSetting_NotificationSetting_EmailSetting) GetEnabled() bool { if x != nil { return x.Enabled } return false } func (x *InstanceSetting_NotificationSetting_EmailSetting) GetSmtpHost() string { if x != nil { return x.SmtpHost } return "" } func (x *InstanceSetting_NotificationSetting_EmailSetting) GetSmtpPort() int32 { if x != nil { return x.SmtpPort } return 0 } func (x *InstanceSetting_NotificationSetting_EmailSetting) GetSmtpUsername() string { if x != nil { return x.SmtpUsername } return "" } func (x *InstanceSetting_NotificationSetting_EmailSetting) GetSmtpPassword() string { if x != nil { return x.SmtpPassword } return "" } func (x *InstanceSetting_NotificationSetting_EmailSetting) GetFromEmail() string { if x != nil { return x.FromEmail } return "" } func (x *InstanceSetting_NotificationSetting_EmailSetting) GetFromName() string { if x != nil { return x.FromName } return "" } func (x *InstanceSetting_NotificationSetting_EmailSetting) GetReplyTo() string { if x != nil { return x.ReplyTo } return "" } func (x *InstanceSetting_NotificationSetting_EmailSetting) GetUseTls() bool { if x != nil { return x.UseTls } return false } func (x *InstanceSetting_NotificationSetting_EmailSetting) GetUseSsl() bool { if x != nil { return x.UseSsl } return false } var File_api_v1_instance_service_proto protoreflect.FileDescriptor const file_api_v1_instance_service_proto_rawDesc = "" + "\n" + "\x1dapi/v1/instance_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\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\x1a\x17google/type/color.proto\"\x8c\x01\n" + "\x0fInstanceProfile\x12\x18\n" + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + "\x04demo\x18\x03 \x01(\bR\x04demo\x12!\n" + "\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\x12(\n" + "\x05admin\x18\a \x01(\v2\x12.memos.api.v1.UserR\x05admin\"\x1b\n" + "\x19GetInstanceProfileRequest\"\xe0\x15\n" + "\x0fInstanceSetting\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12W\n" + "\x0fgeneral_setting\x18\x02 \x01(\v2,.memos.api.v1.InstanceSetting.GeneralSettingH\x00R\x0egeneralSetting\x12W\n" + "\x0fstorage_setting\x18\x03 \x01(\v2,.memos.api.v1.InstanceSetting.StorageSettingH\x00R\x0estorageSetting\x12d\n" + "\x14memo_related_setting\x18\x04 \x01(\v20.memos.api.v1.InstanceSetting.MemoRelatedSettingH\x00R\x12memoRelatedSetting\x12N\n" + "\ftags_setting\x18\x05 \x01(\v2).memos.api.v1.InstanceSetting.TagsSettingH\x00R\vtagsSetting\x12f\n" + "\x14notification_setting\x18\x06 \x01(\v21.memos.api.v1.InstanceSetting.NotificationSettingH\x00R\x13notificationSetting\x1a\xca\x04\n" + "\x0eGeneralSetting\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\x12a\n" + "\x0ecustom_profile\x18\x06 \x01(\v2:.memos.api.v1.InstanceSetting.GeneralSetting.CustomProfileR\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\x1ab\n" + "\rCustomProfile\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\x1a\xbc\x04\n" + "\x0eStorageSetting\x12[\n" + "\fstorage_type\x18\x01 \x01(\x0e28.memos.api.v1.InstanceSetting.StorageSetting.StorageTypeR\vstorageType\x12+\n" + "\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" + "\x14upload_size_limit_mb\x18\x03 \x01(\x03R\x11uploadSizeLimitMb\x12R\n" + "\ts3_config\x18\x04 \x01(\v25.memos.api.v1.InstanceSetting.StorageSetting.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\x1a\xd6\x01\n" + "\x12MemoRelatedSetting\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\x1c\n" + "\treactions\x18\a \x03(\tR\treactions\x1aL\n" + "\vTagMetadata\x12=\n" + "\x10background_color\x18\x01 \x01(\v2\x12.google.type.ColorR\x0fbackgroundColor\x1a\xba\x01\n" + "\vTagsSetting\x12G\n" + "\x04tags\x18\x01 \x03(\v23.memos.api.v1.InstanceSetting.TagsSetting.TagsEntryR\x04tags\x1ab\n" + "\tTagsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12?\n" + "\x05value\x18\x02 \x01(\v2).memos.api.v1.InstanceSetting.TagMetadataR\x05value:\x028\x01\x1a\xa3\x03\n" + "\x13NotificationSetting\x12T\n" + "\x05email\x18\x01 \x01(\v2>.memos.api.v1.InstanceSetting.NotificationSetting.EmailSettingR\x05email\x1a\xb5\x02\n" + "\fEmailSetting\x12\x18\n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x12\x1b\n" + "\tsmtp_host\x18\x02 \x01(\tR\bsmtpHost\x12\x1b\n" + "\tsmtp_port\x18\x03 \x01(\x05R\bsmtpPort\x12#\n" + "\rsmtp_username\x18\x04 \x01(\tR\fsmtpUsername\x12#\n" + "\rsmtp_password\x18\x05 \x01(\tR\fsmtpPassword\x12\x1d\n" + "\n" + "from_email\x18\x06 \x01(\tR\tfromEmail\x12\x1b\n" + "\tfrom_name\x18\a \x01(\tR\bfromName\x12\x19\n" + "\breply_to\x18\b \x01(\tR\areplyTo\x12\x17\n" + "\ause_tls\x18\t \x01(\bR\x06useTls\x12\x17\n" + "\ause_ssl\x18\n" + " \x01(\bR\x06useSsl\"b\n" + "\x03Key\x12\x13\n" + "\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" + "\aGENERAL\x10\x01\x12\v\n" + "\aSTORAGE\x10\x02\x12\x10\n" + "\fMEMO_RELATED\x10\x03\x12\b\n" + "\x04TAGS\x10\x04\x12\x10\n" + "\fNOTIFICATION\x10\x05:a\xeaA^\n" + "\x1cmemos.api.v1/InstanceSetting\x12\x1binstance/settings/{setting}*\x10instanceSettings2\x0finstanceSettingB\a\n" + "\x05value\"U\n" + "\x19GetInstanceSettingRequest\x128\n" + "\x04name\x18\x01 \x01(\tB$\xe0A\x02\xfaA\x1e\n" + "\x1cmemos.api.v1/InstanceSettingR\x04name\"\x9e\x01\n" + "\x1cUpdateInstanceSettingRequest\x12<\n" + "\asetting\x18\x01 \x01(\v2\x1d.memos.api.v1.InstanceSettingB\x03\xe0A\x02R\asetting\x12@\n" + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x01R\n" + "updateMask2\xdb\x03\n" + "\x0fInstanceService\x12~\n" + "\x12GetInstanceProfile\x12'.memos.api.v1.GetInstanceProfileRequest\x1a\x1d.memos.api.v1.InstanceProfile\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/api/v1/instance/profile\x12\x8f\x01\n" + "\x12GetInstanceSetting\x12'.memos.api.v1.GetInstanceSettingRequest\x1a\x1d.memos.api.v1.InstanceSetting\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$\x12\"/api/v1/{name=instance/settings/*}\x12\xb5\x01\n" + "\x15UpdateInstanceSetting\x12*.memos.api.v1.UpdateInstanceSettingRequest\x1a\x1d.memos.api.v1.InstanceSetting\"Q\xdaA\x13setting,update_mask\x82\xd3\xe4\x93\x025:\asetting2*/api/v1/{setting.name=instance/settings/*}B\xac\x01\n" + "\x10com.memos.api.v1B\x14InstanceServiceProtoP\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_instance_service_proto_rawDescOnce sync.Once file_api_v1_instance_service_proto_rawDescData []byte ) func file_api_v1_instance_service_proto_rawDescGZIP() []byte { file_api_v1_instance_service_proto_rawDescOnce.Do(func() { file_api_v1_instance_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_instance_service_proto_rawDesc), len(file_api_v1_instance_service_proto_rawDesc))) }) return file_api_v1_instance_service_proto_rawDescData } var file_api_v1_instance_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_api_v1_instance_service_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_api_v1_instance_service_proto_goTypes = []any{ (InstanceSetting_Key)(0), // 0: memos.api.v1.InstanceSetting.Key (InstanceSetting_StorageSetting_StorageType)(0), // 1: memos.api.v1.InstanceSetting.StorageSetting.StorageType (*InstanceProfile)(nil), // 2: memos.api.v1.InstanceProfile (*GetInstanceProfileRequest)(nil), // 3: memos.api.v1.GetInstanceProfileRequest (*InstanceSetting)(nil), // 4: memos.api.v1.InstanceSetting (*GetInstanceSettingRequest)(nil), // 5: memos.api.v1.GetInstanceSettingRequest (*UpdateInstanceSettingRequest)(nil), // 6: memos.api.v1.UpdateInstanceSettingRequest (*InstanceSetting_GeneralSetting)(nil), // 7: memos.api.v1.InstanceSetting.GeneralSetting (*InstanceSetting_StorageSetting)(nil), // 8: memos.api.v1.InstanceSetting.StorageSetting (*InstanceSetting_MemoRelatedSetting)(nil), // 9: memos.api.v1.InstanceSetting.MemoRelatedSetting (*InstanceSetting_TagMetadata)(nil), // 10: memos.api.v1.InstanceSetting.TagMetadata (*InstanceSetting_TagsSetting)(nil), // 11: memos.api.v1.InstanceSetting.TagsSetting (*InstanceSetting_NotificationSetting)(nil), // 12: memos.api.v1.InstanceSetting.NotificationSetting (*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 13: memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile (*InstanceSetting_StorageSetting_S3Config)(nil), // 14: memos.api.v1.InstanceSetting.StorageSetting.S3Config nil, // 15: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry (*InstanceSetting_NotificationSetting_EmailSetting)(nil), // 16: memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting (*User)(nil), // 17: memos.api.v1.User (*fieldmaskpb.FieldMask)(nil), // 18: google.protobuf.FieldMask (*color.Color)(nil), // 19: google.type.Color } var file_api_v1_instance_service_proto_depIdxs = []int32{ 17, // 0: memos.api.v1.InstanceProfile.admin:type_name -> memos.api.v1.User 7, // 1: memos.api.v1.InstanceSetting.general_setting:type_name -> memos.api.v1.InstanceSetting.GeneralSetting 8, // 2: memos.api.v1.InstanceSetting.storage_setting:type_name -> memos.api.v1.InstanceSetting.StorageSetting 9, // 3: memos.api.v1.InstanceSetting.memo_related_setting:type_name -> memos.api.v1.InstanceSetting.MemoRelatedSetting 11, // 4: memos.api.v1.InstanceSetting.tags_setting:type_name -> memos.api.v1.InstanceSetting.TagsSetting 12, // 5: memos.api.v1.InstanceSetting.notification_setting:type_name -> memos.api.v1.InstanceSetting.NotificationSetting 4, // 6: memos.api.v1.UpdateInstanceSettingRequest.setting:type_name -> memos.api.v1.InstanceSetting 18, // 7: memos.api.v1.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask 13, // 8: memos.api.v1.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile 1, // 9: memos.api.v1.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.v1.InstanceSetting.StorageSetting.StorageType 14, // 10: memos.api.v1.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.v1.InstanceSetting.StorageSetting.S3Config 19, // 11: memos.api.v1.InstanceSetting.TagMetadata.background_color:type_name -> google.type.Color 15, // 12: memos.api.v1.InstanceSetting.TagsSetting.tags:type_name -> memos.api.v1.InstanceSetting.TagsSetting.TagsEntry 16, // 13: memos.api.v1.InstanceSetting.NotificationSetting.email:type_name -> memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting 10, // 14: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.InstanceSetting.TagMetadata 3, // 15: memos.api.v1.InstanceService.GetInstanceProfile:input_type -> memos.api.v1.GetInstanceProfileRequest 5, // 16: memos.api.v1.InstanceService.GetInstanceSetting:input_type -> memos.api.v1.GetInstanceSettingRequest 6, // 17: memos.api.v1.InstanceService.UpdateInstanceSetting:input_type -> memos.api.v1.UpdateInstanceSettingRequest 2, // 18: memos.api.v1.InstanceService.GetInstanceProfile:output_type -> memos.api.v1.InstanceProfile 4, // 19: memos.api.v1.InstanceService.GetInstanceSetting:output_type -> memos.api.v1.InstanceSetting 4, // 20: memos.api.v1.InstanceService.UpdateInstanceSetting:output_type -> memos.api.v1.InstanceSetting 18, // [18:21] is the sub-list for method output_type 15, // [15:18] is the sub-list for method input_type 15, // [15:15] is the sub-list for extension type_name 15, // [15:15] is the sub-list for extension extendee 0, // [0:15] is the sub-list for field type_name } func init() { file_api_v1_instance_service_proto_init() } func file_api_v1_instance_service_proto_init() { if File_api_v1_instance_service_proto != nil { return } file_api_v1_user_service_proto_init() file_api_v1_instance_service_proto_msgTypes[2].OneofWrappers = []any{ (*InstanceSetting_GeneralSetting_)(nil), (*InstanceSetting_StorageSetting_)(nil), (*InstanceSetting_MemoRelatedSetting_)(nil), (*InstanceSetting_TagsSetting_)(nil), (*InstanceSetting_NotificationSetting_)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_instance_service_proto_rawDesc), len(file_api_v1_instance_service_proto_rawDesc)), NumEnums: 2, NumMessages: 15, NumExtensions: 0, NumServices: 1, }, GoTypes: file_api_v1_instance_service_proto_goTypes, DependencyIndexes: file_api_v1_instance_service_proto_depIdxs, EnumInfos: file_api_v1_instance_service_proto_enumTypes, MessageInfos: file_api_v1_instance_service_proto_msgTypes, }.Build() File_api_v1_instance_service_proto = out.File file_api_v1_instance_service_proto_goTypes = nil file_api_v1_instance_service_proto_depIdxs = nil } ================================================ FILE: proto/gen/api/v1/instance_service.pb.gw.go ================================================ // Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. // source: api/v1/instance_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_InstanceService_GetInstanceProfile_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetInstanceProfileRequest metadata runtime.ServerMetadata ) if req.Body != nil { _, _ = io.Copy(io.Discard, req.Body) } msg, err := client.GetInstanceProfile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_InstanceService_GetInstanceProfile_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetInstanceProfileRequest metadata runtime.ServerMetadata ) msg, err := server.GetInstanceProfile(ctx, &protoReq) return msg, metadata, err } func request_InstanceService_GetInstanceSetting_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetInstanceSettingRequest 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.GetInstanceSetting(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_InstanceService_GetInstanceSetting_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetInstanceSettingRequest 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.GetInstanceSetting(ctx, &protoReq) return msg, metadata, err } var filter_InstanceService_UpdateInstanceSetting_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_InstanceService_UpdateInstanceSetting_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq UpdateInstanceSettingRequest 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_InstanceService_UpdateInstanceSetting_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.UpdateInstanceSetting(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_InstanceService_UpdateInstanceSetting_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq UpdateInstanceSettingRequest 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_InstanceService_UpdateInstanceSetting_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.UpdateInstanceSetting(ctx, &protoReq) return msg, metadata, err } // RegisterInstanceServiceHandlerServer registers the http handlers for service InstanceService to "mux". // UnaryRPC :call InstanceServiceServer 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 RegisterInstanceServiceHandlerFromEndpoint 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 RegisterInstanceServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server InstanceServiceServer) error { mux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceProfile_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.InstanceService/GetInstanceProfile", runtime.WithHTTPPathPattern("/api/v1/instance/profile")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_InstanceService_GetInstanceProfile_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_InstanceService_GetInstanceProfile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceSetting_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.InstanceService/GetInstanceSetting", runtime.WithHTTPPathPattern("/api/v1/{name=instance/settings/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_InstanceService_GetInstanceSetting_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_InstanceService_GetInstanceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPatch, pattern_InstanceService_UpdateInstanceSetting_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.InstanceService/UpdateInstanceSetting", runtime.WithHTTPPathPattern("/api/v1/{setting.name=instance/settings/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_InstanceService_UpdateInstanceSetting_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_InstanceService_UpdateInstanceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) return nil } // RegisterInstanceServiceHandlerFromEndpoint is same as RegisterInstanceServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterInstanceServiceHandlerFromEndpoint(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 RegisterInstanceServiceHandler(ctx, mux, conn) } // RegisterInstanceServiceHandler registers the http handlers for service InstanceService to "mux". // The handlers forward requests to the grpc endpoint over "conn". func RegisterInstanceServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { return RegisterInstanceServiceHandlerClient(ctx, mux, NewInstanceServiceClient(conn)) } // RegisterInstanceServiceHandlerClient registers the http handlers for service InstanceService // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "InstanceServiceClient". // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "InstanceServiceClient" // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in // "InstanceServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. func RegisterInstanceServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client InstanceServiceClient) error { mux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceProfile_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.InstanceService/GetInstanceProfile", runtime.WithHTTPPathPattern("/api/v1/instance/profile")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_InstanceService_GetInstanceProfile_0(annotatedContext, inboundMarshaler, client, req, pathParams) annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) if err != nil { runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) return } forward_InstanceService_GetInstanceProfile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceSetting_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.InstanceService/GetInstanceSetting", runtime.WithHTTPPathPattern("/api/v1/{name=instance/settings/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_InstanceService_GetInstanceSetting_0(annotatedContext, inboundMarshaler, client, req, pathParams) annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) if err != nil { runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) return } forward_InstanceService_GetInstanceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPatch, pattern_InstanceService_UpdateInstanceSetting_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.InstanceService/UpdateInstanceSetting", runtime.WithHTTPPathPattern("/api/v1/{setting.name=instance/settings/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_InstanceService_UpdateInstanceSetting_0(annotatedContext, inboundMarshaler, client, req, pathParams) annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) if err != nil { runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) return } forward_InstanceService_UpdateInstanceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) return nil } var ( pattern_InstanceService_GetInstanceProfile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "instance", "profile"}, "")) pattern_InstanceService_GetInstanceSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 3, 5, 4}, []string{"api", "v1", "instance", "settings", "name"}, "")) pattern_InstanceService_UpdateInstanceSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 3, 5, 4}, []string{"api", "v1", "instance", "settings", "setting.name"}, "")) ) var ( forward_InstanceService_GetInstanceProfile_0 = runtime.ForwardResponseMessage forward_InstanceService_GetInstanceSetting_0 = runtime.ForwardResponseMessage forward_InstanceService_UpdateInstanceSetting_0 = runtime.ForwardResponseMessage ) ================================================ FILE: proto/gen/api/v1/instance_service_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 // - protoc (unknown) // source: api/v1/instance_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 ( InstanceService_GetInstanceProfile_FullMethodName = "/memos.api.v1.InstanceService/GetInstanceProfile" InstanceService_GetInstanceSetting_FullMethodName = "/memos.api.v1.InstanceService/GetInstanceSetting" InstanceService_UpdateInstanceSetting_FullMethodName = "/memos.api.v1.InstanceService/UpdateInstanceSetting" ) // InstanceServiceClient is the client API for InstanceService 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 InstanceServiceClient interface { // Gets the instance profile. GetInstanceProfile(ctx context.Context, in *GetInstanceProfileRequest, opts ...grpc.CallOption) (*InstanceProfile, error) // Gets an instance setting. GetInstanceSetting(ctx context.Context, in *GetInstanceSettingRequest, opts ...grpc.CallOption) (*InstanceSetting, error) // Updates an instance setting. UpdateInstanceSetting(ctx context.Context, in *UpdateInstanceSettingRequest, opts ...grpc.CallOption) (*InstanceSetting, error) } type instanceServiceClient struct { cc grpc.ClientConnInterface } func NewInstanceServiceClient(cc grpc.ClientConnInterface) InstanceServiceClient { return &instanceServiceClient{cc} } func (c *instanceServiceClient) GetInstanceProfile(ctx context.Context, in *GetInstanceProfileRequest, opts ...grpc.CallOption) (*InstanceProfile, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(InstanceProfile) err := c.cc.Invoke(ctx, InstanceService_GetInstanceProfile_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *instanceServiceClient) GetInstanceSetting(ctx context.Context, in *GetInstanceSettingRequest, opts ...grpc.CallOption) (*InstanceSetting, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(InstanceSetting) err := c.cc.Invoke(ctx, InstanceService_GetInstanceSetting_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *instanceServiceClient) UpdateInstanceSetting(ctx context.Context, in *UpdateInstanceSettingRequest, opts ...grpc.CallOption) (*InstanceSetting, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(InstanceSetting) err := c.cc.Invoke(ctx, InstanceService_UpdateInstanceSetting_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } // InstanceServiceServer is the server API for InstanceService service. // All implementations must embed UnimplementedInstanceServiceServer // for forward compatibility. type InstanceServiceServer interface { // Gets the instance profile. GetInstanceProfile(context.Context, *GetInstanceProfileRequest) (*InstanceProfile, error) // Gets an instance setting. GetInstanceSetting(context.Context, *GetInstanceSettingRequest) (*InstanceSetting, error) // Updates an instance setting. UpdateInstanceSetting(context.Context, *UpdateInstanceSettingRequest) (*InstanceSetting, error) mustEmbedUnimplementedInstanceServiceServer() } // UnimplementedInstanceServiceServer 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 UnimplementedInstanceServiceServer struct{} func (UnimplementedInstanceServiceServer) GetInstanceProfile(context.Context, *GetInstanceProfileRequest) (*InstanceProfile, error) { return nil, status.Error(codes.Unimplemented, "method GetInstanceProfile not implemented") } func (UnimplementedInstanceServiceServer) GetInstanceSetting(context.Context, *GetInstanceSettingRequest) (*InstanceSetting, error) { return nil, status.Error(codes.Unimplemented, "method GetInstanceSetting not implemented") } func (UnimplementedInstanceServiceServer) UpdateInstanceSetting(context.Context, *UpdateInstanceSettingRequest) (*InstanceSetting, error) { return nil, status.Error(codes.Unimplemented, "method UpdateInstanceSetting not implemented") } func (UnimplementedInstanceServiceServer) mustEmbedUnimplementedInstanceServiceServer() {} func (UnimplementedInstanceServiceServer) testEmbeddedByValue() {} // UnsafeInstanceServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to InstanceServiceServer will // result in compilation errors. type UnsafeInstanceServiceServer interface { mustEmbedUnimplementedInstanceServiceServer() } func RegisterInstanceServiceServer(s grpc.ServiceRegistrar, srv InstanceServiceServer) { // If the following call panics, it indicates UnimplementedInstanceServiceServer 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(&InstanceService_ServiceDesc, srv) } func _InstanceService_GetInstanceProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetInstanceProfileRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(InstanceServiceServer).GetInstanceProfile(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: InstanceService_GetInstanceProfile_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(InstanceServiceServer).GetInstanceProfile(ctx, req.(*GetInstanceProfileRequest)) } return interceptor(ctx, in, info, handler) } func _InstanceService_GetInstanceSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetInstanceSettingRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(InstanceServiceServer).GetInstanceSetting(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: InstanceService_GetInstanceSetting_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(InstanceServiceServer).GetInstanceSetting(ctx, req.(*GetInstanceSettingRequest)) } return interceptor(ctx, in, info, handler) } func _InstanceService_UpdateInstanceSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UpdateInstanceSettingRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(InstanceServiceServer).UpdateInstanceSetting(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: InstanceService_UpdateInstanceSetting_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(InstanceServiceServer).UpdateInstanceSetting(ctx, req.(*UpdateInstanceSettingRequest)) } return interceptor(ctx, in, info, handler) } // InstanceService_ServiceDesc is the grpc.ServiceDesc for InstanceService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var InstanceService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "memos.api.v1.InstanceService", HandlerType: (*InstanceServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetInstanceProfile", Handler: _InstanceService_GetInstanceProfile_Handler, }, { MethodName: "GetInstanceSetting", Handler: _InstanceService_GetInstanceSetting_Handler, }, { MethodName: "UpdateInstanceSetting", Handler: _InstanceService_UpdateInstanceSetting_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "api/v1/instance_service.proto", } ================================================ FILE: proto/gen/api/v1/memo_service.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // 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{12, 0} } type Reaction struct { state protoimpl.MessageState `protogen:"open.v1"` // The resource name of the reaction. // Format: memos/{memo}/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"` // The creation timestamp. // If not set on creation, the server will set it to the current time. CreateTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` // The last update timestamp. // If not set on creation, the server will set it to the current time. 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"` // 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) 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"` 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 "" } type ListMemosRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // 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,1,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,2,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,3,opt,name=state,proto3,enum=memos.api.v1.State" json:"state,omitempty"` // Optional. The order to sort results by. // Default to "display_time desc". // Supports comma-separated list of fields following AIP-132. // Example: "pinned desc, display_time desc" or "create_time asc" // Supported fields: pinned, display_time, create_time, update_time, name OrderBy string `protobuf:"bytes,4,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,5,opt,name=filter,proto3" json:"filter,omitempty"` // Optional. If true, show deleted memos in the response. ShowDeleted bool `protobuf:"varint,6,opt,name=show_deleted,json=showDeleted,proto3" json:"show_deleted,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) 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 } 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"` 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 "" } 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"` 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 "" } 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"` 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 } 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 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[9] 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[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 SetMemoAttachmentsRequest.ProtoReflect.Descriptor instead. func (*SetMemoAttachmentsRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{9} } 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[10] 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[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 ListMemoAttachmentsRequest.ProtoReflect.Descriptor instead. func (*ListMemoAttachmentsRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{10} } 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"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListMemoAttachmentsResponse) Reset() { *x = ListMemoAttachmentsResponse{} mi := &file_api_v1_memo_service_proto_msgTypes[11] 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[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 ListMemoAttachmentsResponse.ProtoReflect.Descriptor instead. func (*ListMemoAttachmentsResponse) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{11} } 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 "" } 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[12] 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[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 MemoRelation.ProtoReflect.Descriptor instead. func (*MemoRelation) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{12} } 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[13] 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[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 SetMemoRelationsRequest.ProtoReflect.Descriptor instead. func (*SetMemoRelationsRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{13} } 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[14] 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[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 ListMemoRelationsRequest.ProtoReflect.Descriptor instead. func (*ListMemoRelationsRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{14} } 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"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListMemoRelationsResponse) Reset() { *x = ListMemoRelationsResponse{} mi := &file_api_v1_memo_service_proto_msgTypes[15] 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[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 ListMemoRelationsResponse.ProtoReflect.Descriptor instead. func (*ListMemoRelationsResponse) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{15} } 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 "" } 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[16] 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[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 CreateMemoCommentRequest.ProtoReflect.Descriptor instead. func (*CreateMemoCommentRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{16} } 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[17] 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[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 ListMemoCommentsRequest.ProtoReflect.Descriptor instead. func (*ListMemoCommentsRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{17} } 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[18] 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[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 ListMemoCommentsResponse.ProtoReflect.Descriptor instead. func (*ListMemoCommentsResponse) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{18} } 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[19] 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[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 ListMemoReactionsRequest.ProtoReflect.Descriptor instead. func (*ListMemoReactionsRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{19} } 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[20] 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[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 ListMemoReactionsResponse.ProtoReflect.Descriptor instead. func (*ListMemoReactionsResponse) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{20} } 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[21] 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[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 UpsertMemoReactionRequest.ProtoReflect.Descriptor instead. func (*UpsertMemoReactionRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{21} } 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: memos/{memo}/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[22] 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[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 DeleteMemoReactionRequest.ProtoReflect.Descriptor instead. func (*DeleteMemoReactionRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{22} } func (x *DeleteMemoReactionRequest) GetName() string { if x != nil { return x.Name } return "" } // MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token. type MemoShare struct { state protoimpl.MessageState `protogen:"open.v1"` // The resource name of the share. Format: memos/{memo}/shares/{share} // The {share} segment is the opaque token used in the share URL. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Output only. When this share link was created. CreateTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` // Optional. When set, the share link stops working after this time. // If unset, the link never expires. ExpireTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expire_time,json=expireTime,proto3,oneof" json:"expire_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *MemoShare) Reset() { *x = MemoShare{} mi := &file_api_v1_memo_service_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *MemoShare) String() string { return protoimpl.X.MessageStringOf(x) } func (*MemoShare) ProtoMessage() {} func (x *MemoShare) 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 MemoShare.ProtoReflect.Descriptor instead. func (*MemoShare) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{23} } func (x *MemoShare) GetName() string { if x != nil { return x.Name } return "" } func (x *MemoShare) GetCreateTime() *timestamppb.Timestamp { if x != nil { return x.CreateTime } return nil } func (x *MemoShare) GetExpireTime() *timestamppb.Timestamp { if x != nil { return x.ExpireTime } return nil } type CreateMemoShareRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The resource name of the memo to share. // Format: memos/{memo} Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` // Required. The share to create. MemoShare *MemoShare `protobuf:"bytes,2,opt,name=memo_share,json=memoShare,proto3" json:"memo_share,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CreateMemoShareRequest) Reset() { *x = CreateMemoShareRequest{} mi := &file_api_v1_memo_service_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CreateMemoShareRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateMemoShareRequest) ProtoMessage() {} func (x *CreateMemoShareRequest) 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 CreateMemoShareRequest.ProtoReflect.Descriptor instead. func (*CreateMemoShareRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{24} } func (x *CreateMemoShareRequest) GetParent() string { if x != nil { return x.Parent } return "" } func (x *CreateMemoShareRequest) GetMemoShare() *MemoShare { if x != nil { return x.MemoShare } return nil } type ListMemoSharesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The resource name of the memo. // Format: memos/{memo} Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListMemoSharesRequest) Reset() { *x = ListMemoSharesRequest{} mi := &file_api_v1_memo_service_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListMemoSharesRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListMemoSharesRequest) ProtoMessage() {} func (x *ListMemoSharesRequest) 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 ListMemoSharesRequest.ProtoReflect.Descriptor instead. func (*ListMemoSharesRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{25} } func (x *ListMemoSharesRequest) GetParent() string { if x != nil { return x.Parent } return "" } type ListMemoSharesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The list of share links. MemoShares []*MemoShare `protobuf:"bytes,1,rep,name=memo_shares,json=memoShares,proto3" json:"memo_shares,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListMemoSharesResponse) Reset() { *x = ListMemoSharesResponse{} mi := &file_api_v1_memo_service_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListMemoSharesResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListMemoSharesResponse) ProtoMessage() {} func (x *ListMemoSharesResponse) 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 ListMemoSharesResponse.ProtoReflect.Descriptor instead. func (*ListMemoSharesResponse) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{26} } func (x *ListMemoSharesResponse) GetMemoShares() []*MemoShare { if x != nil { return x.MemoShares } return nil } type DeleteMemoShareRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The resource name of the share to delete. // Format: memos/{memo}/shares/{share} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeleteMemoShareRequest) Reset() { *x = DeleteMemoShareRequest{} mi := &file_api_v1_memo_service_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DeleteMemoShareRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteMemoShareRequest) ProtoMessage() {} func (x *DeleteMemoShareRequest) 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 DeleteMemoShareRequest.ProtoReflect.Descriptor instead. func (*DeleteMemoShareRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{27} } func (x *DeleteMemoShareRequest) GetName() string { if x != nil { return x.Name } return "" } type GetMemoByShareRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The share token extracted from the share URL (/s/{share_id}). ShareId string `protobuf:"bytes,1,opt,name=share_id,json=shareId,proto3" json:"share_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetMemoByShareRequest) Reset() { *x = GetMemoByShareRequest{} mi := &file_api_v1_memo_service_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GetMemoByShareRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetMemoByShareRequest) ProtoMessage() {} func (x *GetMemoByShareRequest) 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 GetMemoByShareRequest.ProtoReflect.Descriptor instead. func (*GetMemoByShareRequest) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{28} } func (x *GetMemoByShareRequest) GetShareId() string { if x != nil { return x.ShareId } return "" } // 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"` // The title extracted from the first H1 heading, if present. Title string `protobuf:"bytes,5,opt,name=title,proto3" json:"title,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Memo_Property) Reset() { *x = Memo_Property{} mi := &file_api_v1_memo_service_proto_msgTypes[29] 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[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 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 } func (x *Memo_Property) GetTitle() string { if x != nil { return x.Title } return "" } // 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[30] 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[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 MemoRelation_Memo.ProtoReflect.Descriptor instead. func (*MemoRelation_Memo) Descriptor() ([]byte, []int) { return file_api_v1_memo_service_proto_rawDescGZIP(), []int{12, 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\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\"\xdb\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:X\xeaAU\n" + "\x15memos.api.v1/Reaction\x12!memos/{memo}/reactions/{reaction}\x1a\x04name*\treactions2\breaction\"\xee\b\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\x01R\n" + "createTime\x12@\n" + "\vupdate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x01R\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" + "\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\xac\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\x14\n" + "\x05title\x18\x05 \x01(\tR\x05title: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\"^\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\"\xed\x01\n" + "\x10ListMemosRequest\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.\n" + "\x05state\x18\x03 \x01(\x0e2\x13.memos.api.v1.StateB\x03\xe0A\x01R\x05state\x12\x1e\n" + "\border_by\x18\x04 \x01(\tB\x03\xe0A\x01R\aorderBy\x12\x1b\n" + "\x06filter\x18\x05 \x01(\tB\x03\xe0A\x01R\x06filter\x12&\n" + "\fshow_deleted\x18\x06 \x01(\bB\x03\xe0A\x01R\vshowDeleted\"e\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\"?\n" + "\x0eGetMemoRequest\x12-\n" + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + "\x11memos.api.v1/MemoR\x04name\"\x82\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\"]\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\"\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\"\x81\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\"\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\"}\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\"\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\"\x86\x02\n" + "\tMemoShare\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\x12E\n" + "\vexpire_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x01H\x00R\n" + "expireTime\x88\x01\x01:G\xeaAD\n" + "\x16memos.api.v1/MemoShare\x12\x1bmemos/{memo}/shares/{share}*\x06shares2\x05shareB\x0e\n" + "\f_expire_time\"\x88\x01\n" + "\x16CreateMemoShareRequest\x121\n" + "\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + "\x11memos.api.v1/MemoR\x06parent\x12;\n" + "\n" + "memo_share\x18\x02 \x01(\v2\x17.memos.api.v1.MemoShareB\x03\xe0A\x02R\tmemoShare\"J\n" + "\x15ListMemoSharesRequest\x121\n" + "\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + "\x11memos.api.v1/MemoR\x06parent\"R\n" + "\x16ListMemoSharesResponse\x128\n" + "\vmemo_shares\x18\x01 \x03(\v2\x17.memos.api.v1.MemoShareR\n" + "memoShares\"L\n" + "\x16DeleteMemoShareRequest\x122\n" + "\x04name\x18\x01 \x01(\tB\x1e\xe0A\x02\xfaA\x18\n" + "\x16memos.api.v1/MemoShareR\x04name\"7\n" + "\x15GetMemoByShareRequest\x12\x1e\n" + "\bshare_id\x18\x01 \x01(\tB\x03\xe0A\x02R\ashareId*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\xee\x12\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\x12f\n" + "\tListMemos\x12\x1e.memos.api.v1.ListMemosRequest\x1a\x1f.memos.api.v1.ListMemosResponse\"\x18\xdaA\x00\x82\xd3\xe4\x93\x02\x0f\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\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\x88\x01\n" + "\x12DeleteMemoReaction\x12'.memos.api.v1.DeleteMemoReactionRequest\x1a\x16.google.protobuf.Empty\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$*\"/api/v1/{name=memos/*/reactions/*}\x12\x99\x01\n" + "\x0fCreateMemoShare\x12$.memos.api.v1.CreateMemoShareRequest\x1a\x17.memos.api.v1.MemoShare\"G\xdaA\x11parent,memo_share\x82\xd3\xe4\x93\x02-:\n" + "memo_share\"\x1f/api/v1/{parent=memos/*}/shares\x12\x8d\x01\n" + "\x0eListMemoShares\x12#.memos.api.v1.ListMemoSharesRequest\x1a$.memos.api.v1.ListMemoSharesResponse\"0\xdaA\x06parent\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/{parent=memos/*}/shares\x12\x7f\n" + "\x0fDeleteMemoShare\x12$.memos.api.v1.DeleteMemoShareRequest\x1a\x16.google.protobuf.Empty\".\xdaA\x04name\x82\xd3\xe4\x93\x02!*\x1f/api/v1/{name=memos/*/shares/*}\x12l\n" + "\x0eGetMemoByShare\x12#.memos.api.v1.GetMemoByShareRequest\x1a\x12.memos.api.v1.Memo\"!\x82\xd3\xe4\x93\x02\x1b\x12\x19/api/v1/shares/{share_id}B\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, 31) 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 (*SetMemoAttachmentsRequest)(nil), // 11: memos.api.v1.SetMemoAttachmentsRequest (*ListMemoAttachmentsRequest)(nil), // 12: memos.api.v1.ListMemoAttachmentsRequest (*ListMemoAttachmentsResponse)(nil), // 13: memos.api.v1.ListMemoAttachmentsResponse (*MemoRelation)(nil), // 14: memos.api.v1.MemoRelation (*SetMemoRelationsRequest)(nil), // 15: memos.api.v1.SetMemoRelationsRequest (*ListMemoRelationsRequest)(nil), // 16: memos.api.v1.ListMemoRelationsRequest (*ListMemoRelationsResponse)(nil), // 17: memos.api.v1.ListMemoRelationsResponse (*CreateMemoCommentRequest)(nil), // 18: memos.api.v1.CreateMemoCommentRequest (*ListMemoCommentsRequest)(nil), // 19: memos.api.v1.ListMemoCommentsRequest (*ListMemoCommentsResponse)(nil), // 20: memos.api.v1.ListMemoCommentsResponse (*ListMemoReactionsRequest)(nil), // 21: memos.api.v1.ListMemoReactionsRequest (*ListMemoReactionsResponse)(nil), // 22: memos.api.v1.ListMemoReactionsResponse (*UpsertMemoReactionRequest)(nil), // 23: memos.api.v1.UpsertMemoReactionRequest (*DeleteMemoReactionRequest)(nil), // 24: memos.api.v1.DeleteMemoReactionRequest (*MemoShare)(nil), // 25: memos.api.v1.MemoShare (*CreateMemoShareRequest)(nil), // 26: memos.api.v1.CreateMemoShareRequest (*ListMemoSharesRequest)(nil), // 27: memos.api.v1.ListMemoSharesRequest (*ListMemoSharesResponse)(nil), // 28: memos.api.v1.ListMemoSharesResponse (*DeleteMemoShareRequest)(nil), // 29: memos.api.v1.DeleteMemoShareRequest (*GetMemoByShareRequest)(nil), // 30: memos.api.v1.GetMemoByShareRequest (*Memo_Property)(nil), // 31: memos.api.v1.Memo.Property (*MemoRelation_Memo)(nil), // 32: memos.api.v1.MemoRelation.Memo (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp (State)(0), // 34: memos.api.v1.State (*Attachment)(nil), // 35: memos.api.v1.Attachment (*fieldmaskpb.FieldMask)(nil), // 36: google.protobuf.FieldMask (*emptypb.Empty)(nil), // 37: google.protobuf.Empty } var file_api_v1_memo_service_proto_depIdxs = []int32{ 33, // 0: memos.api.v1.Reaction.create_time:type_name -> google.protobuf.Timestamp 34, // 1: memos.api.v1.Memo.state:type_name -> memos.api.v1.State 33, // 2: memos.api.v1.Memo.create_time:type_name -> google.protobuf.Timestamp 33, // 3: memos.api.v1.Memo.update_time:type_name -> google.protobuf.Timestamp 33, // 4: memos.api.v1.Memo.display_time:type_name -> google.protobuf.Timestamp 0, // 5: memos.api.v1.Memo.visibility:type_name -> memos.api.v1.Visibility 35, // 6: memos.api.v1.Memo.attachments:type_name -> memos.api.v1.Attachment 14, // 7: memos.api.v1.Memo.relations:type_name -> memos.api.v1.MemoRelation 2, // 8: memos.api.v1.Memo.reactions:type_name -> memos.api.v1.Reaction 31, // 9: memos.api.v1.Memo.property:type_name -> memos.api.v1.Memo.Property 4, // 10: memos.api.v1.Memo.location:type_name -> memos.api.v1.Location 3, // 11: memos.api.v1.CreateMemoRequest.memo:type_name -> memos.api.v1.Memo 34, // 12: memos.api.v1.ListMemosRequest.state:type_name -> memos.api.v1.State 3, // 13: memos.api.v1.ListMemosResponse.memos:type_name -> memos.api.v1.Memo 3, // 14: memos.api.v1.UpdateMemoRequest.memo:type_name -> memos.api.v1.Memo 36, // 15: memos.api.v1.UpdateMemoRequest.update_mask:type_name -> google.protobuf.FieldMask 35, // 16: memos.api.v1.SetMemoAttachmentsRequest.attachments:type_name -> memos.api.v1.Attachment 35, // 17: memos.api.v1.ListMemoAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment 32, // 18: memos.api.v1.MemoRelation.memo:type_name -> memos.api.v1.MemoRelation.Memo 32, // 19: memos.api.v1.MemoRelation.related_memo:type_name -> memos.api.v1.MemoRelation.Memo 1, // 20: memos.api.v1.MemoRelation.type:type_name -> memos.api.v1.MemoRelation.Type 14, // 21: memos.api.v1.SetMemoRelationsRequest.relations:type_name -> memos.api.v1.MemoRelation 14, // 22: memos.api.v1.ListMemoRelationsResponse.relations:type_name -> memos.api.v1.MemoRelation 3, // 23: memos.api.v1.CreateMemoCommentRequest.comment:type_name -> memos.api.v1.Memo 3, // 24: memos.api.v1.ListMemoCommentsResponse.memos:type_name -> memos.api.v1.Memo 2, // 25: memos.api.v1.ListMemoReactionsResponse.reactions:type_name -> memos.api.v1.Reaction 2, // 26: memos.api.v1.UpsertMemoReactionRequest.reaction:type_name -> memos.api.v1.Reaction 33, // 27: memos.api.v1.MemoShare.create_time:type_name -> google.protobuf.Timestamp 33, // 28: memos.api.v1.MemoShare.expire_time:type_name -> google.protobuf.Timestamp 25, // 29: memos.api.v1.CreateMemoShareRequest.memo_share:type_name -> memos.api.v1.MemoShare 25, // 30: memos.api.v1.ListMemoSharesResponse.memo_shares:type_name -> memos.api.v1.MemoShare 5, // 31: memos.api.v1.MemoService.CreateMemo:input_type -> memos.api.v1.CreateMemoRequest 6, // 32: memos.api.v1.MemoService.ListMemos:input_type -> memos.api.v1.ListMemosRequest 8, // 33: memos.api.v1.MemoService.GetMemo:input_type -> memos.api.v1.GetMemoRequest 9, // 34: memos.api.v1.MemoService.UpdateMemo:input_type -> memos.api.v1.UpdateMemoRequest 10, // 35: memos.api.v1.MemoService.DeleteMemo:input_type -> memos.api.v1.DeleteMemoRequest 11, // 36: memos.api.v1.MemoService.SetMemoAttachments:input_type -> memos.api.v1.SetMemoAttachmentsRequest 12, // 37: memos.api.v1.MemoService.ListMemoAttachments:input_type -> memos.api.v1.ListMemoAttachmentsRequest 15, // 38: memos.api.v1.MemoService.SetMemoRelations:input_type -> memos.api.v1.SetMemoRelationsRequest 16, // 39: memos.api.v1.MemoService.ListMemoRelations:input_type -> memos.api.v1.ListMemoRelationsRequest 18, // 40: memos.api.v1.MemoService.CreateMemoComment:input_type -> memos.api.v1.CreateMemoCommentRequest 19, // 41: memos.api.v1.MemoService.ListMemoComments:input_type -> memos.api.v1.ListMemoCommentsRequest 21, // 42: memos.api.v1.MemoService.ListMemoReactions:input_type -> memos.api.v1.ListMemoReactionsRequest 23, // 43: memos.api.v1.MemoService.UpsertMemoReaction:input_type -> memos.api.v1.UpsertMemoReactionRequest 24, // 44: memos.api.v1.MemoService.DeleteMemoReaction:input_type -> memos.api.v1.DeleteMemoReactionRequest 26, // 45: memos.api.v1.MemoService.CreateMemoShare:input_type -> memos.api.v1.CreateMemoShareRequest 27, // 46: memos.api.v1.MemoService.ListMemoShares:input_type -> memos.api.v1.ListMemoSharesRequest 29, // 47: memos.api.v1.MemoService.DeleteMemoShare:input_type -> memos.api.v1.DeleteMemoShareRequest 30, // 48: memos.api.v1.MemoService.GetMemoByShare:input_type -> memos.api.v1.GetMemoByShareRequest 3, // 49: memos.api.v1.MemoService.CreateMemo:output_type -> memos.api.v1.Memo 7, // 50: memos.api.v1.MemoService.ListMemos:output_type -> memos.api.v1.ListMemosResponse 3, // 51: memos.api.v1.MemoService.GetMemo:output_type -> memos.api.v1.Memo 3, // 52: memos.api.v1.MemoService.UpdateMemo:output_type -> memos.api.v1.Memo 37, // 53: memos.api.v1.MemoService.DeleteMemo:output_type -> google.protobuf.Empty 37, // 54: memos.api.v1.MemoService.SetMemoAttachments:output_type -> google.protobuf.Empty 13, // 55: memos.api.v1.MemoService.ListMemoAttachments:output_type -> memos.api.v1.ListMemoAttachmentsResponse 37, // 56: memos.api.v1.MemoService.SetMemoRelations:output_type -> google.protobuf.Empty 17, // 57: memos.api.v1.MemoService.ListMemoRelations:output_type -> memos.api.v1.ListMemoRelationsResponse 3, // 58: memos.api.v1.MemoService.CreateMemoComment:output_type -> memos.api.v1.Memo 20, // 59: memos.api.v1.MemoService.ListMemoComments:output_type -> memos.api.v1.ListMemoCommentsResponse 22, // 60: memos.api.v1.MemoService.ListMemoReactions:output_type -> memos.api.v1.ListMemoReactionsResponse 2, // 61: memos.api.v1.MemoService.UpsertMemoReaction:output_type -> memos.api.v1.Reaction 37, // 62: memos.api.v1.MemoService.DeleteMemoReaction:output_type -> google.protobuf.Empty 25, // 63: memos.api.v1.MemoService.CreateMemoShare:output_type -> memos.api.v1.MemoShare 28, // 64: memos.api.v1.MemoService.ListMemoShares:output_type -> memos.api.v1.ListMemoSharesResponse 37, // 65: memos.api.v1.MemoService.DeleteMemoShare:output_type -> google.protobuf.Empty 3, // 66: memos.api.v1.MemoService.GetMemoByShare:output_type -> memos.api.v1.Memo 49, // [49:67] is the sub-list for method output_type 31, // [31:49] is the sub-list for method input_type 31, // [31:31] is the sub-list for extension type_name 31, // [31:31] is the sub-list for extension extendee 0, // [0:31] 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_memo_service_proto_msgTypes[1].OneofWrappers = []any{} file_api_v1_memo_service_proto_msgTypes[23].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: 31, 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 } ================================================ FILE: proto/gen/api/v1/memo_service.pb.gw.go ================================================ // 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 } 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) } 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) } 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_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_CreateMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq CreateMemoShareRequest metadata runtime.ServerMetadata err error ) if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.MemoShare); 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.CreateMemoShare(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_MemoService_CreateMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq CreateMemoShareRequest metadata runtime.ServerMetadata err error ) if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.MemoShare); 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.CreateMemoShare(ctx, &protoReq) return msg, metadata, err } func request_MemoService_ListMemoShares_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq ListMemoSharesRequest 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.ListMemoShares(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_MemoService_ListMemoShares_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq ListMemoSharesRequest 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.ListMemoShares(ctx, &protoReq) return msg, metadata, err } func request_MemoService_DeleteMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq DeleteMemoShareRequest 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.DeleteMemoShare(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_MemoService_DeleteMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq DeleteMemoShareRequest 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.DeleteMemoShare(ctx, &protoReq) return msg, metadata, err } func request_MemoService_GetMemoByShare_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetMemoByShareRequest metadata runtime.ServerMetadata err error ) if req.Body != nil { _, _ = io.Copy(io.Discard, req.Body) } val, ok := pathParams["share_id"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "share_id") } protoReq.ShareId, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "share_id", err) } msg, err := client.GetMemoByShare(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_MemoService_GetMemoByShare_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetMemoByShareRequest metadata runtime.ServerMetadata err error ) val, ok := pathParams["share_id"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "share_id") } protoReq.ShareId, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "share_id", err) } msg, err := server.GetMemoByShare(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_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_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=memos/*/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_CreateMemoShare_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/CreateMemoShare", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/shares")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_MemoService_CreateMemoShare_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_CreateMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_MemoService_ListMemoShares_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/ListMemoShares", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/shares")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_MemoService_ListMemoShares_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_ListMemoShares_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoShare_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/DeleteMemoShare", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*/shares/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_MemoService_DeleteMemoShare_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_DeleteMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_MemoService_GetMemoByShare_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/GetMemoByShare", runtime.WithHTTPPathPattern("/api/v1/shares/{share_id}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_MemoService_GetMemoByShare_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_GetMemoByShare_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_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_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=memos/*/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_CreateMemoShare_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/CreateMemoShare", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/shares")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_MemoService_CreateMemoShare_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_CreateMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_MemoService_ListMemoShares_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/ListMemoShares", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/shares")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_MemoService_ListMemoShares_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_ListMemoShares_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoShare_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/DeleteMemoShare", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*/shares/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_MemoService_DeleteMemoShare_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_DeleteMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_MemoService_GetMemoByShare_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/GetMemoByShare", runtime.WithHTTPPathPattern("/api/v1/shares/{share_id}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_MemoService_GetMemoByShare_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_GetMemoByShare_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_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_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, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "memos", "reactions", "name"}, "")) pattern_MemoService_CreateMemoShare_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", "shares"}, "")) pattern_MemoService_ListMemoShares_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", "shares"}, "")) pattern_MemoService_DeleteMemoShare_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", "memos", "shares", "name"}, "")) pattern_MemoService_GetMemoByShare_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "shares", "share_id"}, "")) ) var ( forward_MemoService_CreateMemo_0 = runtime.ForwardResponseMessage forward_MemoService_ListMemos_0 = runtime.ForwardResponseMessage forward_MemoService_GetMemo_0 = runtime.ForwardResponseMessage forward_MemoService_UpdateMemo_0 = runtime.ForwardResponseMessage forward_MemoService_DeleteMemo_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_CreateMemoShare_0 = runtime.ForwardResponseMessage forward_MemoService_ListMemoShares_0 = runtime.ForwardResponseMessage forward_MemoService_DeleteMemoShare_0 = runtime.ForwardResponseMessage forward_MemoService_GetMemoByShare_0 = runtime.ForwardResponseMessage ) ================================================ FILE: proto/gen/api/v1/memo_service_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.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_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_CreateMemoShare_FullMethodName = "/memos.api.v1.MemoService/CreateMemoShare" MemoService_ListMemoShares_FullMethodName = "/memos.api.v1.MemoService/ListMemoShares" MemoService_DeleteMemoShare_FullMethodName = "/memos.api.v1.MemoService/DeleteMemoShare" MemoService_GetMemoByShare_FullMethodName = "/memos.api.v1.MemoService/GetMemoByShare" ) // 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) // 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) // CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator. CreateMemoShare(ctx context.Context, in *CreateMemoShareRequest, opts ...grpc.CallOption) (*MemoShare, error) // ListMemoShares lists all share links for a memo. Requires authentication as the memo creator. ListMemoShares(ctx context.Context, in *ListMemoSharesRequest, opts ...grpc.CallOption) (*ListMemoSharesResponse, error) // DeleteMemoShare revokes a share link. Requires authentication as the memo creator. DeleteMemoShare(ctx context.Context, in *DeleteMemoShareRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // GetMemoByShare resolves a share token to its memo. No authentication required. // Returns NOT_FOUND if the token is invalid or expired. GetMemoByShare(ctx context.Context, in *GetMemoByShareRequest, opts ...grpc.CallOption) (*Memo, 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) 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) CreateMemoShare(ctx context.Context, in *CreateMemoShareRequest, opts ...grpc.CallOption) (*MemoShare, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(MemoShare) err := c.cc.Invoke(ctx, MemoService_CreateMemoShare_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *memoServiceClient) ListMemoShares(ctx context.Context, in *ListMemoSharesRequest, opts ...grpc.CallOption) (*ListMemoSharesResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListMemoSharesResponse) err := c.cc.Invoke(ctx, MemoService_ListMemoShares_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *memoServiceClient) DeleteMemoShare(ctx context.Context, in *DeleteMemoShareRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, MemoService_DeleteMemoShare_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *memoServiceClient) GetMemoByShare(ctx context.Context, in *GetMemoByShareRequest, opts ...grpc.CallOption) (*Memo, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Memo) err := c.cc.Invoke(ctx, MemoService_GetMemoByShare_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) // 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) // CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator. CreateMemoShare(context.Context, *CreateMemoShareRequest) (*MemoShare, error) // ListMemoShares lists all share links for a memo. Requires authentication as the memo creator. ListMemoShares(context.Context, *ListMemoSharesRequest) (*ListMemoSharesResponse, error) // DeleteMemoShare revokes a share link. Requires authentication as the memo creator. DeleteMemoShare(context.Context, *DeleteMemoShareRequest) (*emptypb.Empty, error) // GetMemoByShare resolves a share token to its memo. No authentication required. // Returns NOT_FOUND if the token is invalid or expired. GetMemoByShare(context.Context, *GetMemoByShareRequest) (*Memo, 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.Error(codes.Unimplemented, "method CreateMemo not implemented") } func (UnimplementedMemoServiceServer) ListMemos(context.Context, *ListMemosRequest) (*ListMemosResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListMemos not implemented") } func (UnimplementedMemoServiceServer) GetMemo(context.Context, *GetMemoRequest) (*Memo, error) { return nil, status.Error(codes.Unimplemented, "method GetMemo not implemented") } func (UnimplementedMemoServiceServer) UpdateMemo(context.Context, *UpdateMemoRequest) (*Memo, error) { return nil, status.Error(codes.Unimplemented, "method UpdateMemo not implemented") } func (UnimplementedMemoServiceServer) DeleteMemo(context.Context, *DeleteMemoRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method DeleteMemo not implemented") } func (UnimplementedMemoServiceServer) SetMemoAttachments(context.Context, *SetMemoAttachmentsRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method SetMemoAttachments not implemented") } func (UnimplementedMemoServiceServer) ListMemoAttachments(context.Context, *ListMemoAttachmentsRequest) (*ListMemoAttachmentsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListMemoAttachments not implemented") } func (UnimplementedMemoServiceServer) SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method SetMemoRelations not implemented") } func (UnimplementedMemoServiceServer) ListMemoRelations(context.Context, *ListMemoRelationsRequest) (*ListMemoRelationsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListMemoRelations not implemented") } func (UnimplementedMemoServiceServer) CreateMemoComment(context.Context, *CreateMemoCommentRequest) (*Memo, error) { return nil, status.Error(codes.Unimplemented, "method CreateMemoComment not implemented") } func (UnimplementedMemoServiceServer) ListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListMemoComments not implemented") } func (UnimplementedMemoServiceServer) ListMemoReactions(context.Context, *ListMemoReactionsRequest) (*ListMemoReactionsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListMemoReactions not implemented") } func (UnimplementedMemoServiceServer) UpsertMemoReaction(context.Context, *UpsertMemoReactionRequest) (*Reaction, error) { return nil, status.Error(codes.Unimplemented, "method UpsertMemoReaction not implemented") } func (UnimplementedMemoServiceServer) DeleteMemoReaction(context.Context, *DeleteMemoReactionRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method DeleteMemoReaction not implemented") } func (UnimplementedMemoServiceServer) CreateMemoShare(context.Context, *CreateMemoShareRequest) (*MemoShare, error) { return nil, status.Error(codes.Unimplemented, "method CreateMemoShare not implemented") } func (UnimplementedMemoServiceServer) ListMemoShares(context.Context, *ListMemoSharesRequest) (*ListMemoSharesResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListMemoShares not implemented") } func (UnimplementedMemoServiceServer) DeleteMemoShare(context.Context, *DeleteMemoShareRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method DeleteMemoShare not implemented") } func (UnimplementedMemoServiceServer) GetMemoByShare(context.Context, *GetMemoByShareRequest) (*Memo, error) { return nil, status.Error(codes.Unimplemented, "method GetMemoByShare 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 panics, 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_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_CreateMemoShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateMemoShareRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(MemoServiceServer).CreateMemoShare(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: MemoService_CreateMemoShare_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(MemoServiceServer).CreateMemoShare(ctx, req.(*CreateMemoShareRequest)) } return interceptor(ctx, in, info, handler) } func _MemoService_ListMemoShares_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListMemoSharesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(MemoServiceServer).ListMemoShares(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: MemoService_ListMemoShares_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(MemoServiceServer).ListMemoShares(ctx, req.(*ListMemoSharesRequest)) } return interceptor(ctx, in, info, handler) } func _MemoService_DeleteMemoShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteMemoShareRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(MemoServiceServer).DeleteMemoShare(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: MemoService_DeleteMemoShare_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(MemoServiceServer).DeleteMemoShare(ctx, req.(*DeleteMemoShareRequest)) } return interceptor(ctx, in, info, handler) } func _MemoService_GetMemoByShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetMemoByShareRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(MemoServiceServer).GetMemoByShare(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: MemoService_GetMemoByShare_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(MemoServiceServer).GetMemoByShare(ctx, req.(*GetMemoByShareRequest)) } 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: "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: "CreateMemoShare", Handler: _MemoService_CreateMemoShare_Handler, }, { MethodName: "ListMemoShares", Handler: _MemoService_ListMemoShares_Handler, }, { MethodName: "DeleteMemoShare", Handler: _MemoService_DeleteMemoShare_Handler, }, { MethodName: "GetMemoByShare", Handler: _MemoService_GetMemoByShare_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "api/v1/memo_service.proto", } ================================================ FILE: proto/gen/api/v1/shortcut_service.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // 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 } ================================================ FILE: proto/gen/api/v1/shortcut_service.pb.gw.go ================================================ // 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 ) ================================================ FILE: proto/gen/api/v1/shortcut_service_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.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.Error(codes.Unimplemented, "method ListShortcuts not implemented") } func (UnimplementedShortcutServiceServer) GetShortcut(context.Context, *GetShortcutRequest) (*Shortcut, error) { return nil, status.Error(codes.Unimplemented, "method GetShortcut not implemented") } func (UnimplementedShortcutServiceServer) CreateShortcut(context.Context, *CreateShortcutRequest) (*Shortcut, error) { return nil, status.Error(codes.Unimplemented, "method CreateShortcut not implemented") } func (UnimplementedShortcutServiceServer) UpdateShortcut(context.Context, *UpdateShortcutRequest) (*Shortcut, error) { return nil, status.Error(codes.Unimplemented, "method UpdateShortcut not implemented") } func (UnimplementedShortcutServiceServer) DeleteShortcut(context.Context, *DeleteShortcutRequest) (*emptypb.Empty, error) { return nil, status.Error(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 panics, 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", } ================================================ FILE: proto/gen/api/v1/user_service.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc (unknown) // source: api/v1/user_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) ) // User role enumeration. type User_Role int32 const ( User_ROLE_UNSPECIFIED User_Role = 0 // Admin role with system access. User_ADMIN User_Role = 2 // User role with limited access. User_USER User_Role = 3 ) // Enum value maps for User_Role. var ( User_Role_name = map[int32]string{ 0: "ROLE_UNSPECIFIED", 2: "ADMIN", 3: "USER", } User_Role_value = map[string]int32{ "ROLE_UNSPECIFIED": 0, "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} } // Enumeration of user setting keys. type UserSetting_Key int32 const ( UserSetting_KEY_UNSPECIFIED UserSetting_Key = 0 // GENERAL is the key for general user settings. UserSetting_GENERAL UserSetting_Key = 1 // WEBHOOKS is the key for user webhooks. UserSetting_WEBHOOKS UserSetting_Key = 4 ) // Enum value maps for UserSetting_Key. var ( UserSetting_Key_name = map[int32]string{ 0: "KEY_UNSPECIFIED", 1: "GENERAL", 4: "WEBHOOKS", } UserSetting_Key_value = map[string]int32{ "KEY_UNSPECIFIED": 0, "GENERAL": 1, "WEBHOOKS": 4, } ) 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_api_v1_user_service_proto_enumTypes[1].Descriptor() } func (UserSetting_Key) Type() protoreflect.EnumType { return &file_api_v1_user_service_proto_enumTypes[1] } 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_api_v1_user_service_proto_rawDescGZIP(), []int{11, 0} } type UserNotification_Status int32 const ( UserNotification_STATUS_UNSPECIFIED UserNotification_Status = 0 UserNotification_UNREAD UserNotification_Status = 1 UserNotification_ARCHIVED UserNotification_Status = 2 ) // Enum value maps for UserNotification_Status. var ( UserNotification_Status_name = map[int32]string{ 0: "STATUS_UNSPECIFIED", 1: "UNREAD", 2: "ARCHIVED", } UserNotification_Status_value = map[string]int32{ "STATUS_UNSPECIFIED": 0, "UNREAD": 1, "ARCHIVED": 2, } ) func (x UserNotification_Status) Enum() *UserNotification_Status { p := new(UserNotification_Status) *p = x return p } func (x UserNotification_Status) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (UserNotification_Status) Descriptor() protoreflect.EnumDescriptor { return file_api_v1_user_service_proto_enumTypes[2].Descriptor() } func (UserNotification_Status) Type() protoreflect.EnumType { return &file_api_v1_user_service_proto_enumTypes[2] } func (x UserNotification_Status) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use UserNotification_Status.Descriptor instead. func (UserNotification_Status) EnumDescriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{28, 0} } type UserNotification_Type int32 const ( UserNotification_TYPE_UNSPECIFIED UserNotification_Type = 0 UserNotification_MEMO_COMMENT UserNotification_Type = 1 ) // Enum value maps for UserNotification_Type. var ( UserNotification_Type_name = map[int32]string{ 0: "TYPE_UNSPECIFIED", 1: "MEMO_COMMENT", } UserNotification_Type_value = map[string]int32{ "TYPE_UNSPECIFIED": 0, "MEMO_COMMENT": 1, } ) func (x UserNotification_Type) Enum() *UserNotification_Type { p := new(UserNotification_Type) *p = x return p } func (x UserNotification_Type) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (UserNotification_Type) Descriptor() protoreflect.EnumDescriptor { return file_api_v1_user_service_proto_enumTypes[3].Descriptor() } func (UserNotification_Type) Type() protoreflect.EnumType { return &file_api_v1_user_service_proto_enumTypes[3] } func (x UserNotification_Type) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use UserNotification_Type.Descriptor instead. func (UserNotification_Type) EnumDescriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{28, 1} } 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: "username == 'steven'" // Supported operators: == // Supported fields: username Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` // Optional. If true, show deleted users in the response. ShowDeleted bool `protobuf:"varint,4,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) 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. // Supports both numeric IDs and username strings: // - users/{id} (e.g., users/101) // - users/{username} (e.g., users/steven) // // Format: users/{id_or_username} 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 } // 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[7] 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[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 UserStats.ProtoReflect.Descriptor instead. func (*UserStats) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{7} } 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[8] 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[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 GetUserStatsRequest.ProtoReflect.Descriptor instead. func (*GetUserStatsRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{8} } func (x *GetUserStatsRequest) GetName() string { if x != nil { return x.Name } return "" } type ListAllUserStatsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListAllUserStatsRequest) Reset() { *x = ListAllUserStatsRequest{} mi := &file_api_v1_user_service_proto_msgTypes[9] 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[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 ListAllUserStatsRequest.ProtoReflect.Descriptor instead. func (*ListAllUserStatsRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{9} } type ListAllUserStatsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The list of user statistics. Stats []*UserStats `protobuf:"bytes,1,rep,name=stats,proto3" json:"stats,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListAllUserStatsResponse) Reset() { *x = ListAllUserStatsResponse{} mi := &file_api_v1_user_service_proto_msgTypes[10] 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[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 ListAllUserStatsResponse.ProtoReflect.Descriptor instead. func (*ListAllUserStatsResponse) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{10} } func (x *ListAllUserStatsResponse) GetStats() []*UserStats { if x != nil { return x.Stats } return nil } // User settings message type UserSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // The name of the user setting. // Format: users/{user}/settings/{setting}, {setting} is the key for the setting. // For example, "users/123/settings/GENERAL" for general settings. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Types that are valid to be assigned to Value: // // *UserSetting_GeneralSetting_ // *UserSetting_WebhooksSetting_ Value isUserSetting_Value `protobuf_oneof:"value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UserSetting) Reset() { *x = UserSetting{} mi := &file_api_v1_user_service_proto_msgTypes[11] 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[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 UserSetting.ProtoReflect.Descriptor instead. func (*UserSetting) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{11} } func (x *UserSetting) GetName() string { if x != nil { return x.Name } return "" } func (x *UserSetting) GetValue() isUserSetting_Value { if x != nil { return x.Value } return nil } func (x *UserSetting) GetGeneralSetting() *UserSetting_GeneralSetting { if x != nil { if x, ok := x.Value.(*UserSetting_GeneralSetting_); ok { return x.GeneralSetting } } return nil } func (x *UserSetting) GetWebhooksSetting() *UserSetting_WebhooksSetting { if x != nil { if x, ok := x.Value.(*UserSetting_WebhooksSetting_); ok { return x.WebhooksSetting } } return nil } type isUserSetting_Value interface { isUserSetting_Value() } type UserSetting_GeneralSetting_ struct { GeneralSetting *UserSetting_GeneralSetting `protobuf:"bytes,2,opt,name=general_setting,json=generalSetting,proto3,oneof"` } type UserSetting_WebhooksSetting_ struct { WebhooksSetting *UserSetting_WebhooksSetting `protobuf:"bytes,5,opt,name=webhooks_setting,json=webhooksSetting,proto3,oneof"` } func (*UserSetting_GeneralSetting_) isUserSetting_Value() {} func (*UserSetting_WebhooksSetting_) isUserSetting_Value() {} type GetUserSettingRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The resource name of the user setting. // Format: users/{user}/settings/{setting} 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[12] 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[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 GetUserSettingRequest.ProtoReflect.Descriptor instead. func (*GetUserSettingRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{12} } 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[13] 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[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 UpdateUserSettingRequest.ProtoReflect.Descriptor instead. func (*UpdateUserSettingRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{13} } 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 } // Request message for ListUserSettings method. type ListUserSettingsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The parent resource whose settings will be listed. // Format: users/{user} Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` // Optional. The maximum number of settings to return. // The service may return fewer than this value. // If unspecified, at most 50 settings 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 `ListUserSettings` call. // Provide this to retrieve the subsequent page. PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListUserSettingsRequest) Reset() { *x = ListUserSettingsRequest{} mi := &file_api_v1_user_service_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListUserSettingsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListUserSettingsRequest) ProtoMessage() {} func (x *ListUserSettingsRequest) 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 ListUserSettingsRequest.ProtoReflect.Descriptor instead. func (*ListUserSettingsRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{14} } func (x *ListUserSettingsRequest) GetParent() string { if x != nil { return x.Parent } return "" } func (x *ListUserSettingsRequest) GetPageSize() int32 { if x != nil { return x.PageSize } return 0 } func (x *ListUserSettingsRequest) GetPageToken() string { if x != nil { return x.PageToken } return "" } // Response message for ListUserSettings method. type ListUserSettingsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The list of user settings. Settings []*UserSetting `protobuf:"bytes,1,rep,name=settings,proto3" json:"settings,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 settings (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 *ListUserSettingsResponse) Reset() { *x = ListUserSettingsResponse{} mi := &file_api_v1_user_service_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListUserSettingsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListUserSettingsResponse) ProtoMessage() {} func (x *ListUserSettingsResponse) 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 ListUserSettingsResponse.ProtoReflect.Descriptor instead. func (*ListUserSettingsResponse) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{15} } func (x *ListUserSettingsResponse) GetSettings() []*UserSetting { if x != nil { return x.Settings } return nil } func (x *ListUserSettingsResponse) GetNextPageToken() string { if x != nil { return x.NextPageToken } return "" } func (x *ListUserSettingsResponse) GetTotalSize() int32 { if x != nil { return x.TotalSize } return 0 } // PersonalAccessToken represents a long-lived token for API/script access. // PATs are distinct from short-lived JWT access tokens used for session authentication. type PersonalAccessToken struct { state protoimpl.MessageState `protogen:"open.v1"` // The resource name of the personal access token. // Format: users/{user}/personalAccessTokens/{personal_access_token} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The description of the token. Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` // Output only. The creation timestamp. CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // Optional. The expiration timestamp. ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // Output only. The last used timestamp. LastUsedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_used_at,json=lastUsedAt,proto3" json:"last_used_at,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PersonalAccessToken) Reset() { *x = PersonalAccessToken{} mi := &file_api_v1_user_service_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PersonalAccessToken) String() string { return protoimpl.X.MessageStringOf(x) } func (*PersonalAccessToken) ProtoMessage() {} func (x *PersonalAccessToken) 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 PersonalAccessToken.ProtoReflect.Descriptor instead. func (*PersonalAccessToken) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{16} } func (x *PersonalAccessToken) GetName() string { if x != nil { return x.Name } return "" } func (x *PersonalAccessToken) GetDescription() string { if x != nil { return x.Description } return "" } func (x *PersonalAccessToken) GetCreatedAt() *timestamppb.Timestamp { if x != nil { return x.CreatedAt } return nil } func (x *PersonalAccessToken) GetExpiresAt() *timestamppb.Timestamp { if x != nil { return x.ExpiresAt } return nil } func (x *PersonalAccessToken) GetLastUsedAt() *timestamppb.Timestamp { if x != nil { return x.LastUsedAt } return nil } type ListPersonalAccessTokensRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The parent resource whose personal 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 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 *ListPersonalAccessTokensRequest) Reset() { *x = ListPersonalAccessTokensRequest{} mi := &file_api_v1_user_service_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListPersonalAccessTokensRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListPersonalAccessTokensRequest) ProtoMessage() {} func (x *ListPersonalAccessTokensRequest) 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 ListPersonalAccessTokensRequest.ProtoReflect.Descriptor instead. func (*ListPersonalAccessTokensRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{17} } func (x *ListPersonalAccessTokensRequest) GetParent() string { if x != nil { return x.Parent } return "" } func (x *ListPersonalAccessTokensRequest) GetPageSize() int32 { if x != nil { return x.PageSize } return 0 } func (x *ListPersonalAccessTokensRequest) GetPageToken() string { if x != nil { return x.PageToken } return "" } type ListPersonalAccessTokensResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The list of personal access tokens. PersonalAccessTokens []*PersonalAccessToken `protobuf:"bytes,1,rep,name=personal_access_tokens,json=personalAccessTokens,proto3" json:"personal_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 personal 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 *ListPersonalAccessTokensResponse) Reset() { *x = ListPersonalAccessTokensResponse{} mi := &file_api_v1_user_service_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListPersonalAccessTokensResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListPersonalAccessTokensResponse) ProtoMessage() {} func (x *ListPersonalAccessTokensResponse) 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 ListPersonalAccessTokensResponse.ProtoReflect.Descriptor instead. func (*ListPersonalAccessTokensResponse) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{18} } func (x *ListPersonalAccessTokensResponse) GetPersonalAccessTokens() []*PersonalAccessToken { if x != nil { return x.PersonalAccessTokens } return nil } func (x *ListPersonalAccessTokensResponse) GetNextPageToken() string { if x != nil { return x.NextPageToken } return "" } func (x *ListPersonalAccessTokensResponse) GetTotalSize() int32 { if x != nil { return x.TotalSize } return 0 } type CreatePersonalAccessTokenRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The parent resource where this token will be created. // Format: users/{user} Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` // Optional. Description of the personal access token. Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` // Optional. Expiration duration in days (0 = never expires). ExpiresInDays int32 `protobuf:"varint,3,opt,name=expires_in_days,json=expiresInDays,proto3" json:"expires_in_days,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CreatePersonalAccessTokenRequest) Reset() { *x = CreatePersonalAccessTokenRequest{} mi := &file_api_v1_user_service_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CreatePersonalAccessTokenRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreatePersonalAccessTokenRequest) ProtoMessage() {} func (x *CreatePersonalAccessTokenRequest) 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 CreatePersonalAccessTokenRequest.ProtoReflect.Descriptor instead. func (*CreatePersonalAccessTokenRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{19} } func (x *CreatePersonalAccessTokenRequest) GetParent() string { if x != nil { return x.Parent } return "" } func (x *CreatePersonalAccessTokenRequest) GetDescription() string { if x != nil { return x.Description } return "" } func (x *CreatePersonalAccessTokenRequest) GetExpiresInDays() int32 { if x != nil { return x.ExpiresInDays } return 0 } type CreatePersonalAccessTokenResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The personal access token metadata. PersonalAccessToken *PersonalAccessToken `protobuf:"bytes,1,opt,name=personal_access_token,json=personalAccessToken,proto3" json:"personal_access_token,omitempty"` // The actual token value - only returned on creation. // This is the only time the token value will be visible. Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CreatePersonalAccessTokenResponse) Reset() { *x = CreatePersonalAccessTokenResponse{} mi := &file_api_v1_user_service_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CreatePersonalAccessTokenResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreatePersonalAccessTokenResponse) ProtoMessage() {} func (x *CreatePersonalAccessTokenResponse) 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 CreatePersonalAccessTokenResponse.ProtoReflect.Descriptor instead. func (*CreatePersonalAccessTokenResponse) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{20} } func (x *CreatePersonalAccessTokenResponse) GetPersonalAccessToken() *PersonalAccessToken { if x != nil { return x.PersonalAccessToken } return nil } func (x *CreatePersonalAccessTokenResponse) GetToken() string { if x != nil { return x.Token } return "" } type DeletePersonalAccessTokenRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The resource name of the personal access token to delete. // Format: users/{user}/personalAccessTokens/{personal_access_token} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeletePersonalAccessTokenRequest) Reset() { *x = DeletePersonalAccessTokenRequest{} mi := &file_api_v1_user_service_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DeletePersonalAccessTokenRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeletePersonalAccessTokenRequest) ProtoMessage() {} func (x *DeletePersonalAccessTokenRequest) 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 DeletePersonalAccessTokenRequest.ProtoReflect.Descriptor instead. func (*DeletePersonalAccessTokenRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{21} } func (x *DeletePersonalAccessTokenRequest) GetName() string { if x != nil { return x.Name } return "" } // UserWebhook represents a webhook owned by a user. type UserWebhook struct { state protoimpl.MessageState `protogen:"open.v1"` // The name of the webhook. // Format: users/{user}/webhooks/{webhook} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The URL to send the webhook to. Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` // Optional. Human-readable name for the webhook. DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // The creation time of the webhook. CreateTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` // The last update time of the webhook. UpdateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UserWebhook) Reset() { *x = UserWebhook{} mi := &file_api_v1_user_service_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *UserWebhook) String() string { return protoimpl.X.MessageStringOf(x) } func (*UserWebhook) ProtoMessage() {} func (x *UserWebhook) 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 UserWebhook.ProtoReflect.Descriptor instead. func (*UserWebhook) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{22} } func (x *UserWebhook) GetName() string { if x != nil { return x.Name } return "" } func (x *UserWebhook) GetUrl() string { if x != nil { return x.Url } return "" } func (x *UserWebhook) GetDisplayName() string { if x != nil { return x.DisplayName } return "" } func (x *UserWebhook) GetCreateTime() *timestamppb.Timestamp { if x != nil { return x.CreateTime } return nil } func (x *UserWebhook) GetUpdateTime() *timestamppb.Timestamp { if x != nil { return x.UpdateTime } return nil } type ListUserWebhooksRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The parent user resource. // Format: users/{user} Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListUserWebhooksRequest) Reset() { *x = ListUserWebhooksRequest{} mi := &file_api_v1_user_service_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListUserWebhooksRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListUserWebhooksRequest) ProtoMessage() {} func (x *ListUserWebhooksRequest) 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 ListUserWebhooksRequest.ProtoReflect.Descriptor instead. func (*ListUserWebhooksRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{23} } func (x *ListUserWebhooksRequest) GetParent() string { if x != nil { return x.Parent } return "" } type ListUserWebhooksResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The list of webhooks. Webhooks []*UserWebhook `protobuf:"bytes,1,rep,name=webhooks,proto3" json:"webhooks,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListUserWebhooksResponse) Reset() { *x = ListUserWebhooksResponse{} mi := &file_api_v1_user_service_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListUserWebhooksResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListUserWebhooksResponse) ProtoMessage() {} func (x *ListUserWebhooksResponse) 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 ListUserWebhooksResponse.ProtoReflect.Descriptor instead. func (*ListUserWebhooksResponse) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{24} } func (x *ListUserWebhooksResponse) GetWebhooks() []*UserWebhook { if x != nil { return x.Webhooks } return nil } type CreateUserWebhookRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The parent user resource. // Format: users/{user} Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` // The webhook to create. Webhook *UserWebhook `protobuf:"bytes,2,opt,name=webhook,proto3" json:"webhook,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CreateUserWebhookRequest) Reset() { *x = CreateUserWebhookRequest{} mi := &file_api_v1_user_service_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CreateUserWebhookRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateUserWebhookRequest) ProtoMessage() {} func (x *CreateUserWebhookRequest) 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 CreateUserWebhookRequest.ProtoReflect.Descriptor instead. func (*CreateUserWebhookRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{25} } func (x *CreateUserWebhookRequest) GetParent() string { if x != nil { return x.Parent } return "" } func (x *CreateUserWebhookRequest) GetWebhook() *UserWebhook { if x != nil { return x.Webhook } return nil } type UpdateUserWebhookRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The webhook to update. Webhook *UserWebhook `protobuf:"bytes,1,opt,name=webhook,proto3" json:"webhook,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 *UpdateUserWebhookRequest) Reset() { *x = UpdateUserWebhookRequest{} mi := &file_api_v1_user_service_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *UpdateUserWebhookRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*UpdateUserWebhookRequest) ProtoMessage() {} func (x *UpdateUserWebhookRequest) ProtoReflect() protoreflect.Message { mi := &file_api_v1_user_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 UpdateUserWebhookRequest.ProtoReflect.Descriptor instead. func (*UpdateUserWebhookRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{26} } func (x *UpdateUserWebhookRequest) GetWebhook() *UserWebhook { if x != nil { return x.Webhook } return nil } func (x *UpdateUserWebhookRequest) GetUpdateMask() *fieldmaskpb.FieldMask { if x != nil { return x.UpdateMask } return nil } type DeleteUserWebhookRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The 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 *DeleteUserWebhookRequest) Reset() { *x = DeleteUserWebhookRequest{} mi := &file_api_v1_user_service_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DeleteUserWebhookRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteUserWebhookRequest) ProtoMessage() {} func (x *DeleteUserWebhookRequest) 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 DeleteUserWebhookRequest.ProtoReflect.Descriptor instead. func (*DeleteUserWebhookRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{27} } func (x *DeleteUserWebhookRequest) GetName() string { if x != nil { return x.Name } return "" } type UserNotification struct { state protoimpl.MessageState `protogen:"open.v1"` // The resource name of the notification. // Format: users/{user}/notifications/{notification} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The sender of the notification. // Format: users/{user} Sender string `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"` // The status of the notification. Status UserNotification_Status `protobuf:"varint,3,opt,name=status,proto3,enum=memos.api.v1.UserNotification_Status" json:"status,omitempty"` // The creation timestamp. CreateTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` // The type of the notification. Type UserNotification_Type `protobuf:"varint,5,opt,name=type,proto3,enum=memos.api.v1.UserNotification_Type" json:"type,omitempty"` // Types that are valid to be assigned to Payload: // // *UserNotification_MemoComment Payload isUserNotification_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UserNotification) Reset() { *x = UserNotification{} mi := &file_api_v1_user_service_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *UserNotification) String() string { return protoimpl.X.MessageStringOf(x) } func (*UserNotification) ProtoMessage() {} func (x *UserNotification) 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 UserNotification.ProtoReflect.Descriptor instead. func (*UserNotification) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{28} } func (x *UserNotification) GetName() string { if x != nil { return x.Name } return "" } func (x *UserNotification) GetSender() string { if x != nil { return x.Sender } return "" } func (x *UserNotification) GetStatus() UserNotification_Status { if x != nil { return x.Status } return UserNotification_STATUS_UNSPECIFIED } func (x *UserNotification) GetCreateTime() *timestamppb.Timestamp { if x != nil { return x.CreateTime } return nil } func (x *UserNotification) GetType() UserNotification_Type { if x != nil { return x.Type } return UserNotification_TYPE_UNSPECIFIED } func (x *UserNotification) GetPayload() isUserNotification_Payload { if x != nil { return x.Payload } return nil } func (x *UserNotification) GetMemoComment() *UserNotification_MemoCommentPayload { if x != nil { if x, ok := x.Payload.(*UserNotification_MemoComment); ok { return x.MemoComment } } return nil } type isUserNotification_Payload interface { isUserNotification_Payload() } type UserNotification_MemoComment struct { MemoComment *UserNotification_MemoCommentPayload `protobuf:"bytes,6,opt,name=memo_comment,json=memoComment,proto3,oneof"` } func (*UserNotification_MemoComment) isUserNotification_Payload() {} type ListUserNotificationsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The parent user resource. // Format: users/{user} Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` Filter string `protobuf:"bytes,4,opt,name=filter,proto3" json:"filter,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListUserNotificationsRequest) Reset() { *x = ListUserNotificationsRequest{} mi := &file_api_v1_user_service_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListUserNotificationsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListUserNotificationsRequest) ProtoMessage() {} func (x *ListUserNotificationsRequest) ProtoReflect() protoreflect.Message { mi := &file_api_v1_user_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 ListUserNotificationsRequest.ProtoReflect.Descriptor instead. func (*ListUserNotificationsRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{29} } func (x *ListUserNotificationsRequest) GetParent() string { if x != nil { return x.Parent } return "" } func (x *ListUserNotificationsRequest) GetPageSize() int32 { if x != nil { return x.PageSize } return 0 } func (x *ListUserNotificationsRequest) GetPageToken() string { if x != nil { return x.PageToken } return "" } func (x *ListUserNotificationsRequest) GetFilter() string { if x != nil { return x.Filter } return "" } type ListUserNotificationsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Notifications []*UserNotification `protobuf:"bytes,1,rep,name=notifications,proto3" json:"notifications,omitempty"` 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 *ListUserNotificationsResponse) Reset() { *x = ListUserNotificationsResponse{} mi := &file_api_v1_user_service_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListUserNotificationsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListUserNotificationsResponse) ProtoMessage() {} func (x *ListUserNotificationsResponse) ProtoReflect() protoreflect.Message { mi := &file_api_v1_user_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 ListUserNotificationsResponse.ProtoReflect.Descriptor instead. func (*ListUserNotificationsResponse) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{30} } func (x *ListUserNotificationsResponse) GetNotifications() []*UserNotification { if x != nil { return x.Notifications } return nil } func (x *ListUserNotificationsResponse) GetNextPageToken() string { if x != nil { return x.NextPageToken } return "" } type UpdateUserNotificationRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Notification *UserNotification `protobuf:"bytes,1,opt,name=notification,proto3" json:"notification,omitempty"` 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 *UpdateUserNotificationRequest) Reset() { *x = UpdateUserNotificationRequest{} mi := &file_api_v1_user_service_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *UpdateUserNotificationRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*UpdateUserNotificationRequest) ProtoMessage() {} func (x *UpdateUserNotificationRequest) ProtoReflect() protoreflect.Message { mi := &file_api_v1_user_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 UpdateUserNotificationRequest.ProtoReflect.Descriptor instead. func (*UpdateUserNotificationRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{31} } func (x *UpdateUserNotificationRequest) GetNotification() *UserNotification { if x != nil { return x.Notification } return nil } func (x *UpdateUserNotificationRequest) GetUpdateMask() *fieldmaskpb.FieldMask { if x != nil { return x.UpdateMask } return nil } type DeleteUserNotificationRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Format: users/{user}/notifications/{notification} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeleteUserNotificationRequest) Reset() { *x = DeleteUserNotificationRequest{} mi := &file_api_v1_user_service_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DeleteUserNotificationRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteUserNotificationRequest) ProtoMessage() {} func (x *DeleteUserNotificationRequest) ProtoReflect() protoreflect.Message { mi := &file_api_v1_user_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 DeleteUserNotificationRequest.ProtoReflect.Descriptor instead. func (*DeleteUserNotificationRequest) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{32} } func (x *DeleteUserNotificationRequest) GetName() string { if x != nil { return x.Name } return "" } // 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[34] 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[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 UserStats_MemoTypeStats.ProtoReflect.Descriptor instead. func (*UserStats_MemoTypeStats) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{7, 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 } // General user settings configuration. type UserSetting_GeneralSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // The preferred locale of the user. Locale string `protobuf:"bytes,1,opt,name=locale,proto3" json:"locale,omitempty"` // The default visibility of the memo. MemoVisibility string `protobuf:"bytes,3,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,4,opt,name=theme,proto3" json:"theme,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UserSetting_GeneralSetting) Reset() { *x = UserSetting_GeneralSetting{} mi := &file_api_v1_user_service_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *UserSetting_GeneralSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*UserSetting_GeneralSetting) ProtoMessage() {} func (x *UserSetting_GeneralSetting) ProtoReflect() protoreflect.Message { mi := &file_api_v1_user_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 UserSetting_GeneralSetting.ProtoReflect.Descriptor instead. func (*UserSetting_GeneralSetting) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{11, 0} } func (x *UserSetting_GeneralSetting) GetLocale() string { if x != nil { return x.Locale } return "" } func (x *UserSetting_GeneralSetting) GetMemoVisibility() string { if x != nil { return x.MemoVisibility } return "" } func (x *UserSetting_GeneralSetting) GetTheme() string { if x != nil { return x.Theme } return "" } // User webhooks configuration. type UserSetting_WebhooksSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // List of user webhooks. Webhooks []*UserWebhook `protobuf:"bytes,1,rep,name=webhooks,proto3" json:"webhooks,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UserSetting_WebhooksSetting) Reset() { *x = UserSetting_WebhooksSetting{} mi := &file_api_v1_user_service_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *UserSetting_WebhooksSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*UserSetting_WebhooksSetting) ProtoMessage() {} func (x *UserSetting_WebhooksSetting) ProtoReflect() protoreflect.Message { mi := &file_api_v1_user_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 UserSetting_WebhooksSetting.ProtoReflect.Descriptor instead. func (*UserSetting_WebhooksSetting) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{11, 1} } func (x *UserSetting_WebhooksSetting) GetWebhooks() []*UserWebhook { if x != nil { return x.Webhooks } return nil } type UserNotification_MemoCommentPayload 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 *UserNotification_MemoCommentPayload) Reset() { *x = UserNotification_MemoCommentPayload{} mi := &file_api_v1_user_service_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *UserNotification_MemoCommentPayload) String() string { return protoimpl.X.MessageStringOf(x) } func (*UserNotification_MemoCommentPayload) ProtoMessage() {} func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Message { mi := &file_api_v1_user_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 UserNotification_MemoCommentPayload.ProtoReflect.Descriptor instead. func (*UserNotification_MemoCommentPayload) Descriptor() ([]byte, []int) { return file_api_v1_user_service_proto_rawDescGZIP(), []int{28, 0} } func (x *UserNotification_MemoCommentPayload) GetMemo() string { if x != nil { return x.Memo } return "" } func (x *UserNotification_MemoCommentPayload) GetRelatedMemo() string { if x != nil { return x.RelatedMemo } 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/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc1\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\"1\n" + "\x04Role\x12\x14\n" + "\x10ROLE_UNSPECIFIED\x10\x00\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\"\x9d\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&\n" + "\fshow_deleted\x18\x04 \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\"\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\"\x19\n" + "\x17ListAllUserStatsRequest\"I\n" + "\x18ListAllUserStatsResponse\x12-\n" + "\x05stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\x05stats\"\xb0\x04\n" + "\vUserSetting\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12S\n" + "\x0fgeneral_setting\x18\x02 \x01(\v2(.memos.api.v1.UserSetting.GeneralSettingH\x00R\x0egeneralSetting\x12V\n" + "\x10webhooks_setting\x18\x05 \x01(\v2).memos.api.v1.UserSetting.WebhooksSettingH\x00R\x0fwebhooksSetting\x1av\n" + "\x0eGeneralSetting\x12\x1b\n" + "\x06locale\x18\x01 \x01(\tB\x03\xe0A\x01R\x06locale\x12,\n" + "\x0fmemo_visibility\x18\x03 \x01(\tB\x03\xe0A\x01R\x0ememoVisibility\x12\x19\n" + "\x05theme\x18\x04 \x01(\tB\x03\xe0A\x01R\x05theme\x1aH\n" + "\x0fWebhooksSetting\x125\n" + "\bwebhooks\x18\x01 \x03(\v2\x19.memos.api.v1.UserWebhookR\bwebhooks\"5\n" + "\x03Key\x12\x13\n" + "\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" + "\aGENERAL\x10\x01\x12\f\n" + "\bWEBHOOKS\x10\x04:Y\xeaAV\n" + "\x18memos.api.v1/UserSetting\x12\x1fusers/{user}/settings/{setting}*\fuserSettings2\vuserSettingB\a\n" + "\x05value\"M\n" + "\x15GetUserSettingRequest\x124\n" + "\x04name\x18\x01 \x01(\tB \xe0A\x02\xfaA\x1a\n" + "\x18memos.api.v1/UserSettingR\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\"\x92\x01\n" + "\x17ListUserSettingsRequest\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\"\x98\x01\n" + "\x18ListUserSettingsResponse\x125\n" + "\bsettings\x18\x01 \x03(\v2\x19.memos.api.v1.UserSettingR\bsettings\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + "\n" + "total_size\x18\x03 \x01(\x05R\ttotalSize\"\xa7\x03\n" + "\x13PersonalAccessToken\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12%\n" + "\vdescription\x18\x02 \x01(\tB\x03\xe0A\x01R\vdescription\x12>\n" + "\n" + "created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\tcreatedAt\x12>\n" + "\n" + "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x01R\texpiresAt\x12A\n" + "\flast_used_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + "lastUsedAt:\x8c\x01\xeaA\x88\x01\n" + " memos.api.v1/PersonalAccessToken\x129users/{user}/personalAccessTokens/{personal_access_token}*\x14personalAccessTokens2\x13personalAccessToken\"\x9a\x01\n" + "\x1fListPersonalAccessTokensRequest\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\"\xc2\x01\n" + " ListPersonalAccessTokensResponse\x12W\n" + "\x16personal_access_tokens\x18\x01 \x03(\v2!.memos.api.v1.PersonalAccessTokenR\x14personalAccessTokens\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + "\n" + "total_size\x18\x03 \x01(\x05R\ttotalSize\"\xa9\x01\n" + " CreatePersonalAccessTokenRequest\x121\n" + "\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + "\x11memos.api.v1/UserR\x06parent\x12%\n" + "\vdescription\x18\x02 \x01(\tB\x03\xe0A\x01R\vdescription\x12+\n" + "\x0fexpires_in_days\x18\x03 \x01(\x05B\x03\xe0A\x01R\rexpiresInDays\"\x90\x01\n" + "!CreatePersonalAccessTokenResponse\x12U\n" + "\x15personal_access_token\x18\x01 \x01(\v2!.memos.api.v1.PersonalAccessTokenR\x13personalAccessToken\x12\x14\n" + "\x05token\x18\x02 \x01(\tR\x05token\"`\n" + " DeletePersonalAccessTokenRequest\x12<\n" + "\x04name\x18\x01 \x01(\tB(\xe0A\x02\xfaA\"\n" + " memos.api.v1/PersonalAccessTokenR\x04name\"\xda\x01\n" + "\vUserWebhook\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + "\x03url\x18\x02 \x01(\tR\x03url\x12!\n" + "\fdisplay_name\x18\x03 \x01(\tR\vdisplayName\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\"6\n" + "\x17ListUserWebhooksRequest\x12\x1b\n" + "\x06parent\x18\x01 \x01(\tB\x03\xe0A\x02R\x06parent\"Q\n" + "\x18ListUserWebhooksResponse\x125\n" + "\bwebhooks\x18\x01 \x03(\v2\x19.memos.api.v1.UserWebhookR\bwebhooks\"q\n" + "\x18CreateUserWebhookRequest\x12\x1b\n" + "\x06parent\x18\x01 \x01(\tB\x03\xe0A\x02R\x06parent\x128\n" + "\awebhook\x18\x02 \x01(\v2\x19.memos.api.v1.UserWebhookB\x03\xe0A\x02R\awebhook\"\x91\x01\n" + "\x18UpdateUserWebhookRequest\x128\n" + "\awebhook\x18\x01 \x01(\v2\x19.memos.api.v1.UserWebhookB\x03\xe0A\x02R\awebhook\x12;\n" + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskR\n" + "updateMask\"3\n" + "\x18DeleteUserWebhookRequest\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\x02R\x04name\"\xb8\x05\n" + "\x10UserNotification\x12\x1a\n" + "\x04name\x18\x01 \x01(\tB\x06\xe0A\x03\xe0A\bR\x04name\x121\n" + "\x06sender\x18\x02 \x01(\tB\x19\xe0A\x03\xfaA\x13\n" + "\x11memos.api.v1/UserR\x06sender\x12B\n" + "\x06status\x18\x03 \x01(\x0e2%.memos.api.v1.UserNotification.StatusB\x03\xe0A\x01R\x06status\x12@\n" + "\vcreate_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + "createTime\x12<\n" + "\x04type\x18\x05 \x01(\x0e2#.memos.api.v1.UserNotification.TypeB\x03\xe0A\x03R\x04type\x12[\n" + "\fmemo_comment\x18\x06 \x01(\v21.memos.api.v1.UserNotification.MemoCommentPayloadB\x03\xe0A\x03H\x00R\vmemoComment\x1aK\n" + "\x12MemoCommentPayload\x12\x12\n" + "\x04memo\x18\x01 \x01(\tR\x04memo\x12!\n" + "\frelated_memo\x18\x02 \x01(\tR\vrelatedMemo\":\n" + "\x06Status\x12\x16\n" + "\x12STATUS_UNSPECIFIED\x10\x00\x12\n" + "\n" + "\x06UNREAD\x10\x01\x12\f\n" + "\bARCHIVED\x10\x02\".\n" + "\x04Type\x12\x14\n" + "\x10TYPE_UNSPECIFIED\x10\x00\x12\x10\n" + "\fMEMO_COMMENT\x10\x01:p\xeaAm\n" + "\x1dmemos.api.v1/UserNotification\x12)users/{user}/notifications/{notification}\x1a\x04name*\rnotifications2\fnotificationB\t\n" + "\apayload\"\xb4\x01\n" + "\x1cListUserNotificationsRequest\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\"\x8d\x01\n" + "\x1dListUserNotificationsResponse\x12D\n" + "\rnotifications\x18\x01 \x03(\v2\x1e.memos.api.v1.UserNotificationR\rnotifications\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xaa\x01\n" + "\x1dUpdateUserNotificationRequest\x12G\n" + "\fnotification\x18\x01 \x01(\v2\x1e.memos.api.v1.UserNotificationB\x03\xe0A\x02R\fnotification\x12@\n" + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + "updateMask\"Z\n" + "\x1dDeleteUserNotificationRequest\x129\n" + "\x04name\x18\x01 \x01(\tB%\xe0A\x02\xfaA\x1f\n" + "\x1dmemos.api.v1/UserNotificationR\x04name2\x83\x17\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/*}\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/*/settings/*}\x12\xa8\x01\n" + "\x11UpdateUserSetting\x12&.memos.api.v1.UpdateUserSettingRequest\x1a\x19.memos.api.v1.UserSetting\"P\xdaA\x13setting,update_mask\x82\xd3\xe4\x93\x024:\asetting2)/api/v1/{setting.name=users/*/settings/*}\x12\x95\x01\n" + "\x10ListUserSettings\x12%.memos.api.v1.ListUserSettingsRequest\x1a&.memos.api.v1.ListUserSettingsResponse\"2\xdaA\x06parent\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=users/*}/settings\x12\xb9\x01\n" + "\x18ListPersonalAccessTokens\x12-.memos.api.v1.ListPersonalAccessTokensRequest\x1a..memos.api.v1.ListPersonalAccessTokensResponse\">\xdaA\x06parent\x82\xd3\xe4\x93\x02/\x12-/api/v1/{parent=users/*}/personalAccessTokens\x12\xb6\x01\n" + "\x19CreatePersonalAccessToken\x12..memos.api.v1.CreatePersonalAccessTokenRequest\x1a/.memos.api.v1.CreatePersonalAccessTokenResponse\"8\x82\xd3\xe4\x93\x022:\x01*\"-/api/v1/{parent=users/*}/personalAccessTokens\x12\xa1\x01\n" + "\x19DeletePersonalAccessToken\x12..memos.api.v1.DeletePersonalAccessTokenRequest\x1a\x16.google.protobuf.Empty\"<\xdaA\x04name\x82\xd3\xe4\x93\x02/*-/api/v1/{name=users/*/personalAccessTokens/*}\x12\x95\x01\n" + "\x10ListUserWebhooks\x12%.memos.api.v1.ListUserWebhooksRequest\x1a&.memos.api.v1.ListUserWebhooksResponse\"2\xdaA\x06parent\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=users/*}/webhooks\x12\x9b\x01\n" + "\x11CreateUserWebhook\x12&.memos.api.v1.CreateUserWebhookRequest\x1a\x19.memos.api.v1.UserWebhook\"C\xdaA\x0eparent,webhook\x82\xd3\xe4\x93\x02,:\awebhook\"!/api/v1/{parent=users/*}/webhooks\x12\xa8\x01\n" + "\x11UpdateUserWebhook\x12&.memos.api.v1.UpdateUserWebhookRequest\x1a\x19.memos.api.v1.UserWebhook\"P\xdaA\x13webhook,update_mask\x82\xd3\xe4\x93\x024:\awebhook2)/api/v1/{webhook.name=users/*/webhooks/*}\x12\x85\x01\n" + "\x11DeleteUserWebhook\x12&.memos.api.v1.DeleteUserWebhookRequest\x1a\x16.google.protobuf.Empty\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#*!/api/v1/{name=users/*/webhooks/*}\x12\xa9\x01\n" + "\x15ListUserNotifications\x12*.memos.api.v1.ListUserNotificationsRequest\x1a+.memos.api.v1.ListUserNotificationsResponse\"7\xdaA\x06parent\x82\xd3\xe4\x93\x02(\x12&/api/v1/{parent=users/*}/notifications\x12\xcb\x01\n" + "\x16UpdateUserNotification\x12+.memos.api.v1.UpdateUserNotificationRequest\x1a\x1e.memos.api.v1.UserNotification\"d\xdaA\x18notification,update_mask\x82\xd3\xe4\x93\x02C:\fnotification23/api/v1/{notification.name=users/*/notifications/*}\x12\x94\x01\n" + "\x16DeleteUserNotification\x12+.memos.api.v1.DeleteUserNotificationRequest\x1a\x16.google.protobuf.Empty\"5\xdaA\x04name\x82\xd3\xe4\x93\x02(*&/api/v1/{name=users/*/notifications/*}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, 4) var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_api_v1_user_service_proto_goTypes = []any{ (User_Role)(0), // 0: memos.api.v1.User.Role (UserSetting_Key)(0), // 1: memos.api.v1.UserSetting.Key (UserNotification_Status)(0), // 2: memos.api.v1.UserNotification.Status (UserNotification_Type)(0), // 3: memos.api.v1.UserNotification.Type (*User)(nil), // 4: memos.api.v1.User (*ListUsersRequest)(nil), // 5: memos.api.v1.ListUsersRequest (*ListUsersResponse)(nil), // 6: memos.api.v1.ListUsersResponse (*GetUserRequest)(nil), // 7: memos.api.v1.GetUserRequest (*CreateUserRequest)(nil), // 8: memos.api.v1.CreateUserRequest (*UpdateUserRequest)(nil), // 9: memos.api.v1.UpdateUserRequest (*DeleteUserRequest)(nil), // 10: memos.api.v1.DeleteUserRequest (*UserStats)(nil), // 11: memos.api.v1.UserStats (*GetUserStatsRequest)(nil), // 12: memos.api.v1.GetUserStatsRequest (*ListAllUserStatsRequest)(nil), // 13: memos.api.v1.ListAllUserStatsRequest (*ListAllUserStatsResponse)(nil), // 14: memos.api.v1.ListAllUserStatsResponse (*UserSetting)(nil), // 15: memos.api.v1.UserSetting (*GetUserSettingRequest)(nil), // 16: memos.api.v1.GetUserSettingRequest (*UpdateUserSettingRequest)(nil), // 17: memos.api.v1.UpdateUserSettingRequest (*ListUserSettingsRequest)(nil), // 18: memos.api.v1.ListUserSettingsRequest (*ListUserSettingsResponse)(nil), // 19: memos.api.v1.ListUserSettingsResponse (*PersonalAccessToken)(nil), // 20: memos.api.v1.PersonalAccessToken (*ListPersonalAccessTokensRequest)(nil), // 21: memos.api.v1.ListPersonalAccessTokensRequest (*ListPersonalAccessTokensResponse)(nil), // 22: memos.api.v1.ListPersonalAccessTokensResponse (*CreatePersonalAccessTokenRequest)(nil), // 23: memos.api.v1.CreatePersonalAccessTokenRequest (*CreatePersonalAccessTokenResponse)(nil), // 24: memos.api.v1.CreatePersonalAccessTokenResponse (*DeletePersonalAccessTokenRequest)(nil), // 25: memos.api.v1.DeletePersonalAccessTokenRequest (*UserWebhook)(nil), // 26: memos.api.v1.UserWebhook (*ListUserWebhooksRequest)(nil), // 27: memos.api.v1.ListUserWebhooksRequest (*ListUserWebhooksResponse)(nil), // 28: memos.api.v1.ListUserWebhooksResponse (*CreateUserWebhookRequest)(nil), // 29: memos.api.v1.CreateUserWebhookRequest (*UpdateUserWebhookRequest)(nil), // 30: memos.api.v1.UpdateUserWebhookRequest (*DeleteUserWebhookRequest)(nil), // 31: memos.api.v1.DeleteUserWebhookRequest (*UserNotification)(nil), // 32: memos.api.v1.UserNotification (*ListUserNotificationsRequest)(nil), // 33: memos.api.v1.ListUserNotificationsRequest (*ListUserNotificationsResponse)(nil), // 34: memos.api.v1.ListUserNotificationsResponse (*UpdateUserNotificationRequest)(nil), // 35: memos.api.v1.UpdateUserNotificationRequest (*DeleteUserNotificationRequest)(nil), // 36: memos.api.v1.DeleteUserNotificationRequest nil, // 37: memos.api.v1.UserStats.TagCountEntry (*UserStats_MemoTypeStats)(nil), // 38: memos.api.v1.UserStats.MemoTypeStats (*UserSetting_GeneralSetting)(nil), // 39: memos.api.v1.UserSetting.GeneralSetting (*UserSetting_WebhooksSetting)(nil), // 40: memos.api.v1.UserSetting.WebhooksSetting (*UserNotification_MemoCommentPayload)(nil), // 41: memos.api.v1.UserNotification.MemoCommentPayload (State)(0), // 42: memos.api.v1.State (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp (*fieldmaskpb.FieldMask)(nil), // 44: google.protobuf.FieldMask (*emptypb.Empty)(nil), // 45: google.protobuf.Empty } var file_api_v1_user_service_proto_depIdxs = []int32{ 0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role 42, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State 43, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp 43, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp 4, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User 44, // 5: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask 4, // 6: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User 4, // 7: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User 44, // 8: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask 43, // 9: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp 38, // 10: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats 37, // 11: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry 11, // 12: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats 39, // 13: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting 40, // 14: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting 15, // 15: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting 44, // 16: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask 15, // 17: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting 43, // 18: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp 43, // 19: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp 43, // 20: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp 20, // 21: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken 20, // 22: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken 43, // 23: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp 43, // 24: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp 26, // 25: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook 26, // 26: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook 26, // 27: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook 44, // 28: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask 2, // 29: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status 43, // 30: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp 3, // 31: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type 41, // 32: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload 32, // 33: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification 32, // 34: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification 44, // 35: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask 26, // 36: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook 5, // 37: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest 7, // 38: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest 8, // 39: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest 9, // 40: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest 10, // 41: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest 13, // 42: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest 12, // 43: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest 16, // 44: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest 17, // 45: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest 18, // 46: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest 21, // 47: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest 23, // 48: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest 25, // 49: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest 27, // 50: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest 29, // 51: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest 30, // 52: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest 31, // 53: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest 33, // 54: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest 35, // 55: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest 36, // 56: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest 6, // 57: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse 4, // 58: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User 4, // 59: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User 4, // 60: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User 45, // 61: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty 14, // 62: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse 11, // 63: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats 15, // 64: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting 15, // 65: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting 19, // 66: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse 22, // 67: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse 24, // 68: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse 45, // 69: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty 28, // 70: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse 26, // 71: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook 26, // 72: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook 45, // 73: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty 34, // 74: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse 32, // 75: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification 45, // 76: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty 57, // [57:77] is the sub-list for method output_type 37, // [37:57] is the sub-list for method input_type 37, // [37:37] is the sub-list for extension type_name 37, // [37:37] is the sub-list for extension extendee 0, // [0:37] 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() file_api_v1_user_service_proto_msgTypes[11].OneofWrappers = []any{ (*UserSetting_GeneralSetting_)(nil), (*UserSetting_WebhooksSetting_)(nil), } file_api_v1_user_service_proto_msgTypes[28].OneofWrappers = []any{ (*UserNotification_MemoComment)(nil), } 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: 4, NumMessages: 38, 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 } ================================================ FILE: proto/gen/api/v1/user_service.pb.gw.go ================================================ // 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 } 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) } 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 ) 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_ListUserSettings_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} func request_UserService_ListUserSettings_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq ListUserSettingsRequest 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_ListUserSettings_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.ListUserSettings(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_ListUserSettings_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq ListUserSettingsRequest 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_ListUserSettings_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.ListUserSettings(ctx, &protoReq) return msg, metadata, err } var filter_UserService_ListPersonalAccessTokens_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} func request_UserService_ListPersonalAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq ListPersonalAccessTokensRequest 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_ListPersonalAccessTokens_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.ListPersonalAccessTokens(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_ListPersonalAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq ListPersonalAccessTokensRequest 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_ListPersonalAccessTokens_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.ListPersonalAccessTokens(ctx, &protoReq) return msg, metadata, err } func request_UserService_CreatePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq CreatePersonalAccessTokenRequest 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.CreatePersonalAccessToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_CreatePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq CreatePersonalAccessTokenRequest 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.CreatePersonalAccessToken(ctx, &protoReq) return msg, metadata, err } func request_UserService_DeletePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq DeletePersonalAccessTokenRequest 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.DeletePersonalAccessToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_DeletePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq DeletePersonalAccessTokenRequest 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.DeletePersonalAccessToken(ctx, &protoReq) return msg, metadata, err } func request_UserService_ListUserWebhooks_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq ListUserWebhooksRequest 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.ListUserWebhooks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_ListUserWebhooks_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq ListUserWebhooksRequest 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.ListUserWebhooks(ctx, &protoReq) return msg, metadata, err } func request_UserService_CreateUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq CreateUserWebhookRequest 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) } msg, err := client.CreateUserWebhook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_CreateUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq CreateUserWebhookRequest 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) } msg, err := server.CreateUserWebhook(ctx, &protoReq) return msg, metadata, err } var filter_UserService_UpdateUserWebhook_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_UserService_UpdateUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq UpdateUserWebhookRequest 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_UserService_UpdateUserWebhook_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.UpdateUserWebhook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_UpdateUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq UpdateUserWebhookRequest 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_UserService_UpdateUserWebhook_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.UpdateUserWebhook(ctx, &protoReq) return msg, metadata, err } func request_UserService_DeleteUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq DeleteUserWebhookRequest 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.DeleteUserWebhook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_DeleteUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq DeleteUserWebhookRequest 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.DeleteUserWebhook(ctx, &protoReq) return msg, metadata, err } var filter_UserService_ListUserNotifications_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} func request_UserService_ListUserNotifications_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq ListUserNotificationsRequest 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_ListUserNotifications_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.ListUserNotifications(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_ListUserNotifications_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq ListUserNotificationsRequest 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_ListUserNotifications_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.ListUserNotifications(ctx, &protoReq) return msg, metadata, err } var filter_UserService_UpdateUserNotification_0 = &utilities.DoubleArray{Encoding: map[string]int{"notification": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} func request_UserService_UpdateUserNotification_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq UpdateUserNotificationRequest 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.Notification); 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.Notification); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } else { protoReq.UpdateMask = fieldMask } } val, ok := pathParams["notification.name"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "notification.name") } err = runtime.PopulateFieldFromPath(&protoReq, "notification.name", val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "notification.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_UpdateUserNotification_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.UpdateUserNotification(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_UpdateUserNotification_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq UpdateUserNotificationRequest 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.Notification); 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.Notification); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } else { protoReq.UpdateMask = fieldMask } } val, ok := pathParams["notification.name"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "notification.name") } err = runtime.PopulateFieldFromPath(&protoReq, "notification.name", val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "notification.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_UpdateUserNotification_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.UpdateUserNotification(ctx, &protoReq) return msg, metadata, err } func request_UserService_DeleteUserNotification_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq DeleteUserNotificationRequest 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.DeleteUserNotification(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_UserService_DeleteUserNotification_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq DeleteUserNotificationRequest 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.DeleteUserNotification(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_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/*/settings/*}")) 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/*/settings/*}")) 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_ListUserSettings_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/ListUserSettings", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/settings")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_UserService_ListUserSettings_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_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_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/ListPersonalAccessTokens", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/personalAccessTokens")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_UserService_ListPersonalAccessTokens_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_ListPersonalAccessTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPost, pattern_UserService_CreatePersonalAccessToken_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/CreatePersonalAccessToken", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/personalAccessTokens")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_UserService_CreatePersonalAccessToken_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_CreatePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodDelete, pattern_UserService_DeletePersonalAccessToken_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/DeletePersonalAccessToken", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/personalAccessTokens/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_UserService_DeletePersonalAccessToken_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_DeletePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_UserService_ListUserWebhooks_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/ListUserWebhooks", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_UserService_ListUserWebhooks_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_ListUserWebhooks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPost, pattern_UserService_CreateUserWebhook_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/CreateUserWebhook", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_UserService_CreateUserWebhook_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_CreateUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPatch, pattern_UserService_UpdateUserWebhook_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/UpdateUserWebhook", 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_UserService_UpdateUserWebhook_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_UpdateUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodDelete, pattern_UserService_DeleteUserWebhook_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/DeleteUserWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/webhooks/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_UserService_DeleteUserWebhook_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_DeleteUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_UserService_ListUserNotifications_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/ListUserNotifications", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/notifications")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_UserService_ListUserNotifications_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_ListUserNotifications_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPatch, pattern_UserService_UpdateUserNotification_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/UpdateUserNotification", runtime.WithHTTPPathPattern("/api/v1/{notification.name=users/*/notifications/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_UserService_UpdateUserNotification_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_UpdateUserNotification_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodDelete, pattern_UserService_DeleteUserNotification_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/DeleteUserNotification", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/notifications/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_UserService_DeleteUserNotification_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_DeleteUserNotification_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_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/*/settings/*}")) 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/*/settings/*}")) 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_ListUserSettings_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/ListUserSettings", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/settings")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_ListUserSettings_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_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_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/ListPersonalAccessTokens", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/personalAccessTokens")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_ListPersonalAccessTokens_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_ListPersonalAccessTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPost, pattern_UserService_CreatePersonalAccessToken_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/CreatePersonalAccessToken", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/personalAccessTokens")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_CreatePersonalAccessToken_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_CreatePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodDelete, pattern_UserService_DeletePersonalAccessToken_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/DeletePersonalAccessToken", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/personalAccessTokens/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_DeletePersonalAccessToken_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_DeletePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_UserService_ListUserWebhooks_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/ListUserWebhooks", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_ListUserWebhooks_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_ListUserWebhooks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPost, pattern_UserService_CreateUserWebhook_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/CreateUserWebhook", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_CreateUserWebhook_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_CreateUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPatch, pattern_UserService_UpdateUserWebhook_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/UpdateUserWebhook", runtime.WithHTTPPathPattern("/api/v1/{webhook.name=users/*/webhooks/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_UpdateUserWebhook_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_UpdateUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodDelete, pattern_UserService_DeleteUserWebhook_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/DeleteUserWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/webhooks/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_DeleteUserWebhook_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_DeleteUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodGet, pattern_UserService_ListUserNotifications_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/ListUserNotifications", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/notifications")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_ListUserNotifications_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_ListUserNotifications_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodPatch, pattern_UserService_UpdateUserNotification_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/UpdateUserNotification", runtime.WithHTTPPathPattern("/api/v1/{notification.name=users/*/notifications/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_UpdateUserNotification_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_UpdateUserNotification_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle(http.MethodDelete, pattern_UserService_DeleteUserNotification_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/DeleteUserNotification", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/notifications/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_UserService_DeleteUserNotification_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_DeleteUserNotification_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_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, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "name"}, "")) pattern_UserService_UpdateUserSetting_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", "settings", "setting.name"}, "")) pattern_UserService_ListUserSettings_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", "settings"}, "")) pattern_UserService_ListPersonalAccessTokens_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", "personalAccessTokens"}, "")) pattern_UserService_CreatePersonalAccessToken_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", "personalAccessTokens"}, "")) pattern_UserService_DeletePersonalAccessToken_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", "personalAccessTokens", "name"}, "")) pattern_UserService_ListUserWebhooks_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_UserService_CreateUserWebhook_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_UserService_UpdateUserWebhook_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_UserService_DeleteUserWebhook_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_UserService_ListUserNotifications_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", "notifications"}, "")) pattern_UserService_UpdateUserNotification_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", "notifications", "notification.name"}, "")) pattern_UserService_DeleteUserNotification_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", "notifications", "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_ListAllUserStats_0 = runtime.ForwardResponseMessage forward_UserService_GetUserStats_0 = runtime.ForwardResponseMessage forward_UserService_GetUserSetting_0 = runtime.ForwardResponseMessage forward_UserService_UpdateUserSetting_0 = runtime.ForwardResponseMessage forward_UserService_ListUserSettings_0 = runtime.ForwardResponseMessage forward_UserService_ListPersonalAccessTokens_0 = runtime.ForwardResponseMessage forward_UserService_CreatePersonalAccessToken_0 = runtime.ForwardResponseMessage forward_UserService_DeletePersonalAccessToken_0 = runtime.ForwardResponseMessage forward_UserService_ListUserWebhooks_0 = runtime.ForwardResponseMessage forward_UserService_CreateUserWebhook_0 = runtime.ForwardResponseMessage forward_UserService_UpdateUserWebhook_0 = runtime.ForwardResponseMessage forward_UserService_DeleteUserWebhook_0 = runtime.ForwardResponseMessage forward_UserService_ListUserNotifications_0 = runtime.ForwardResponseMessage forward_UserService_UpdateUserNotification_0 = runtime.ForwardResponseMessage forward_UserService_DeleteUserNotification_0 = runtime.ForwardResponseMessage ) ================================================ FILE: proto/gen/api/v1/user_service_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 // - protoc (unknown) // source: api/v1/user_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 ( 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_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_ListUserSettings_FullMethodName = "/memos.api.v1.UserService/ListUserSettings" UserService_ListPersonalAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListPersonalAccessTokens" UserService_CreatePersonalAccessToken_FullMethodName = "/memos.api.v1.UserService/CreatePersonalAccessToken" UserService_DeletePersonalAccessToken_FullMethodName = "/memos.api.v1.UserService/DeletePersonalAccessToken" UserService_ListUserWebhooks_FullMethodName = "/memos.api.v1.UserService/ListUserWebhooks" UserService_CreateUserWebhook_FullMethodName = "/memos.api.v1.UserService/CreateUserWebhook" UserService_UpdateUserWebhook_FullMethodName = "/memos.api.v1.UserService/UpdateUserWebhook" UserService_DeleteUserWebhook_FullMethodName = "/memos.api.v1.UserService/DeleteUserWebhook" UserService_ListUserNotifications_FullMethodName = "/memos.api.v1.UserService/ListUserNotifications" UserService_UpdateUserNotification_FullMethodName = "/memos.api.v1.UserService/UpdateUserNotification" UserService_DeleteUserNotification_FullMethodName = "/memos.api.v1.UserService/DeleteUserNotification" ) // 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 ID or username. // Supports both numeric IDs and username strings: // - users/{id} (e.g., users/101) // - users/{username} (e.g., users/steven) 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) // 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) // ListUserSettings returns a list of user settings. ListUserSettings(ctx context.Context, in *ListUserSettingsRequest, opts ...grpc.CallOption) (*ListUserSettingsResponse, error) // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error) // CreatePersonalAccessToken creates a new Personal Access Token for a user. // The token value is only returned once upon creation. CreatePersonalAccessToken(ctx context.Context, in *CreatePersonalAccessTokenRequest, opts ...grpc.CallOption) (*CreatePersonalAccessTokenResponse, error) // DeletePersonalAccessToken deletes a Personal Access Token. DeletePersonalAccessToken(ctx context.Context, in *DeletePersonalAccessTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // ListUserWebhooks returns a list of webhooks for a user. ListUserWebhooks(ctx context.Context, in *ListUserWebhooksRequest, opts ...grpc.CallOption) (*ListUserWebhooksResponse, error) // CreateUserWebhook creates a new webhook for a user. CreateUserWebhook(ctx context.Context, in *CreateUserWebhookRequest, opts ...grpc.CallOption) (*UserWebhook, error) // UpdateUserWebhook updates an existing webhook for a user. UpdateUserWebhook(ctx context.Context, in *UpdateUserWebhookRequest, opts ...grpc.CallOption) (*UserWebhook, error) // DeleteUserWebhook deletes a webhook for a user. DeleteUserWebhook(ctx context.Context, in *DeleteUserWebhookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // ListUserNotifications lists notifications for a user. ListUserNotifications(ctx context.Context, in *ListUserNotificationsRequest, opts ...grpc.CallOption) (*ListUserNotificationsResponse, error) // UpdateUserNotification updates a notification. UpdateUserNotification(ctx context.Context, in *UpdateUserNotificationRequest, opts ...grpc.CallOption) (*UserNotification, error) // DeleteUserNotification deletes a notification. DeleteUserNotification(ctx context.Context, in *DeleteUserNotificationRequest, 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) 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) ListUserSettings(ctx context.Context, in *ListUserSettingsRequest, opts ...grpc.CallOption) (*ListUserSettingsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListUserSettingsResponse) err := c.cc.Invoke(ctx, UserService_ListUserSettings_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListPersonalAccessTokensResponse) err := c.cc.Invoke(ctx, UserService_ListPersonalAccessTokens_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) CreatePersonalAccessToken(ctx context.Context, in *CreatePersonalAccessTokenRequest, opts ...grpc.CallOption) (*CreatePersonalAccessTokenResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CreatePersonalAccessTokenResponse) err := c.cc.Invoke(ctx, UserService_CreatePersonalAccessToken_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) DeletePersonalAccessToken(ctx context.Context, in *DeletePersonalAccessTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, UserService_DeletePersonalAccessToken_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) ListUserWebhooks(ctx context.Context, in *ListUserWebhooksRequest, opts ...grpc.CallOption) (*ListUserWebhooksResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListUserWebhooksResponse) err := c.cc.Invoke(ctx, UserService_ListUserWebhooks_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) CreateUserWebhook(ctx context.Context, in *CreateUserWebhookRequest, opts ...grpc.CallOption) (*UserWebhook, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UserWebhook) err := c.cc.Invoke(ctx, UserService_CreateUserWebhook_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) UpdateUserWebhook(ctx context.Context, in *UpdateUserWebhookRequest, opts ...grpc.CallOption) (*UserWebhook, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UserWebhook) err := c.cc.Invoke(ctx, UserService_UpdateUserWebhook_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) DeleteUserWebhook(ctx context.Context, in *DeleteUserWebhookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, UserService_DeleteUserWebhook_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) ListUserNotifications(ctx context.Context, in *ListUserNotificationsRequest, opts ...grpc.CallOption) (*ListUserNotificationsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListUserNotificationsResponse) err := c.cc.Invoke(ctx, UserService_ListUserNotifications_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) UpdateUserNotification(ctx context.Context, in *UpdateUserNotificationRequest, opts ...grpc.CallOption) (*UserNotification, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UserNotification) err := c.cc.Invoke(ctx, UserService_UpdateUserNotification_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) DeleteUserNotification(ctx context.Context, in *DeleteUserNotificationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, UserService_DeleteUserNotification_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 ID or username. // Supports both numeric IDs and username strings: // - users/{id} (e.g., users/101) // - users/{username} (e.g., users/steven) 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) // 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) // ListUserSettings returns a list of user settings. ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error) // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error) // CreatePersonalAccessToken creates a new Personal Access Token for a user. // The token value is only returned once upon creation. CreatePersonalAccessToken(context.Context, *CreatePersonalAccessTokenRequest) (*CreatePersonalAccessTokenResponse, error) // DeletePersonalAccessToken deletes a Personal Access Token. DeletePersonalAccessToken(context.Context, *DeletePersonalAccessTokenRequest) (*emptypb.Empty, error) // ListUserWebhooks returns a list of webhooks for a user. ListUserWebhooks(context.Context, *ListUserWebhooksRequest) (*ListUserWebhooksResponse, error) // CreateUserWebhook creates a new webhook for a user. CreateUserWebhook(context.Context, *CreateUserWebhookRequest) (*UserWebhook, error) // UpdateUserWebhook updates an existing webhook for a user. UpdateUserWebhook(context.Context, *UpdateUserWebhookRequest) (*UserWebhook, error) // DeleteUserWebhook deletes a webhook for a user. DeleteUserWebhook(context.Context, *DeleteUserWebhookRequest) (*emptypb.Empty, error) // ListUserNotifications lists notifications for a user. ListUserNotifications(context.Context, *ListUserNotificationsRequest) (*ListUserNotificationsResponse, error) // UpdateUserNotification updates a notification. UpdateUserNotification(context.Context, *UpdateUserNotificationRequest) (*UserNotification, error) // DeleteUserNotification deletes a notification. DeleteUserNotification(context.Context, *DeleteUserNotificationRequest) (*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.Error(codes.Unimplemented, "method ListUsers not implemented") } func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*User, error) { return nil, status.Error(codes.Unimplemented, "method GetUser not implemented") } func (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*User, error) { return nil, status.Error(codes.Unimplemented, "method CreateUser not implemented") } func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserRequest) (*User, error) { return nil, status.Error(codes.Unimplemented, "method UpdateUser not implemented") } func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method DeleteUser not implemented") } func (UnimplementedUserServiceServer) ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListAllUserStats not implemented") } func (UnimplementedUserServiceServer) GetUserStats(context.Context, *GetUserStatsRequest) (*UserStats, error) { return nil, status.Error(codes.Unimplemented, "method GetUserStats not implemented") } func (UnimplementedUserServiceServer) GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error) { return nil, status.Error(codes.Unimplemented, "method GetUserSetting not implemented") } func (UnimplementedUserServiceServer) UpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error) { return nil, status.Error(codes.Unimplemented, "method UpdateUserSetting not implemented") } func (UnimplementedUserServiceServer) ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListUserSettings not implemented") } func (UnimplementedUserServiceServer) ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListPersonalAccessTokens not implemented") } func (UnimplementedUserServiceServer) CreatePersonalAccessToken(context.Context, *CreatePersonalAccessTokenRequest) (*CreatePersonalAccessTokenResponse, error) { return nil, status.Error(codes.Unimplemented, "method CreatePersonalAccessToken not implemented") } func (UnimplementedUserServiceServer) DeletePersonalAccessToken(context.Context, *DeletePersonalAccessTokenRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method DeletePersonalAccessToken not implemented") } func (UnimplementedUserServiceServer) ListUserWebhooks(context.Context, *ListUserWebhooksRequest) (*ListUserWebhooksResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListUserWebhooks not implemented") } func (UnimplementedUserServiceServer) CreateUserWebhook(context.Context, *CreateUserWebhookRequest) (*UserWebhook, error) { return nil, status.Error(codes.Unimplemented, "method CreateUserWebhook not implemented") } func (UnimplementedUserServiceServer) UpdateUserWebhook(context.Context, *UpdateUserWebhookRequest) (*UserWebhook, error) { return nil, status.Error(codes.Unimplemented, "method UpdateUserWebhook not implemented") } func (UnimplementedUserServiceServer) DeleteUserWebhook(context.Context, *DeleteUserWebhookRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method DeleteUserWebhook not implemented") } func (UnimplementedUserServiceServer) ListUserNotifications(context.Context, *ListUserNotificationsRequest) (*ListUserNotificationsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListUserNotifications not implemented") } func (UnimplementedUserServiceServer) UpdateUserNotification(context.Context, *UpdateUserNotificationRequest) (*UserNotification, error) { return nil, status.Error(codes.Unimplemented, "method UpdateUserNotification not implemented") } func (UnimplementedUserServiceServer) DeleteUserNotification(context.Context, *DeleteUserNotificationRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method DeleteUserNotification 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 panics, 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_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_ListUserSettings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListUserSettingsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).ListUserSettings(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_ListUserSettings_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).ListUserSettings(ctx, req.(*ListUserSettingsRequest)) } return interceptor(ctx, in, info, handler) } func _UserService_ListPersonalAccessTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListPersonalAccessTokensRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).ListPersonalAccessTokens(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_ListPersonalAccessTokens_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).ListPersonalAccessTokens(ctx, req.(*ListPersonalAccessTokensRequest)) } return interceptor(ctx, in, info, handler) } func _UserService_CreatePersonalAccessToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreatePersonalAccessTokenRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).CreatePersonalAccessToken(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_CreatePersonalAccessToken_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).CreatePersonalAccessToken(ctx, req.(*CreatePersonalAccessTokenRequest)) } return interceptor(ctx, in, info, handler) } func _UserService_DeletePersonalAccessToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeletePersonalAccessTokenRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).DeletePersonalAccessToken(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_DeletePersonalAccessToken_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).DeletePersonalAccessToken(ctx, req.(*DeletePersonalAccessTokenRequest)) } return interceptor(ctx, in, info, handler) } func _UserService_ListUserWebhooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListUserWebhooksRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).ListUserWebhooks(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_ListUserWebhooks_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).ListUserWebhooks(ctx, req.(*ListUserWebhooksRequest)) } return interceptor(ctx, in, info, handler) } func _UserService_CreateUserWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateUserWebhookRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).CreateUserWebhook(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_CreateUserWebhook_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).CreateUserWebhook(ctx, req.(*CreateUserWebhookRequest)) } return interceptor(ctx, in, info, handler) } func _UserService_UpdateUserWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UpdateUserWebhookRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).UpdateUserWebhook(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_UpdateUserWebhook_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).UpdateUserWebhook(ctx, req.(*UpdateUserWebhookRequest)) } return interceptor(ctx, in, info, handler) } func _UserService_DeleteUserWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteUserWebhookRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).DeleteUserWebhook(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_DeleteUserWebhook_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).DeleteUserWebhook(ctx, req.(*DeleteUserWebhookRequest)) } return interceptor(ctx, in, info, handler) } func _UserService_ListUserNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListUserNotificationsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).ListUserNotifications(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_ListUserNotifications_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).ListUserNotifications(ctx, req.(*ListUserNotificationsRequest)) } return interceptor(ctx, in, info, handler) } func _UserService_UpdateUserNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UpdateUserNotificationRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).UpdateUserNotification(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_UpdateUserNotification_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).UpdateUserNotification(ctx, req.(*UpdateUserNotificationRequest)) } return interceptor(ctx, in, info, handler) } func _UserService_DeleteUserNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteUserNotificationRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(UserServiceServer).DeleteUserNotification(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: UserService_DeleteUserNotification_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(UserServiceServer).DeleteUserNotification(ctx, req.(*DeleteUserNotificationRequest)) } 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: "ListAllUserStats", Handler: _UserService_ListAllUserStats_Handler, }, { MethodName: "GetUserStats", Handler: _UserService_GetUserStats_Handler, }, { MethodName: "GetUserSetting", Handler: _UserService_GetUserSetting_Handler, }, { MethodName: "UpdateUserSetting", Handler: _UserService_UpdateUserSetting_Handler, }, { MethodName: "ListUserSettings", Handler: _UserService_ListUserSettings_Handler, }, { MethodName: "ListPersonalAccessTokens", Handler: _UserService_ListPersonalAccessTokens_Handler, }, { MethodName: "CreatePersonalAccessToken", Handler: _UserService_CreatePersonalAccessToken_Handler, }, { MethodName: "DeletePersonalAccessToken", Handler: _UserService_DeletePersonalAccessToken_Handler, }, { MethodName: "ListUserWebhooks", Handler: _UserService_ListUserWebhooks_Handler, }, { MethodName: "CreateUserWebhook", Handler: _UserService_CreateUserWebhook_Handler, }, { MethodName: "UpdateUserWebhook", Handler: _UserService_UpdateUserWebhook_Handler, }, { MethodName: "DeleteUserWebhook", Handler: _UserService_DeleteUserWebhook_Handler, }, { MethodName: "ListUserNotifications", Handler: _UserService_ListUserNotifications_Handler, }, { MethodName: "UpdateUserNotification", Handler: _UserService_UpdateUserNotification_Handler, }, { MethodName: "DeleteUserNotification", Handler: _UserService_DeleteUserNotification_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "api/v1/user_service.proto", } ================================================ FILE: proto/gen/openapi.yaml ================================================ # Generated with protoc-gen-openapi # https://github.com/google/gnostic/tree/master/cmd/protoc-gen-openapi openapi: 3.0.3 info: title: "" version: 0.0.1 paths: /api/v1/attachments: get: tags: - AttachmentService description: ListAttachments lists all attachments. operationId: AttachmentService_ListAttachments parameters: - name: pageSize in: query description: |- 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. schema: type: integer format: int32 - name: pageToken in: query description: |- Optional. A page token, received from a previous `ListAttachments` call. Provide this to retrieve the subsequent page. schema: type: string - name: filter in: query description: |- Optional. Filter to apply to the list results. Example: "mime_type==\"image/png\"" or "filename.contains(\"test\")" Supported operators: =, !=, <, <=, >, >=, : (contains), in Supported fields: filename, mime_type, create_time, memo schema: type: string - name: orderBy in: query description: |- Optional. The order to sort results by. Example: "create_time desc" or "filename asc" schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListAttachmentsResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' post: tags: - AttachmentService description: CreateAttachment creates a new attachment. operationId: AttachmentService_CreateAttachment parameters: - name: attachmentId in: query description: |- Optional. The attachment ID to use for this attachment. If empty, a unique ID will be generated. schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/Attachment' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Attachment' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/attachments/{attachment}: get: tags: - AttachmentService description: GetAttachment returns an attachment by name. operationId: AttachmentService_GetAttachment parameters: - name: attachment in: path description: The attachment id. required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Attachment' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' delete: tags: - AttachmentService description: DeleteAttachment deletes an attachment by name. operationId: AttachmentService_DeleteAttachment parameters: - name: attachment in: path description: The attachment id. required: true schema: type: string responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - AttachmentService description: UpdateAttachment updates an attachment. operationId: AttachmentService_UpdateAttachment parameters: - name: attachment in: path description: The attachment id. required: true schema: type: string - name: updateMask in: query description: Required. The list of fields to update. schema: type: string format: field-mask requestBody: content: application/json: schema: $ref: '#/components/schemas/Attachment' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Attachment' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/auth/me: get: tags: - AuthService description: |- GetCurrentUser returns the authenticated user's information. Validates the access token and returns user details. Similar to OIDC's /userinfo endpoint. operationId: AuthService_GetCurrentUser responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/GetCurrentUserResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/auth/refresh: post: tags: - AuthService description: |- RefreshToken exchanges a valid refresh token for a new access token. The refresh token is read from the HttpOnly cookie. Returns a new short-lived access token. operationId: AuthService_RefreshToken requestBody: content: application/json: schema: $ref: '#/components/schemas/RefreshTokenRequest' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/RefreshTokenResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/auth/signin: post: tags: - AuthService description: |- SignIn authenticates a user with credentials and returns tokens. On success, returns an access token and sets a refresh token cookie. Supports password-based and SSO authentication methods. operationId: AuthService_SignIn requestBody: content: application/json: schema: $ref: '#/components/schemas/SignInRequest' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/SignInResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/auth/signout: post: tags: - AuthService description: |- SignOut terminates the user's authentication. Revokes the refresh token and clears the authentication cookie. operationId: AuthService_SignOut responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/identity-providers: get: tags: - IdentityProviderService description: ListIdentityProviders lists identity providers. operationId: IdentityProviderService_ListIdentityProviders responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListIdentityProvidersResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' post: tags: - IdentityProviderService description: CreateIdentityProvider creates an identity provider. operationId: IdentityProviderService_CreateIdentityProvider parameters: - name: identityProviderId in: query description: |- 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. schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/IdentityProvider' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/IdentityProvider' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/identity-providers/{identity-provider}: get: tags: - IdentityProviderService description: GetIdentityProvider gets an identity provider. operationId: IdentityProviderService_GetIdentityProvider parameters: - name: identity-provider in: path description: The identity-provider id. required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/IdentityProvider' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' delete: tags: - IdentityProviderService description: DeleteIdentityProvider deletes an identity provider. operationId: IdentityProviderService_DeleteIdentityProvider parameters: - name: identity-provider in: path description: The identity-provider id. required: true schema: type: string responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - IdentityProviderService description: UpdateIdentityProvider updates an identity provider. operationId: IdentityProviderService_UpdateIdentityProvider parameters: - name: identity-provider in: path description: The identity-provider id. required: true schema: type: string - name: updateMask in: query description: |- Required. The update mask applies to the resource. Only the top level fields of IdentityProvider are supported. schema: type: string format: field-mask requestBody: content: application/json: schema: $ref: '#/components/schemas/IdentityProvider' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/IdentityProvider' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/instance/profile: get: tags: - InstanceService description: Gets the instance profile. operationId: InstanceService_GetInstanceProfile responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/InstanceProfile' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/instance/{instance}/*: get: tags: - InstanceService description: Gets an instance setting. operationId: InstanceService_GetInstanceSetting parameters: - name: instance in: path description: The instance id. required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/InstanceSetting' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - InstanceService description: Updates an instance setting. operationId: InstanceService_UpdateInstanceSetting parameters: - name: instance in: path description: The instance id. required: true schema: type: string - name: updateMask in: query description: The list of fields to update. schema: type: string format: field-mask requestBody: content: application/json: schema: $ref: '#/components/schemas/InstanceSetting' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/InstanceSetting' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/memos: get: tags: - MemoService description: ListMemos lists memos with pagination and filter. operationId: MemoService_ListMemos parameters: - name: pageSize in: query 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. schema: type: integer format: int32 - name: pageToken in: query description: |- Optional. A page token, received from a previous `ListMemos` call. Provide this to retrieve the subsequent page. schema: type: string - name: state in: query description: |- Optional. The state of the memos to list. Default to `NORMAL`. Set to `ARCHIVED` to list archived memos. schema: enum: - STATE_UNSPECIFIED - NORMAL - ARCHIVED type: string format: enum - name: orderBy in: query description: |- Optional. The order to sort results by. Default to "display_time desc". Supports comma-separated list of fields following AIP-132. Example: "pinned desc, display_time desc" or "create_time asc" Supported fields: pinned, display_time, create_time, update_time, name schema: type: string - name: filter in: query description: |- Optional. Filter to apply to the list results. Filter is a CEL expression to filter memos. Refer to `Shortcut.filter`. schema: type: string - name: showDeleted in: query description: Optional. If true, show deleted memos in the response. schema: type: boolean responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListMemosResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' post: tags: - MemoService description: CreateMemo creates a memo. operationId: MemoService_CreateMemo parameters: - name: memoId in: query description: |- Optional. The memo ID to use for this memo. If empty, a unique ID will be generated. schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/Memo' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Memo' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/memos/{memo}: get: tags: - MemoService description: GetMemo gets a memo. operationId: MemoService_GetMemo parameters: - name: memo in: path description: The memo id. required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Memo' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' delete: tags: - MemoService description: DeleteMemo deletes a memo. operationId: MemoService_DeleteMemo parameters: - name: memo in: path description: The memo id. required: true schema: type: string - name: force in: query description: Optional. If set to true, the memo will be deleted even if it has associated data. schema: type: boolean responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - MemoService description: UpdateMemo updates a memo. operationId: MemoService_UpdateMemo parameters: - name: memo in: path description: The memo id. required: true schema: type: string - name: updateMask in: query description: Required. The list of fields to update. schema: type: string format: field-mask requestBody: content: application/json: schema: $ref: '#/components/schemas/Memo' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Memo' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/memos/{memo}/attachments: get: tags: - MemoService description: ListMemoAttachments lists attachments for a memo. operationId: MemoService_ListMemoAttachments parameters: - name: memo in: path description: The memo id. required: true schema: type: string - name: pageSize in: query description: Optional. The maximum number of attachments to return. schema: type: integer format: int32 - name: pageToken in: query description: Optional. A page token for pagination. schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListMemoAttachmentsResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - MemoService description: SetMemoAttachments sets attachments for a memo. operationId: MemoService_SetMemoAttachments parameters: - name: memo in: path description: The memo id. required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/SetMemoAttachmentsRequest' required: true responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/memos/{memo}/comments: get: tags: - MemoService description: ListMemoComments lists comments for a memo. operationId: MemoService_ListMemoComments parameters: - name: memo in: path description: The memo id. required: true schema: type: string - name: pageSize in: query description: Optional. The maximum number of comments to return. schema: type: integer format: int32 - name: pageToken in: query description: Optional. A page token for pagination. schema: type: string - name: orderBy in: query description: Optional. The order to sort results by. schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListMemoCommentsResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' post: tags: - MemoService description: CreateMemoComment creates a comment for a memo. operationId: MemoService_CreateMemoComment parameters: - name: memo in: path description: The memo id. required: true schema: type: string - name: commentId in: query description: Optional. The comment ID to use. schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/Memo' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Memo' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/memos/{memo}/reactions: get: tags: - MemoService description: ListMemoReactions lists reactions for a memo. operationId: MemoService_ListMemoReactions parameters: - name: memo in: path description: The memo id. required: true schema: type: string - name: pageSize in: query description: Optional. The maximum number of reactions to return. schema: type: integer format: int32 - name: pageToken in: query description: Optional. A page token for pagination. schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListMemoReactionsResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' post: tags: - MemoService description: UpsertMemoReaction upserts a reaction for a memo. operationId: MemoService_UpsertMemoReaction parameters: - name: memo in: path description: The memo id. required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/UpsertMemoReactionRequest' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Reaction' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/memos/{memo}/reactions/{reaction}: delete: tags: - MemoService description: DeleteMemoReaction deletes a reaction for a memo. operationId: MemoService_DeleteMemoReaction parameters: - name: memo in: path description: The memo id. required: true schema: type: string - name: reaction in: path description: The reaction id. required: true schema: type: string responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/memos/{memo}/relations: get: tags: - MemoService description: ListMemoRelations lists relations for a memo. operationId: MemoService_ListMemoRelations parameters: - name: memo in: path description: The memo id. required: true schema: type: string - name: pageSize in: query description: Optional. The maximum number of relations to return. schema: type: integer format: int32 - name: pageToken in: query description: Optional. A page token for pagination. schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListMemoRelationsResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - MemoService description: SetMemoRelations sets relations for a memo. operationId: MemoService_SetMemoRelations parameters: - name: memo in: path description: The memo id. required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/SetMemoRelationsRequest' required: true responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/memos/{memo}/shares: get: tags: - MemoService description: ListMemoShares lists all share links for a memo. Requires authentication as the memo creator. operationId: MemoService_ListMemoShares parameters: - name: memo in: path description: The memo id. required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListMemoSharesResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' post: tags: - MemoService description: CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator. operationId: MemoService_CreateMemoShare parameters: - name: memo in: path description: The memo id. required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/MemoShare' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/MemoShare' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/memos/{memo}/shares/{share}: delete: tags: - MemoService description: DeleteMemoShare revokes a share link. Requires authentication as the memo creator. operationId: MemoService_DeleteMemoShare parameters: - name: memo in: path description: The memo id. required: true schema: type: string - name: share in: path description: The share id. required: true schema: type: string responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/shares/{shareId}: get: tags: - MemoService description: |- GetMemoByShare resolves a share token to its memo. No authentication required. Returns NOT_FOUND if the token is invalid or expired. operationId: MemoService_GetMemoByShare parameters: - name: shareId in: path description: Required. The share token extracted from the share URL (/s/{share_id}). required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Memo' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users: get: tags: - UserService description: ListUsers returns a list of users. operationId: UserService_ListUsers parameters: - name: pageSize in: query description: |- 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. schema: type: integer format: int32 - name: pageToken in: query description: |- Optional. A page token, received from a previous `ListUsers` call. Provide this to retrieve the subsequent page. schema: type: string - name: filter in: query description: |- Optional. Filter to apply to the list results. Example: "username == 'steven'" Supported operators: == Supported fields: username schema: type: string - name: showDeleted in: query description: Optional. If true, show deleted users in the response. schema: type: boolean responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListUsersResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' post: tags: - UserService description: CreateUser creates a new user. operationId: UserService_CreateUser parameters: - name: userId in: query description: |- Optional. The user ID to use for this user. If empty, a unique ID will be generated. Must match the pattern [a-z0-9-]+ schema: type: string - name: validateOnly in: query description: Optional. If set, validate the request but don't actually create the user. schema: type: boolean - name: requestId in: query description: |- Optional. An idempotency token that can be used to ensure that multiple requests to create a user have the same result. schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/User' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/User' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}: get: tags: - UserService description: |- GetUser gets a user by ID or username. Supports both numeric IDs and username strings: - users/{id} (e.g., users/101) - users/{username} (e.g., users/steven) operationId: UserService_GetUser parameters: - name: user in: path description: The user id. required: true schema: type: string - name: readMask in: query description: |- Optional. The fields to return in the response. If not specified, all fields are returned. schema: type: string format: field-mask responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/User' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' delete: tags: - UserService description: DeleteUser deletes a user. operationId: UserService_DeleteUser parameters: - name: user in: path description: The user id. required: true schema: type: string - name: force in: query description: Optional. If set to true, the user will be deleted even if they have associated data. schema: type: boolean responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - UserService description: UpdateUser updates a user. operationId: UserService_UpdateUser parameters: - name: user in: path description: The user id. required: true schema: type: string - name: updateMask in: query description: Required. The list of fields to update. schema: type: string format: field-mask - name: allowMissing in: query description: Optional. If set to true, allows updating sensitive fields. schema: type: boolean requestBody: content: application/json: schema: $ref: '#/components/schemas/User' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/User' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}/notifications: get: tags: - UserService description: ListUserNotifications lists notifications for a user. operationId: UserService_ListUserNotifications parameters: - name: user in: path description: The user id. required: true schema: type: string - name: pageSize in: query schema: type: integer format: int32 - name: pageToken in: query schema: type: string - name: filter in: query schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListUserNotificationsResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}/notifications/{notification}: delete: tags: - UserService description: DeleteUserNotification deletes a notification. operationId: UserService_DeleteUserNotification parameters: - name: user in: path description: The user id. required: true schema: type: string - name: notification in: path description: The notification id. required: true schema: type: string responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - UserService description: UpdateUserNotification updates a notification. operationId: UserService_UpdateUserNotification parameters: - name: user in: path description: The user id. required: true schema: type: string - name: notification in: path description: The notification id. required: true schema: type: string - name: updateMask in: query schema: type: string format: field-mask requestBody: content: application/json: schema: $ref: '#/components/schemas/UserNotification' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/UserNotification' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}/personalAccessTokens: get: tags: - UserService description: |- ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. operationId: UserService_ListPersonalAccessTokens parameters: - name: user in: path description: The user id. required: true schema: type: string - name: pageSize in: query description: Optional. The maximum number of tokens to return. schema: type: integer format: int32 - name: pageToken in: query description: Optional. A page token for pagination. schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListPersonalAccessTokensResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' post: tags: - UserService description: |- CreatePersonalAccessToken creates a new Personal Access Token for a user. The token value is only returned once upon creation. operationId: UserService_CreatePersonalAccessToken parameters: - name: user in: path description: The user id. required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/CreatePersonalAccessTokenRequest' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/CreatePersonalAccessTokenResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}/personalAccessTokens/{personalAccessToken}: delete: tags: - UserService description: DeletePersonalAccessToken deletes a Personal Access Token. operationId: UserService_DeletePersonalAccessToken parameters: - name: user in: path description: The user id. required: true schema: type: string - name: personalAccessToken in: path description: The personalAccessToken id. required: true schema: type: string responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}/settings: get: tags: - UserService description: ListUserSettings returns a list of user settings. operationId: UserService_ListUserSettings parameters: - name: user in: path description: The user id. required: true schema: type: string - name: pageSize in: query description: |- Optional. The maximum number of settings to return. The service may return fewer than this value. If unspecified, at most 50 settings will be returned. The maximum value is 1000; values above 1000 will be coerced to 1000. schema: type: integer format: int32 - name: pageToken in: query description: |- Optional. A page token, received from a previous `ListUserSettings` call. Provide this to retrieve the subsequent page. schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListUserSettingsResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}/settings/{setting}: get: tags: - UserService description: GetUserSetting returns the user setting. operationId: UserService_GetUserSetting parameters: - name: user in: path description: The user id. required: true schema: type: string - name: setting in: path description: The setting id. required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/UserSetting' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - UserService description: UpdateUserSetting updates the user setting. operationId: UserService_UpdateUserSetting parameters: - name: user in: path description: The user id. required: true schema: type: string - name: setting in: path description: The setting id. required: true schema: type: string - name: updateMask in: query description: Required. The list of fields to update. schema: type: string format: field-mask requestBody: content: application/json: schema: $ref: '#/components/schemas/UserSetting' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/UserSetting' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}/shortcuts: get: tags: - ShortcutService description: ListShortcuts returns a list of shortcuts for a user. operationId: ShortcutService_ListShortcuts parameters: - name: user in: path description: The user id. required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListShortcutsResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' post: tags: - ShortcutService description: CreateShortcut creates a new shortcut for a user. operationId: ShortcutService_CreateShortcut parameters: - name: user in: path description: The user id. required: true schema: type: string - name: validateOnly in: query description: Optional. If set, validate the request, but do not actually create the shortcut. schema: type: boolean requestBody: content: application/json: schema: $ref: '#/components/schemas/Shortcut' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Shortcut' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}/shortcuts/{shortcut}: get: tags: - ShortcutService description: GetShortcut gets a shortcut by name. operationId: ShortcutService_GetShortcut parameters: - name: user in: path description: The user id. required: true schema: type: string - name: shortcut in: path description: The shortcut id. required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Shortcut' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' delete: tags: - ShortcutService description: DeleteShortcut deletes a shortcut for a user. operationId: ShortcutService_DeleteShortcut parameters: - name: user in: path description: The user id. required: true schema: type: string - name: shortcut in: path description: The shortcut id. required: true schema: type: string responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - ShortcutService description: UpdateShortcut updates a shortcut for a user. operationId: ShortcutService_UpdateShortcut parameters: - name: user in: path description: The user id. required: true schema: type: string - name: shortcut in: path description: The shortcut id. required: true schema: type: string - name: updateMask in: query description: Optional. The list of fields to update. schema: type: string format: field-mask requestBody: content: application/json: schema: $ref: '#/components/schemas/Shortcut' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Shortcut' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}/webhooks: get: tags: - UserService description: ListUserWebhooks returns a list of webhooks for a user. operationId: UserService_ListUserWebhooks parameters: - name: user in: path description: The user id. required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListUserWebhooksResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' post: tags: - UserService description: CreateUserWebhook creates a new webhook for a user. operationId: UserService_CreateUserWebhook parameters: - name: user in: path description: The user id. required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/UserWebhook' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/UserWebhook' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}/webhooks/{webhook}: delete: tags: - UserService description: DeleteUserWebhook deletes a webhook for a user. operationId: UserService_DeleteUserWebhook parameters: - name: user in: path description: The user id. required: true schema: type: string - name: webhook in: path description: The webhook id. required: true schema: type: string responses: "200": description: OK content: {} default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' patch: tags: - UserService description: UpdateUserWebhook updates an existing webhook for a user. operationId: UserService_UpdateUserWebhook parameters: - name: user in: path description: The user id. required: true schema: type: string - name: webhook in: path description: The webhook id. required: true schema: type: string - name: updateMask in: query description: The list of fields to update. schema: type: string format: field-mask requestBody: content: application/json: schema: $ref: '#/components/schemas/UserWebhook' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/UserWebhook' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users/{user}:getStats: get: tags: - UserService description: GetUserStats returns statistics for a specific user. operationId: UserService_GetUserStats parameters: - name: user in: path description: The user id. required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/UserStats' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' /api/v1/users:stats: get: tags: - UserService description: ListAllUserStats returns statistics for all users. operationId: UserService_ListAllUserStats responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListAllUserStatsResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' components: schemas: Attachment: required: - filename - type type: object properties: name: type: string description: |- The name of the attachment. Format: attachments/{attachment} createTime: readOnly: true type: string description: Output only. The creation timestamp. format: date-time filename: type: string description: The filename of the attachment. content: writeOnly: true type: string description: Input only. The content of the attachment. format: bytes externalLink: type: string description: Optional. The external link of the attachment. type: type: string description: The MIME type of the attachment. size: readOnly: true type: string description: Output only. The size of the attachment in bytes. memo: type: string description: |- Optional. The related memo. Refer to `Memo.name`. Format: memos/{memo} Color: type: object properties: red: type: number description: The amount of red in the color as a value in the interval [0, 1]. format: float green: type: number description: The amount of green in the color as a value in the interval [0, 1]. format: float blue: type: number description: The amount of blue in the color as a value in the interval [0, 1]. format: float alpha: type: number description: |- The fraction of this color that should be applied to the pixel. That is, the final pixel color is defined by the equation: `pixel color = alpha * (this color) + (1.0 - alpha) * (background color)` This means that a value of 1.0 corresponds to a solid color, whereas a value of 0.0 corresponds to a completely transparent color. This uses a wrapper message rather than a simple float scalar so that it is possible to distinguish between a default value and the value being unset. If omitted, this color object is rendered as a solid color (as if the alpha value had been explicitly given a value of 1.0). format: float description: |- Represents a color in the RGBA color space. This representation is designed for simplicity of conversion to/from color representations in various languages over compactness. For example, the fields of this representation can be trivially provided to the constructor of `java.awt.Color` in Java; it can also be trivially provided to UIColor's `+colorWithRed:green:blue:alpha` method in iOS; and, with just a little work, it can be easily formatted into a CSS `rgba()` string in JavaScript. This reference page doesn't carry information about the absolute color space that should be used to interpret the RGB value (e.g. sRGB, Adobe RGB, DCI-P3, BT.2020, etc.). By default, applications should assume the sRGB color space. When color equality needs to be decided, implementations, unless documented otherwise, treat two colors as equal if all their red, green, blue, and alpha values each differ by at most 1e-5. Example (Java): import com.google.type.Color; // ... public static java.awt.Color fromProto(Color protocolor) { float alpha = protocolor.hasAlpha() ? protocolor.getAlpha().getValue() : 1.0; return new java.awt.Color( protocolor.getRed(), protocolor.getGreen(), protocolor.getBlue(), alpha); } public static Color toProto(java.awt.Color color) { float red = (float) color.getRed(); float green = (float) color.getGreen(); float blue = (float) color.getBlue(); float denominator = 255.0; Color.Builder resultBuilder = Color .newBuilder() .setRed(red / denominator) .setGreen(green / denominator) .setBlue(blue / denominator); int alpha = color.getAlpha(); if (alpha != 255) { result.setAlpha( FloatValue .newBuilder() .setValue(((float) alpha) / denominator) .build()); } return resultBuilder.build(); } // ... Example (iOS / Obj-C): // ... static UIColor* fromProto(Color* protocolor) { float red = [protocolor red]; float green = [protocolor green]; float blue = [protocolor blue]; FloatValue* alpha_wrapper = [protocolor alpha]; float alpha = 1.0; if (alpha_wrapper != nil) { alpha = [alpha_wrapper value]; } return [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; } static Color* toProto(UIColor* color) { CGFloat red, green, blue, alpha; if (![color getRed:&red green:&green blue:&blue alpha:&alpha]) { return nil; } Color* result = [[Color alloc] init]; [result setRed:red]; [result setGreen:green]; [result setBlue:blue]; if (alpha <= 0.9999) { [result setAlpha:floatWrapperWithValue(alpha)]; } [result autorelease]; return result; } // ... Example (JavaScript): // ... var protoToCssColor = function(rgb_color) { var redFrac = rgb_color.red || 0.0; var greenFrac = rgb_color.green || 0.0; var blueFrac = rgb_color.blue || 0.0; var red = Math.floor(redFrac * 255); var green = Math.floor(greenFrac * 255); var blue = Math.floor(blueFrac * 255); if (!('alpha' in rgb_color)) { return rgbToCssColor(red, green, blue); } var alphaFrac = rgb_color.alpha.value || 0.0; var rgbParams = [red, green, blue].join(','); return ['rgba(', rgbParams, ',', alphaFrac, ')'].join(''); }; var rgbToCssColor = function(red, green, blue) { var rgbNumber = new Number((red << 16) | (green << 8) | blue); var hexString = rgbNumber.toString(16); var missingZeros = 6 - hexString.length; var resultBuilder = ['#']; for (var i = 0; i < missingZeros; i++) { resultBuilder.push('0'); } resultBuilder.push(hexString); return resultBuilder.join(''); }; // ... CreatePersonalAccessTokenRequest: required: - parent type: object properties: parent: type: string description: |- Required. The parent resource where this token will be created. Format: users/{user} description: type: string description: Optional. Description of the personal access token. expiresInDays: type: integer description: Optional. Expiration duration in days (0 = never expires). format: int32 CreatePersonalAccessTokenResponse: type: object properties: personalAccessToken: allOf: - $ref: '#/components/schemas/PersonalAccessToken' description: The personal access token metadata. token: type: string description: |- The actual token value - only returned on creation. This is the only time the token value will be visible. FieldMapping: type: object properties: identifier: type: string displayName: type: string email: type: string avatarUrl: type: string GeneralSetting_CustomProfile: type: object properties: title: type: string description: type: string logoUrl: type: string description: Custom profile configuration for instance branding. GetCurrentUserResponse: type: object properties: user: allOf: - $ref: '#/components/schemas/User' description: The authenticated user's information. GoogleProtobufAny: type: object properties: '@type': type: string description: The type of the serialized message. additionalProperties: true description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. IdentityProvider: required: - type - title - config type: object properties: name: type: string description: |- The resource name of the identity provider. Format: identity-providers/{idp} type: enum: - TYPE_UNSPECIFIED - OAUTH2 type: string description: Required. The type of the identity provider. format: enum title: type: string description: Required. The display title of the identity provider. identifierFilter: type: string description: Optional. Filter applied to user identifiers. config: allOf: - $ref: '#/components/schemas/IdentityProviderConfig' description: Required. Configuration for the identity provider. IdentityProviderConfig: type: object properties: oauth2Config: $ref: '#/components/schemas/OAuth2Config' InstanceProfile: type: object properties: version: type: string description: Version is the current version of instance. demo: type: boolean description: Demo indicates if the instance is in demo mode. instanceUrl: type: string description: Instance URL is the URL of the instance. admin: allOf: - $ref: '#/components/schemas/User' description: |- The first administrator who set up this instance. When null, instance requires initial setup (creating the first admin account). description: Instance profile message containing basic instance information. InstanceSetting: type: object properties: name: type: string description: |- The name of the instance setting. Format: instance/settings/{setting} generalSetting: $ref: '#/components/schemas/InstanceSetting_GeneralSetting' storageSetting: $ref: '#/components/schemas/InstanceSetting_StorageSetting' memoRelatedSetting: $ref: '#/components/schemas/InstanceSetting_MemoRelatedSetting' tagsSetting: $ref: '#/components/schemas/InstanceSetting_TagsSetting' notificationSetting: $ref: '#/components/schemas/InstanceSetting_NotificationSetting' description: An instance setting resource. InstanceSetting_GeneralSetting: type: object properties: 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: allOf: - $ref: '#/components/schemas/GeneralSetting_CustomProfile' description: custom_profile is the custom profile. weekStartDayOffset: type: integer description: |- 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. format: int32 disallowChangeUsername: type: boolean description: disallow_change_username disallows changing username. disallowChangeNickname: type: boolean description: disallow_change_nickname disallows changing nickname. description: General instance settings configuration. InstanceSetting_MemoRelatedSetting: type: object properties: displayWithUpdateTime: type: boolean description: display_with_update_time orders and displays memo with update time. contentLengthLimit: type: integer description: content_length_limit is the limit of content length. Unit is byte. format: int32 enableDoubleClickEdit: type: boolean description: enable_double_click_edit enables editing on double click. reactions: type: array items: type: string description: reactions is the list of reactions. description: Memo-related instance settings and policies. InstanceSetting_NotificationSetting: type: object properties: email: $ref: '#/components/schemas/NotificationSetting_EmailSetting' description: Notification transport configuration. InstanceSetting_StorageSetting: type: object properties: storageType: enum: - STORAGE_TYPE_UNSPECIFIED - DATABASE - LOCAL - S3 type: string description: storage_type is the storage type. format: enum filepathTemplate: type: string description: |- The template of file path. e.g. assets/{timestamp}_{filename} uploadSizeLimitMb: type: string description: The max upload size in megabytes. s3Config: allOf: - $ref: '#/components/schemas/StorageSetting_S3Config' description: The S3 config. description: Storage configuration settings for instance attachments. InstanceSetting_TagMetadata: type: object properties: backgroundColor: allOf: - $ref: '#/components/schemas/Color' description: Background color for the tag label. description: Metadata for a tag. InstanceSetting_TagsSetting: type: object properties: tags: type: object additionalProperties: $ref: '#/components/schemas/InstanceSetting_TagMetadata' description: Tag metadata configuration. ListAllUserStatsResponse: type: object properties: stats: type: array items: $ref: '#/components/schemas/UserStats' description: The list of user statistics. ListAttachmentsResponse: type: object properties: attachments: type: array items: $ref: '#/components/schemas/Attachment' description: The list of attachments. 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 description: The total count of attachments (may be approximate). format: int32 ListIdentityProvidersResponse: type: object properties: identityProviders: type: array items: $ref: '#/components/schemas/IdentityProvider' description: The list of identity providers. ListMemoAttachmentsResponse: type: object properties: attachments: type: array items: $ref: '#/components/schemas/Attachment' description: The list of attachments. nextPageToken: type: string description: A token for the next page of results. ListMemoCommentsResponse: type: object properties: memos: type: array items: $ref: '#/components/schemas/Memo' description: The list of comment memos. nextPageToken: type: string description: A token for the next page of results. totalSize: type: integer description: The total count of comments. format: int32 ListMemoReactionsResponse: type: object properties: reactions: type: array items: $ref: '#/components/schemas/Reaction' description: The list of reactions. nextPageToken: type: string description: A token for the next page of results. totalSize: type: integer description: The total count of reactions. format: int32 ListMemoRelationsResponse: type: object properties: relations: type: array items: $ref: '#/components/schemas/MemoRelation' description: The list of relations. nextPageToken: type: string description: A token for the next page of results. ListMemoSharesResponse: type: object properties: memoShares: type: array items: $ref: '#/components/schemas/MemoShare' description: The list of share links. ListMemosResponse: type: object properties: memos: type: array items: $ref: '#/components/schemas/Memo' 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. ListPersonalAccessTokensResponse: type: object properties: personalAccessTokens: type: array items: $ref: '#/components/schemas/PersonalAccessToken' description: The list of personal access tokens. nextPageToken: type: string description: A token for the next page of results. totalSize: type: integer description: The total count of personal access tokens. format: int32 ListShortcutsResponse: type: object properties: shortcuts: type: array items: $ref: '#/components/schemas/Shortcut' description: The list of shortcuts. ListUserNotificationsResponse: type: object properties: notifications: type: array items: $ref: '#/components/schemas/UserNotification' nextPageToken: type: string ListUserSettingsResponse: type: object properties: settings: type: array items: $ref: '#/components/schemas/UserSetting' description: The list of user settings. 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 description: The total count of settings (may be approximate). format: int32 description: Response message for ListUserSettings method. ListUserWebhooksResponse: type: object properties: webhooks: type: array items: $ref: '#/components/schemas/UserWebhook' description: The list of webhooks. ListUsersResponse: type: object properties: users: type: array items: $ref: '#/components/schemas/User' description: The list of users. 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 description: The total count of users (may be approximate). format: int32 Location: type: object properties: placeholder: type: string description: A placeholder text for the location. latitude: type: number description: The latitude of the location. format: double longitude: type: number description: The longitude of the location. format: double Memo: required: - state - content - visibility 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: enum: - STATE_UNSPECIFIED - NORMAL - ARCHIVED type: string description: The state of the memo. format: enum creator: readOnly: true type: string description: |- The name of the creator. Format: users/{user} createTime: type: string description: |- The creation timestamp. If not set on creation, the server will set it to the current time. format: date-time updateTime: type: string description: |- The last update timestamp. If not set on creation, the server will set it to the current time. format: date-time displayTime: type: string description: The display timestamp of the memo. format: date-time content: type: string description: Required. The content of the memo in Markdown format. visibility: enum: - VISIBILITY_UNSPECIFIED - PRIVATE - PROTECTED - PUBLIC type: string description: The visibility of the memo. format: enum tags: readOnly: true type: array items: type: string description: Output only. The tags extracted from the content. pinned: type: boolean description: Whether the memo is pinned. attachments: type: array items: $ref: '#/components/schemas/Attachment' description: Optional. The attachments of the memo. relations: type: array items: $ref: '#/components/schemas/MemoRelation' description: Optional. The relations of the memo. reactions: readOnly: true type: array items: $ref: '#/components/schemas/Reaction' description: Output only. The reactions to the memo. property: readOnly: true allOf: - $ref: '#/components/schemas/Memo_Property' description: Output only. The computed properties of the memo. parent: readOnly: true type: string description: |- Output only. The name of the parent memo. Format: memos/{memo} snippet: readOnly: true type: string description: Output only. The snippet of the memo content. Plain text only. location: allOf: - $ref: '#/components/schemas/Location' description: Optional. The location of the memo. MemoRelation: required: - memo - relatedMemo - type type: object properties: memo: allOf: - $ref: '#/components/schemas/MemoRelation_Memo' description: The memo in the relation. relatedMemo: allOf: - $ref: '#/components/schemas/MemoRelation_Memo' description: The related memo. type: enum: - TYPE_UNSPECIFIED - REFERENCE - COMMENT type: string format: enum MemoRelation_Memo: required: - name type: object properties: name: type: string description: |- The resource name of the memo. Format: memos/{memo} snippet: readOnly: true type: string description: Output only. The snippet of the memo content. Plain text only. description: Memo reference in relations. MemoShare: type: object properties: name: type: string description: |- The resource name of the share. Format: memos/{memo}/shares/{share} The {share} segment is the opaque token used in the share URL. createTime: readOnly: true type: string description: Output only. When this share link was created. format: date-time expireTime: type: string description: |- Optional. When set, the share link stops working after this time. If unset, the link never expires. format: date-time description: MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token. Memo_Property: type: object properties: hasLink: type: boolean hasTaskList: type: boolean hasCode: type: boolean hasIncompleteTasks: type: boolean title: type: string description: The title extracted from the first H1 heading, if present. description: Computed properties of a memo. NotificationSetting_EmailSetting: type: object properties: enabled: type: boolean smtpHost: type: string smtpPort: type: integer format: int32 smtpUsername: type: string smtpPassword: type: string fromEmail: type: string fromName: type: string replyTo: type: string useTls: type: boolean useSsl: type: boolean description: Email delivery configuration for notifications. OAuth2Config: 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: '#/components/schemas/FieldMapping' PersonalAccessToken: type: object properties: name: type: string description: |- The resource name of the personal access token. Format: users/{user}/personalAccessTokens/{personal_access_token} description: type: string description: The description of the token. createdAt: readOnly: true type: string description: Output only. The creation timestamp. format: date-time expiresAt: type: string description: Optional. The expiration timestamp. format: date-time lastUsedAt: readOnly: true type: string description: Output only. The last used timestamp. format: date-time description: |- PersonalAccessToken represents a long-lived token for API/script access. PATs are distinct from short-lived JWT access tokens used for session authentication. Reaction: required: - contentId - reactionType type: object properties: name: readOnly: true type: string description: |- The resource name of the reaction. Format: memos/{memo}/reactions/{reaction} creator: readOnly: true type: string description: |- The resource name of the creator. Format: users/{user} contentId: type: string description: |- 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: readOnly: true type: string description: Output only. The creation timestamp. format: date-time RefreshTokenRequest: type: object properties: {} RefreshTokenResponse: type: object properties: accessToken: type: string description: The new short-lived access token. expiresAt: type: string description: When the access token expires. format: date-time SetMemoAttachmentsRequest: required: - name - attachments type: object properties: name: type: string description: |- Required. The resource name of the memo. Format: memos/{memo} attachments: type: array items: $ref: '#/components/schemas/Attachment' description: Required. The attachments to set for the memo. SetMemoRelationsRequest: required: - name - relations type: object properties: name: type: string description: |- Required. The resource name of the memo. Format: memos/{memo} relations: type: array items: $ref: '#/components/schemas/MemoRelation' description: Required. The relations to set for the memo. Shortcut: required: - title type: object properties: name: type: string description: |- The resource name of the shortcut. Format: users/{user}/shortcuts/{shortcut} title: type: string description: The title of the shortcut. filter: type: string description: The filter expression for the shortcut. SignInRequest: type: object properties: passwordCredentials: allOf: - $ref: '#/components/schemas/SignInRequest_PasswordCredentials' description: Username and password authentication. ssoCredentials: allOf: - $ref: '#/components/schemas/SignInRequest_SSOCredentials' description: SSO provider authentication. SignInRequest_PasswordCredentials: required: - username - password type: object properties: username: type: string description: The username to sign in with. password: type: string description: The password to sign in with. description: Nested message for password-based authentication credentials. SignInRequest_SSOCredentials: required: - idpName - code - redirectUri type: object properties: idpName: type: string description: |- The resource name of the SSO provider. Format: identity-providers/{uid} code: type: string description: The authorization code from the SSO provider. redirectUri: type: string description: The redirect URI used in the SSO flow. codeVerifier: type: string description: |- The PKCE code verifier for enhanced security (RFC 7636). Optional - enables PKCE flow protection against authorization code interception. description: Nested message for SSO authentication credentials. SignInResponse: type: object properties: user: allOf: - $ref: '#/components/schemas/User' description: The authenticated user's information. accessToken: type: string description: |- The short-lived access token for API requests. Store in memory only, not in localStorage. accessTokenExpiresAt: type: string description: |- When the access token expires. Client should call RefreshToken before this time. format: date-time Status: type: object properties: code: type: integer description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. format: int32 message: type: string description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. details: type: array items: $ref: '#/components/schemas/GoogleProtobufAny' description: A list of messages that carry the error details. There is a common set of message types for APIs to use. description: 'The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors).' StorageSetting_S3Config: type: object properties: accessKeyId: type: string accessKeySecret: type: string endpoint: type: string region: type: string bucket: type: string usePathStyle: type: boolean description: |- S3 configuration for cloud storage backend. Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/ UpsertMemoReactionRequest: required: - name - reaction type: object properties: name: type: string description: |- Required. The resource name of the memo. Format: memos/{memo} reaction: allOf: - $ref: '#/components/schemas/Reaction' description: Required. The reaction to upsert. User: required: - role - username - state type: object properties: name: type: string description: |- The resource name of the user. Format: users/{user} role: enum: - ROLE_UNSPECIFIED - ADMIN - USER type: string description: The role of the user. format: enum 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: writeOnly: true type: string description: Input only. The password for the user. state: enum: - STATE_UNSPECIFIED - NORMAL - ARCHIVED type: string description: The state of the user. format: enum createTime: readOnly: true type: string description: Output only. The creation timestamp. format: date-time updateTime: readOnly: true type: string description: Output only. The last update timestamp. format: date-time UserNotification: type: object properties: name: readOnly: true type: string description: |- The resource name of the notification. Format: users/{user}/notifications/{notification} sender: readOnly: true type: string description: |- The sender of the notification. Format: users/{user} status: enum: - STATUS_UNSPECIFIED - UNREAD - ARCHIVED type: string description: The status of the notification. format: enum createTime: readOnly: true type: string description: The creation timestamp. format: date-time type: readOnly: true enum: - TYPE_UNSPECIFIED - MEMO_COMMENT type: string description: The type of the notification. format: enum memoComment: readOnly: true allOf: - $ref: '#/components/schemas/UserNotification_MemoCommentPayload' UserNotification_MemoCommentPayload: type: object properties: memo: type: string description: |- The memo name of comment. Format: memos/{memo} relatedMemo: type: string description: |- The name of related memo. Format: memos/{memo} UserSetting: type: object properties: name: type: string description: |- The name of the user setting. Format: users/{user}/settings/{setting}, {setting} is the key for the setting. For example, "users/123/settings/GENERAL" for general settings. generalSetting: $ref: '#/components/schemas/UserSetting_GeneralSetting' webhooksSetting: $ref: '#/components/schemas/UserSetting_WebhooksSetting' description: User settings message UserSetting_GeneralSetting: type: object properties: locale: type: string description: The preferred locale of the user. memoVisibility: type: string description: The default visibility of the memo. theme: type: string description: |- 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. description: General user settings configuration. UserSetting_WebhooksSetting: type: object properties: webhooks: type: array items: $ref: '#/components/schemas/UserWebhook' description: List of user webhooks. description: User webhooks configuration. UserStats: type: object properties: name: type: string description: |- The resource name of the user whose stats these are. Format: users/{user} memoDisplayTimestamps: type: array items: type: string format: date-time description: The timestamps when the memos were displayed. memoTypeStats: allOf: - $ref: '#/components/schemas/UserStats_MemoTypeStats' 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 description: Total memo count. format: int32 description: User statistics messages UserStats_MemoTypeStats: 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. UserWebhook: type: object properties: name: type: string description: |- The name of the webhook. Format: users/{user}/webhooks/{webhook} url: type: string description: The URL to send the webhook to. displayName: type: string description: Optional. Human-readable name for the webhook. createTime: readOnly: true type: string description: The creation time of the webhook. format: date-time updateTime: readOnly: true type: string description: The last update time of the webhook. format: date-time description: UserWebhook represents a webhook owned by a user. tags: - name: AttachmentService - name: AuthService - name: IdentityProviderService - name: InstanceService - name: MemoService - name: ShortcutService - name: UserService ================================================ FILE: proto/gen/store/attachment.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // 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\x1cstore/instance_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_instance_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 } ================================================ FILE: proto/gen/store/idp.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // 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"` Uid string `protobuf:"bytes,6,opt,name=uid,proto3" json:"uid,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 } func (x *IdentityProvider) GetUid() string { if x != nil { return x.Uid } return "" } 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\"\x94\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\x12\x10\n" + "\x03uid\x18\x06 \x01(\tR\x03uid\"(\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 } ================================================ FILE: proto/gen/store/inbox.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // 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 // Memo comment notification. InboxMessage_MEMO_COMMENT InboxMessage_Type = 1 ) // Enum value maps for InboxMessage_Type. var ( InboxMessage_Type_name = map[int32]string{ 0: "TYPE_UNSPECIFIED", 1: "MEMO_COMMENT", } InboxMessage_Type_value = map[string]int32{ "TYPE_UNSPECIFIED": 0, "MEMO_COMMENT": 1, } ) 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"` // The type of the inbox message. Type InboxMessage_Type `protobuf:"varint,1,opt,name=type,proto3,enum=memos.store.InboxMessage_Type" json:"type,omitempty"` // Types that are valid to be assigned to Payload: // // *InboxMessage_MemoComment Payload isInboxMessage_Payload `protobuf_oneof:"payload"` 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) GetPayload() isInboxMessage_Payload { if x != nil { return x.Payload } return nil } func (x *InboxMessage) GetMemoComment() *InboxMessage_MemoCommentPayload { if x != nil { if x, ok := x.Payload.(*InboxMessage_MemoComment); ok { return x.MemoComment } } return nil } type isInboxMessage_Payload interface { isInboxMessage_Payload() } type InboxMessage_MemoComment struct { MemoComment *InboxMessage_MemoCommentPayload `protobuf:"bytes,2,opt,name=memo_comment,json=memoComment,proto3,oneof"` } func (*InboxMessage_MemoComment) isInboxMessage_Payload() {} type InboxMessage_MemoCommentPayload 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 *InboxMessage_MemoCommentPayload) Reset() { *x = InboxMessage_MemoCommentPayload{} mi := &file_store_inbox_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InboxMessage_MemoCommentPayload) String() string { return protoimpl.X.MessageStringOf(x) } func (*InboxMessage_MemoCommentPayload) ProtoMessage() {} func (x *InboxMessage_MemoCommentPayload) ProtoReflect() protoreflect.Message { mi := &file_store_inbox_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 InboxMessage_MemoCommentPayload.ProtoReflect.Descriptor instead. func (*InboxMessage_MemoCommentPayload) Descriptor() ([]byte, []int) { return file_store_inbox_proto_rawDescGZIP(), []int{0, 0} } func (x *InboxMessage_MemoCommentPayload) GetMemoId() int32 { if x != nil { return x.MemoId } return 0 } func (x *InboxMessage_MemoCommentPayload) GetRelatedMemoId() int32 { if x != nil { return x.RelatedMemoId } return 0 } var File_store_inbox_proto protoreflect.FileDescriptor const file_store_inbox_proto_rawDesc = "" + "\n" + "\x11store/inbox.proto\x12\vmemos.store\"\xa7\x02\n" + "\fInboxMessage\x122\n" + "\x04type\x18\x01 \x01(\x0e2\x1e.memos.store.InboxMessage.TypeR\x04type\x12Q\n" + "\fmemo_comment\x18\x02 \x01(\v2,.memos.store.InboxMessage.MemoCommentPayloadH\x00R\vmemoComment\x1aU\n" + "\x12MemoCommentPayload\x12\x17\n" + "\amemo_id\x18\x01 \x01(\x05R\x06memoId\x12&\n" + "\x0frelated_memo_id\x18\x02 \x01(\x05R\rrelatedMemoId\".\n" + "\x04Type\x12\x14\n" + "\x10TYPE_UNSPECIFIED\x10\x00\x12\x10\n" + "\fMEMO_COMMENT\x10\x01B\t\n" + "\apayloadB\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, 2) var file_store_inbox_proto_goTypes = []any{ (InboxMessage_Type)(0), // 0: memos.store.InboxMessage.Type (*InboxMessage)(nil), // 1: memos.store.InboxMessage (*InboxMessage_MemoCommentPayload)(nil), // 2: memos.store.InboxMessage.MemoCommentPayload } var file_store_inbox_proto_depIdxs = []int32{ 0, // 0: memos.store.InboxMessage.type:type_name -> memos.store.InboxMessage.Type 2, // 1: memos.store.InboxMessage.memo_comment:type_name -> memos.store.InboxMessage.MemoCommentPayload 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_inbox_proto_init() } func file_store_inbox_proto_init() { if File_store_inbox_proto != nil { return } file_store_inbox_proto_msgTypes[0].OneofWrappers = []any{ (*InboxMessage_MemoComment)(nil), } 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: 2, 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 } ================================================ FILE: proto/gen/store/instance_setting.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc (unknown) // source: store/instance_setting.proto package store import ( color "google.golang.org/genproto/googleapis/type/color" 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 InstanceSettingKey int32 const ( InstanceSettingKey_INSTANCE_SETTING_KEY_UNSPECIFIED InstanceSettingKey = 0 // BASIC is the key for basic settings. InstanceSettingKey_BASIC InstanceSettingKey = 1 // GENERAL is the key for general settings. InstanceSettingKey_GENERAL InstanceSettingKey = 2 // STORAGE is the key for storage settings. InstanceSettingKey_STORAGE InstanceSettingKey = 3 // MEMO_RELATED is the key for memo related settings. InstanceSettingKey_MEMO_RELATED InstanceSettingKey = 4 // TAGS is the key for tag metadata. InstanceSettingKey_TAGS InstanceSettingKey = 5 // NOTIFICATION is the key for notification transport settings. InstanceSettingKey_NOTIFICATION InstanceSettingKey = 6 ) // Enum value maps for InstanceSettingKey. var ( InstanceSettingKey_name = map[int32]string{ 0: "INSTANCE_SETTING_KEY_UNSPECIFIED", 1: "BASIC", 2: "GENERAL", 3: "STORAGE", 4: "MEMO_RELATED", 5: "TAGS", 6: "NOTIFICATION", } InstanceSettingKey_value = map[string]int32{ "INSTANCE_SETTING_KEY_UNSPECIFIED": 0, "BASIC": 1, "GENERAL": 2, "STORAGE": 3, "MEMO_RELATED": 4, "TAGS": 5, "NOTIFICATION": 6, } ) func (x InstanceSettingKey) Enum() *InstanceSettingKey { p := new(InstanceSettingKey) *p = x return p } func (x InstanceSettingKey) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (InstanceSettingKey) Descriptor() protoreflect.EnumDescriptor { return file_store_instance_setting_proto_enumTypes[0].Descriptor() } func (InstanceSettingKey) Type() protoreflect.EnumType { return &file_store_instance_setting_proto_enumTypes[0] } func (x InstanceSettingKey) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use InstanceSettingKey.Descriptor instead. func (InstanceSettingKey) EnumDescriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{0} } type InstanceStorageSetting_StorageType int32 const ( InstanceStorageSetting_STORAGE_TYPE_UNSPECIFIED InstanceStorageSetting_StorageType = 0 // STORAGE_TYPE_DATABASE is the database storage type. InstanceStorageSetting_DATABASE InstanceStorageSetting_StorageType = 1 // STORAGE_TYPE_LOCAL is the local storage type. InstanceStorageSetting_LOCAL InstanceStorageSetting_StorageType = 2 // STORAGE_TYPE_S3 is the S3 storage type. InstanceStorageSetting_S3 InstanceStorageSetting_StorageType = 3 ) // Enum value maps for InstanceStorageSetting_StorageType. var ( InstanceStorageSetting_StorageType_name = map[int32]string{ 0: "STORAGE_TYPE_UNSPECIFIED", 1: "DATABASE", 2: "LOCAL", 3: "S3", } InstanceStorageSetting_StorageType_value = map[string]int32{ "STORAGE_TYPE_UNSPECIFIED": 0, "DATABASE": 1, "LOCAL": 2, "S3": 3, } ) func (x InstanceStorageSetting_StorageType) Enum() *InstanceStorageSetting_StorageType { p := new(InstanceStorageSetting_StorageType) *p = x return p } func (x InstanceStorageSetting_StorageType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (InstanceStorageSetting_StorageType) Descriptor() protoreflect.EnumDescriptor { return file_store_instance_setting_proto_enumTypes[1].Descriptor() } func (InstanceStorageSetting_StorageType) Type() protoreflect.EnumType { return &file_store_instance_setting_proto_enumTypes[1] } func (x InstanceStorageSetting_StorageType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use InstanceStorageSetting_StorageType.Descriptor instead. func (InstanceStorageSetting_StorageType) EnumDescriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{4, 0} } type InstanceSetting struct { state protoimpl.MessageState `protogen:"open.v1"` Key InstanceSettingKey `protobuf:"varint,1,opt,name=key,proto3,enum=memos.store.InstanceSettingKey" json:"key,omitempty"` // Types that are valid to be assigned to Value: // // *InstanceSetting_BasicSetting // *InstanceSetting_GeneralSetting // *InstanceSetting_StorageSetting // *InstanceSetting_MemoRelatedSetting // *InstanceSetting_TagsSetting // *InstanceSetting_NotificationSetting Value isInstanceSetting_Value `protobuf_oneof:"value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceSetting) Reset() { *x = InstanceSetting{} mi := &file_store_instance_setting_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceSetting) ProtoMessage() {} func (x *InstanceSetting) ProtoReflect() protoreflect.Message { mi := &file_store_instance_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 InstanceSetting.ProtoReflect.Descriptor instead. func (*InstanceSetting) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{0} } func (x *InstanceSetting) GetKey() InstanceSettingKey { if x != nil { return x.Key } return InstanceSettingKey_INSTANCE_SETTING_KEY_UNSPECIFIED } func (x *InstanceSetting) GetValue() isInstanceSetting_Value { if x != nil { return x.Value } return nil } func (x *InstanceSetting) GetBasicSetting() *InstanceBasicSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_BasicSetting); ok { return x.BasicSetting } } return nil } func (x *InstanceSetting) GetGeneralSetting() *InstanceGeneralSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_GeneralSetting); ok { return x.GeneralSetting } } return nil } func (x *InstanceSetting) GetStorageSetting() *InstanceStorageSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_StorageSetting); ok { return x.StorageSetting } } return nil } func (x *InstanceSetting) GetMemoRelatedSetting() *InstanceMemoRelatedSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_MemoRelatedSetting); ok { return x.MemoRelatedSetting } } return nil } func (x *InstanceSetting) GetTagsSetting() *InstanceTagsSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_TagsSetting); ok { return x.TagsSetting } } return nil } func (x *InstanceSetting) GetNotificationSetting() *InstanceNotificationSetting { if x != nil { if x, ok := x.Value.(*InstanceSetting_NotificationSetting); ok { return x.NotificationSetting } } return nil } type isInstanceSetting_Value interface { isInstanceSetting_Value() } type InstanceSetting_BasicSetting struct { BasicSetting *InstanceBasicSetting `protobuf:"bytes,2,opt,name=basic_setting,json=basicSetting,proto3,oneof"` } type InstanceSetting_GeneralSetting struct { GeneralSetting *InstanceGeneralSetting `protobuf:"bytes,3,opt,name=general_setting,json=generalSetting,proto3,oneof"` } type InstanceSetting_StorageSetting struct { StorageSetting *InstanceStorageSetting `protobuf:"bytes,4,opt,name=storage_setting,json=storageSetting,proto3,oneof"` } type InstanceSetting_MemoRelatedSetting struct { MemoRelatedSetting *InstanceMemoRelatedSetting `protobuf:"bytes,5,opt,name=memo_related_setting,json=memoRelatedSetting,proto3,oneof"` } type InstanceSetting_TagsSetting struct { TagsSetting *InstanceTagsSetting `protobuf:"bytes,6,opt,name=tags_setting,json=tagsSetting,proto3,oneof"` } type InstanceSetting_NotificationSetting struct { NotificationSetting *InstanceNotificationSetting `protobuf:"bytes,7,opt,name=notification_setting,json=notificationSetting,proto3,oneof"` } func (*InstanceSetting_BasicSetting) isInstanceSetting_Value() {} func (*InstanceSetting_GeneralSetting) isInstanceSetting_Value() {} func (*InstanceSetting_StorageSetting) isInstanceSetting_Value() {} func (*InstanceSetting_MemoRelatedSetting) isInstanceSetting_Value() {} func (*InstanceSetting_TagsSetting) isInstanceSetting_Value() {} func (*InstanceSetting_NotificationSetting) isInstanceSetting_Value() {} type InstanceBasicSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // The secret key for instance. 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 *InstanceBasicSetting) Reset() { *x = InstanceBasicSetting{} mi := &file_store_instance_setting_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceBasicSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceBasicSetting) ProtoMessage() {} func (x *InstanceBasicSetting) ProtoReflect() protoreflect.Message { mi := &file_store_instance_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 InstanceBasicSetting.ProtoReflect.Descriptor instead. func (*InstanceBasicSetting) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{1} } func (x *InstanceBasicSetting) GetSecretKey() string { if x != nil { return x.SecretKey } return "" } func (x *InstanceBasicSetting) GetSchemaVersion() string { if x != nil { return x.SchemaVersion } return "" } type InstanceGeneralSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // 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 *InstanceCustomProfile `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 *InstanceGeneralSetting) Reset() { *x = InstanceGeneralSetting{} mi := &file_store_instance_setting_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceGeneralSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceGeneralSetting) ProtoMessage() {} func (x *InstanceGeneralSetting) ProtoReflect() protoreflect.Message { mi := &file_store_instance_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 InstanceGeneralSetting.ProtoReflect.Descriptor instead. func (*InstanceGeneralSetting) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{2} } func (x *InstanceGeneralSetting) GetDisallowUserRegistration() bool { if x != nil { return x.DisallowUserRegistration } return false } func (x *InstanceGeneralSetting) GetDisallowPasswordAuth() bool { if x != nil { return x.DisallowPasswordAuth } return false } func (x *InstanceGeneralSetting) GetAdditionalScript() string { if x != nil { return x.AdditionalScript } return "" } func (x *InstanceGeneralSetting) GetAdditionalStyle() string { if x != nil { return x.AdditionalStyle } return "" } func (x *InstanceGeneralSetting) GetCustomProfile() *InstanceCustomProfile { if x != nil { return x.CustomProfile } return nil } func (x *InstanceGeneralSetting) GetWeekStartDayOffset() int32 { if x != nil { return x.WeekStartDayOffset } return 0 } func (x *InstanceGeneralSetting) GetDisallowChangeUsername() bool { if x != nil { return x.DisallowChangeUsername } return false } func (x *InstanceGeneralSetting) GetDisallowChangeNickname() bool { if x != nil { return x.DisallowChangeNickname } return false } type InstanceCustomProfile 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"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceCustomProfile) Reset() { *x = InstanceCustomProfile{} mi := &file_store_instance_setting_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceCustomProfile) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceCustomProfile) ProtoMessage() {} func (x *InstanceCustomProfile) ProtoReflect() protoreflect.Message { mi := &file_store_instance_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 InstanceCustomProfile.ProtoReflect.Descriptor instead. func (*InstanceCustomProfile) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{3} } func (x *InstanceCustomProfile) GetTitle() string { if x != nil { return x.Title } return "" } func (x *InstanceCustomProfile) GetDescription() string { if x != nil { return x.Description } return "" } func (x *InstanceCustomProfile) GetLogoUrl() string { if x != nil { return x.LogoUrl } return "" } type InstanceStorageSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // storage_type is the storage type. StorageType InstanceStorageSetting_StorageType `protobuf:"varint,1,opt,name=storage_type,json=storageType,proto3,enum=memos.store.InstanceStorageSetting_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 *InstanceStorageSetting) Reset() { *x = InstanceStorageSetting{} mi := &file_store_instance_setting_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceStorageSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceStorageSetting) ProtoMessage() {} func (x *InstanceStorageSetting) ProtoReflect() protoreflect.Message { mi := &file_store_instance_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 InstanceStorageSetting.ProtoReflect.Descriptor instead. func (*InstanceStorageSetting) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{4} } func (x *InstanceStorageSetting) GetStorageType() InstanceStorageSetting_StorageType { if x != nil { return x.StorageType } return InstanceStorageSetting_STORAGE_TYPE_UNSPECIFIED } func (x *InstanceStorageSetting) GetFilepathTemplate() string { if x != nil { return x.FilepathTemplate } return "" } func (x *InstanceStorageSetting) GetUploadSizeLimitMb() int64 { if x != nil { return x.UploadSizeLimitMb } return 0 } func (x *InstanceStorageSetting) 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_instance_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_instance_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_instance_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 InstanceMemoRelatedSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // 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"` // reactions is the list of reactions. Reactions []string `protobuf:"bytes,7,rep,name=reactions,proto3" json:"reactions,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceMemoRelatedSetting) Reset() { *x = InstanceMemoRelatedSetting{} mi := &file_store_instance_setting_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceMemoRelatedSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceMemoRelatedSetting) ProtoMessage() {} func (x *InstanceMemoRelatedSetting) ProtoReflect() protoreflect.Message { mi := &file_store_instance_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 InstanceMemoRelatedSetting.ProtoReflect.Descriptor instead. func (*InstanceMemoRelatedSetting) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{6} } func (x *InstanceMemoRelatedSetting) GetDisplayWithUpdateTime() bool { if x != nil { return x.DisplayWithUpdateTime } return false } func (x *InstanceMemoRelatedSetting) GetContentLengthLimit() int32 { if x != nil { return x.ContentLengthLimit } return 0 } func (x *InstanceMemoRelatedSetting) GetEnableDoubleClickEdit() bool { if x != nil { return x.EnableDoubleClickEdit } return false } func (x *InstanceMemoRelatedSetting) GetReactions() []string { if x != nil { return x.Reactions } return nil } type InstanceTagMetadata struct { state protoimpl.MessageState `protogen:"open.v1"` // Background color for the tag label. BackgroundColor *color.Color `protobuf:"bytes,1,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceTagMetadata) Reset() { *x = InstanceTagMetadata{} mi := &file_store_instance_setting_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceTagMetadata) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceTagMetadata) ProtoMessage() {} func (x *InstanceTagMetadata) ProtoReflect() protoreflect.Message { mi := &file_store_instance_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 InstanceTagMetadata.ProtoReflect.Descriptor instead. func (*InstanceTagMetadata) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{7} } func (x *InstanceTagMetadata) GetBackgroundColor() *color.Color { if x != nil { return x.BackgroundColor } return nil } type InstanceTagsSetting struct { state protoimpl.MessageState `protogen:"open.v1"` Tags map[string]*InstanceTagMetadata `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceTagsSetting) Reset() { *x = InstanceTagsSetting{} mi := &file_store_instance_setting_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceTagsSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceTagsSetting) ProtoMessage() {} func (x *InstanceTagsSetting) ProtoReflect() protoreflect.Message { mi := &file_store_instance_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 InstanceTagsSetting.ProtoReflect.Descriptor instead. func (*InstanceTagsSetting) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{8} } func (x *InstanceTagsSetting) GetTags() map[string]*InstanceTagMetadata { if x != nil { return x.Tags } return nil } type InstanceNotificationSetting struct { state protoimpl.MessageState `protogen:"open.v1"` Email *InstanceNotificationSetting_EmailSetting `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceNotificationSetting) Reset() { *x = InstanceNotificationSetting{} mi := &file_store_instance_setting_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceNotificationSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceNotificationSetting) ProtoMessage() {} func (x *InstanceNotificationSetting) ProtoReflect() protoreflect.Message { mi := &file_store_instance_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 InstanceNotificationSetting.ProtoReflect.Descriptor instead. func (*InstanceNotificationSetting) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{9} } func (x *InstanceNotificationSetting) GetEmail() *InstanceNotificationSetting_EmailSetting { if x != nil { return x.Email } return nil } type InstanceNotificationSetting_EmailSetting struct { state protoimpl.MessageState `protogen:"open.v1"` Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` SmtpHost string `protobuf:"bytes,2,opt,name=smtp_host,json=smtpHost,proto3" json:"smtp_host,omitempty"` SmtpPort int32 `protobuf:"varint,3,opt,name=smtp_port,json=smtpPort,proto3" json:"smtp_port,omitempty"` SmtpUsername string `protobuf:"bytes,4,opt,name=smtp_username,json=smtpUsername,proto3" json:"smtp_username,omitempty"` SmtpPassword string `protobuf:"bytes,5,opt,name=smtp_password,json=smtpPassword,proto3" json:"smtp_password,omitempty"` FromEmail string `protobuf:"bytes,6,opt,name=from_email,json=fromEmail,proto3" json:"from_email,omitempty"` FromName string `protobuf:"bytes,7,opt,name=from_name,json=fromName,proto3" json:"from_name,omitempty"` ReplyTo string `protobuf:"bytes,8,opt,name=reply_to,json=replyTo,proto3" json:"reply_to,omitempty"` UseTls bool `protobuf:"varint,9,opt,name=use_tls,json=useTls,proto3" json:"use_tls,omitempty"` UseSsl bool `protobuf:"varint,10,opt,name=use_ssl,json=useSsl,proto3" json:"use_ssl,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InstanceNotificationSetting_EmailSetting) Reset() { *x = InstanceNotificationSetting_EmailSetting{} mi := &file_store_instance_setting_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *InstanceNotificationSetting_EmailSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*InstanceNotificationSetting_EmailSetting) ProtoMessage() {} func (x *InstanceNotificationSetting_EmailSetting) ProtoReflect() protoreflect.Message { mi := &file_store_instance_setting_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 InstanceNotificationSetting_EmailSetting.ProtoReflect.Descriptor instead. func (*InstanceNotificationSetting_EmailSetting) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{9, 0} } func (x *InstanceNotificationSetting_EmailSetting) GetEnabled() bool { if x != nil { return x.Enabled } return false } func (x *InstanceNotificationSetting_EmailSetting) GetSmtpHost() string { if x != nil { return x.SmtpHost } return "" } func (x *InstanceNotificationSetting_EmailSetting) GetSmtpPort() int32 { if x != nil { return x.SmtpPort } return 0 } func (x *InstanceNotificationSetting_EmailSetting) GetSmtpUsername() string { if x != nil { return x.SmtpUsername } return "" } func (x *InstanceNotificationSetting_EmailSetting) GetSmtpPassword() string { if x != nil { return x.SmtpPassword } return "" } func (x *InstanceNotificationSetting_EmailSetting) GetFromEmail() string { if x != nil { return x.FromEmail } return "" } func (x *InstanceNotificationSetting_EmailSetting) GetFromName() string { if x != nil { return x.FromName } return "" } func (x *InstanceNotificationSetting_EmailSetting) GetReplyTo() string { if x != nil { return x.ReplyTo } return "" } func (x *InstanceNotificationSetting_EmailSetting) GetUseTls() bool { if x != nil { return x.UseTls } return false } func (x *InstanceNotificationSetting_EmailSetting) GetUseSsl() bool { if x != nil { return x.UseSsl } return false } var File_store_instance_setting_proto protoreflect.FileDescriptor const file_store_instance_setting_proto_rawDesc = "" + "\n" + "\x1cstore/instance_setting.proto\x12\vmemos.store\x1a\x17google/type/color.proto\"\xba\x04\n" + "\x0fInstanceSetting\x121\n" + "\x03key\x18\x01 \x01(\x0e2\x1f.memos.store.InstanceSettingKeyR\x03key\x12H\n" + "\rbasic_setting\x18\x02 \x01(\v2!.memos.store.InstanceBasicSettingH\x00R\fbasicSetting\x12N\n" + "\x0fgeneral_setting\x18\x03 \x01(\v2#.memos.store.InstanceGeneralSettingH\x00R\x0egeneralSetting\x12N\n" + "\x0fstorage_setting\x18\x04 \x01(\v2#.memos.store.InstanceStorageSettingH\x00R\x0estorageSetting\x12[\n" + "\x14memo_related_setting\x18\x05 \x01(\v2'.memos.store.InstanceMemoRelatedSettingH\x00R\x12memoRelatedSetting\x12E\n" + "\ftags_setting\x18\x06 \x01(\v2 .memos.store.InstanceTagsSettingH\x00R\vtagsSetting\x12]\n" + "\x14notification_setting\x18\a \x01(\v2(.memos.store.InstanceNotificationSettingH\x00R\x13notificationSettingB\a\n" + "\x05value\"\\\n" + "\x14InstanceBasicSetting\x12\x1d\n" + "\n" + "secret_key\x18\x01 \x01(\tR\tsecretKey\x12%\n" + "\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"\xd6\x03\n" + "\x16InstanceGeneralSetting\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\x12I\n" + "\x0ecustom_profile\x18\x06 \x01(\v2\".memos.store.InstanceCustomProfileR\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\"j\n" + "\x15InstanceCustomProfile\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\"\xd3\x02\n" + "\x16InstanceStorageSetting\x12R\n" + "\fstorage_type\x18\x01 \x01(\x0e2/.memos.store.InstanceStorageSetting.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\"\xde\x01\n" + "\x1aInstanceMemoRelatedSetting\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\x1c\n" + "\treactions\x18\a \x03(\tR\treactions\"T\n" + "\x13InstanceTagMetadata\x12=\n" + "\x10background_color\x18\x01 \x01(\v2\x12.google.type.ColorR\x0fbackgroundColor\"\xb0\x01\n" + "\x13InstanceTagsSetting\x12>\n" + "\x04tags\x18\x01 \x03(\v2*.memos.store.InstanceTagsSetting.TagsEntryR\x04tags\x1aY\n" + "\tTagsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x126\n" + "\x05value\x18\x02 \x01(\v2 .memos.store.InstanceTagMetadataR\x05value:\x028\x01\"\xa2\x03\n" + "\x1bInstanceNotificationSetting\x12K\n" + "\x05email\x18\x01 \x01(\v25.memos.store.InstanceNotificationSetting.EmailSettingR\x05email\x1a\xb5\x02\n" + "\fEmailSetting\x12\x18\n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x12\x1b\n" + "\tsmtp_host\x18\x02 \x01(\tR\bsmtpHost\x12\x1b\n" + "\tsmtp_port\x18\x03 \x01(\x05R\bsmtpPort\x12#\n" + "\rsmtp_username\x18\x04 \x01(\tR\fsmtpUsername\x12#\n" + "\rsmtp_password\x18\x05 \x01(\tR\fsmtpPassword\x12\x1d\n" + "\n" + "from_email\x18\x06 \x01(\tR\tfromEmail\x12\x1b\n" + "\tfrom_name\x18\a \x01(\tR\bfromName\x12\x19\n" + "\breply_to\x18\b \x01(\tR\areplyTo\x12\x17\n" + "\ause_tls\x18\t \x01(\bR\x06useTls\x12\x17\n" + "\ause_ssl\x18\n" + " \x01(\bR\x06useSsl*\x8d\x01\n" + "\x12InstanceSettingKey\x12$\n" + " INSTANCE_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\x04\x12\b\n" + "\x04TAGS\x10\x05\x12\x10\n" + "\fNOTIFICATION\x10\x06B\x9f\x01\n" + "\x0fcom.memos.storeB\x14InstanceSettingProtoP\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_instance_setting_proto_rawDescOnce sync.Once file_store_instance_setting_proto_rawDescData []byte ) func file_store_instance_setting_proto_rawDescGZIP() []byte { file_store_instance_setting_proto_rawDescOnce.Do(func() { file_store_instance_setting_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_instance_setting_proto_rawDesc), len(file_store_instance_setting_proto_rawDesc))) }) return file_store_instance_setting_proto_rawDescData } var file_store_instance_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_store_instance_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_store_instance_setting_proto_goTypes = []any{ (InstanceSettingKey)(0), // 0: memos.store.InstanceSettingKey (InstanceStorageSetting_StorageType)(0), // 1: memos.store.InstanceStorageSetting.StorageType (*InstanceSetting)(nil), // 2: memos.store.InstanceSetting (*InstanceBasicSetting)(nil), // 3: memos.store.InstanceBasicSetting (*InstanceGeneralSetting)(nil), // 4: memos.store.InstanceGeneralSetting (*InstanceCustomProfile)(nil), // 5: memos.store.InstanceCustomProfile (*InstanceStorageSetting)(nil), // 6: memos.store.InstanceStorageSetting (*StorageS3Config)(nil), // 7: memos.store.StorageS3Config (*InstanceMemoRelatedSetting)(nil), // 8: memos.store.InstanceMemoRelatedSetting (*InstanceTagMetadata)(nil), // 9: memos.store.InstanceTagMetadata (*InstanceTagsSetting)(nil), // 10: memos.store.InstanceTagsSetting (*InstanceNotificationSetting)(nil), // 11: memos.store.InstanceNotificationSetting nil, // 12: memos.store.InstanceTagsSetting.TagsEntry (*InstanceNotificationSetting_EmailSetting)(nil), // 13: memos.store.InstanceNotificationSetting.EmailSetting (*color.Color)(nil), // 14: google.type.Color } var file_store_instance_setting_proto_depIdxs = []int32{ 0, // 0: memos.store.InstanceSetting.key:type_name -> memos.store.InstanceSettingKey 3, // 1: memos.store.InstanceSetting.basic_setting:type_name -> memos.store.InstanceBasicSetting 4, // 2: memos.store.InstanceSetting.general_setting:type_name -> memos.store.InstanceGeneralSetting 6, // 3: memos.store.InstanceSetting.storage_setting:type_name -> memos.store.InstanceStorageSetting 8, // 4: memos.store.InstanceSetting.memo_related_setting:type_name -> memos.store.InstanceMemoRelatedSetting 10, // 5: memos.store.InstanceSetting.tags_setting:type_name -> memos.store.InstanceTagsSetting 11, // 6: memos.store.InstanceSetting.notification_setting:type_name -> memos.store.InstanceNotificationSetting 5, // 7: memos.store.InstanceGeneralSetting.custom_profile:type_name -> memos.store.InstanceCustomProfile 1, // 8: memos.store.InstanceStorageSetting.storage_type:type_name -> memos.store.InstanceStorageSetting.StorageType 7, // 9: memos.store.InstanceStorageSetting.s3_config:type_name -> memos.store.StorageS3Config 14, // 10: memos.store.InstanceTagMetadata.background_color:type_name -> google.type.Color 12, // 11: memos.store.InstanceTagsSetting.tags:type_name -> memos.store.InstanceTagsSetting.TagsEntry 13, // 12: memos.store.InstanceNotificationSetting.email:type_name -> memos.store.InstanceNotificationSetting.EmailSetting 9, // 13: memos.store.InstanceTagsSetting.TagsEntry.value:type_name -> memos.store.InstanceTagMetadata 14, // [14:14] is the sub-list for method output_type 14, // [14:14] is the sub-list for method input_type 14, // [14:14] is the sub-list for extension type_name 14, // [14:14] is the sub-list for extension extendee 0, // [0:14] is the sub-list for field type_name } func init() { file_store_instance_setting_proto_init() } func file_store_instance_setting_proto_init() { if File_store_instance_setting_proto != nil { return } file_store_instance_setting_proto_msgTypes[0].OneofWrappers = []any{ (*InstanceSetting_BasicSetting)(nil), (*InstanceSetting_GeneralSetting)(nil), (*InstanceSetting_StorageSetting)(nil), (*InstanceSetting_MemoRelatedSetting)(nil), (*InstanceSetting_TagsSetting)(nil), (*InstanceSetting_NotificationSetting)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_instance_setting_proto_rawDesc), len(file_store_instance_setting_proto_rawDesc)), NumEnums: 2, NumMessages: 12, NumExtensions: 0, NumServices: 0, }, GoTypes: file_store_instance_setting_proto_goTypes, DependencyIndexes: file_store_instance_setting_proto_depIdxs, EnumInfos: file_store_instance_setting_proto_enumTypes, MessageInfos: file_store_instance_setting_proto_msgTypes, }.Build() File_store_instance_setting_proto = out.File file_store_instance_setting_proto_goTypes = nil file_store_instance_setting_proto_depIdxs = nil } ================================================ FILE: proto/gen/store/memo.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // 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 title extracted from the first H1 heading, if present. Title string `protobuf:"bytes,5,opt,name=title,proto3" json:"title,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) GetTitle() string { if x != nil { return x.Title } return "" } 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\"\xb6\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\xac\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\x14\n" + "\x05title\x18\x05 \x01(\tR\x05title\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 } ================================================ FILE: proto/gen/store/user_setting.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // 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 // The shortcuts of the user. UserSetting_SHORTCUTS UserSetting_Key = 4 // The webhooks of the user. UserSetting_WEBHOOKS UserSetting_Key = 5 // Refresh tokens for the user. UserSetting_REFRESH_TOKENS UserSetting_Key = 6 // Personal access tokens for the user. UserSetting_PERSONAL_ACCESS_TOKENS UserSetting_Key = 7 ) // Enum value maps for UserSetting_Key. var ( UserSetting_Key_name = map[int32]string{ 0: "KEY_UNSPECIFIED", 1: "GENERAL", 4: "SHORTCUTS", 5: "WEBHOOKS", 6: "REFRESH_TOKENS", 7: "PERSONAL_ACCESS_TOKENS", } UserSetting_Key_value = map[string]int32{ "KEY_UNSPECIFIED": 0, "GENERAL": 1, "SHORTCUTS": 4, "WEBHOOKS": 5, "REFRESH_TOKENS": 6, "PERSONAL_ACCESS_TOKENS": 7, } ) 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_Shortcuts // *UserSetting_Webhooks // *UserSetting_RefreshTokens // *UserSetting_PersonalAccessTokens 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) 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 } func (x *UserSetting) GetRefreshTokens() *RefreshTokensUserSetting { if x != nil { if x, ok := x.Value.(*UserSetting_RefreshTokens); ok { return x.RefreshTokens } } return nil } func (x *UserSetting) GetPersonalAccessTokens() *PersonalAccessTokensUserSetting { if x != nil { if x, ok := x.Value.(*UserSetting_PersonalAccessTokens); ok { return x.PersonalAccessTokens } } return nil } type isUserSetting_Value interface { isUserSetting_Value() } type UserSetting_General struct { General *GeneralUserSetting `protobuf:"bytes,3,opt,name=general,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"` } type UserSetting_RefreshTokens struct { RefreshTokens *RefreshTokensUserSetting `protobuf:"bytes,8,opt,name=refresh_tokens,json=refreshTokens,proto3,oneof"` } type UserSetting_PersonalAccessTokens struct { PersonalAccessTokens *PersonalAccessTokensUserSetting `protobuf:"bytes,9,opt,name=personal_access_tokens,json=personalAccessTokens,proto3,oneof"` } func (*UserSetting_General) isUserSetting_Value() {} func (*UserSetting_Shortcuts) isUserSetting_Value() {} func (*UserSetting_Webhooks) isUserSetting_Value() {} func (*UserSetting_RefreshTokens) isUserSetting_Value() {} func (*UserSetting_PersonalAccessTokens) 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 memo visibility setting. MemoVisibility string `protobuf:"bytes,2,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,3,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) GetMemoVisibility() string { if x != nil { return x.MemoVisibility } return "" } func (x *GeneralUserSetting) GetTheme() string { if x != nil { return x.Theme } return "" } type RefreshTokensUserSetting struct { state protoimpl.MessageState `protogen:"open.v1"` RefreshTokens []*RefreshTokensUserSetting_RefreshToken `protobuf:"bytes,1,rep,name=refresh_tokens,json=refreshTokens,proto3" json:"refresh_tokens,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RefreshTokensUserSetting) Reset() { *x = RefreshTokensUserSetting{} mi := &file_store_user_setting_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RefreshTokensUserSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*RefreshTokensUserSetting) ProtoMessage() {} func (x *RefreshTokensUserSetting) 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 RefreshTokensUserSetting.ProtoReflect.Descriptor instead. func (*RefreshTokensUserSetting) Descriptor() ([]byte, []int) { return file_store_user_setting_proto_rawDescGZIP(), []int{2} } func (x *RefreshTokensUserSetting) GetRefreshTokens() []*RefreshTokensUserSetting_RefreshToken { if x != nil { return x.RefreshTokens } return nil } type PersonalAccessTokensUserSetting struct { state protoimpl.MessageState `protogen:"open.v1"` Tokens []*PersonalAccessTokensUserSetting_PersonalAccessToken `protobuf:"bytes,1,rep,name=tokens,proto3" json:"tokens,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PersonalAccessTokensUserSetting) Reset() { *x = PersonalAccessTokensUserSetting{} mi := &file_store_user_setting_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PersonalAccessTokensUserSetting) String() string { return protoimpl.X.MessageStringOf(x) } func (*PersonalAccessTokensUserSetting) ProtoMessage() {} func (x *PersonalAccessTokensUserSetting) 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 PersonalAccessTokensUserSetting.ProtoReflect.Descriptor instead. func (*PersonalAccessTokensUserSetting) Descriptor() ([]byte, []int) { return file_store_user_setting_proto_rawDescGZIP(), []int{3} } func (x *PersonalAccessTokensUserSetting) GetTokens() []*PersonalAccessTokensUserSetting_PersonalAccessToken { if x != nil { return x.Tokens } 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 RefreshTokensUserSetting_RefreshToken struct { state protoimpl.MessageState `protogen:"open.v1"` // Unique identifier (matches 'tid' claim in JWT) TokenId string `protobuf:"bytes,1,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"` // When the token expires ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // When the token was created CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // Client information for session management UI ClientInfo *RefreshTokensUserSetting_ClientInfo `protobuf:"bytes,4,opt,name=client_info,json=clientInfo,proto3" json:"client_info,omitempty"` // Optional description Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RefreshTokensUserSetting_RefreshToken) Reset() { *x = RefreshTokensUserSetting_RefreshToken{} mi := &file_store_user_setting_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RefreshTokensUserSetting_RefreshToken) String() string { return protoimpl.X.MessageStringOf(x) } func (*RefreshTokensUserSetting_RefreshToken) ProtoMessage() {} func (x *RefreshTokensUserSetting_RefreshToken) 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 RefreshTokensUserSetting_RefreshToken.ProtoReflect.Descriptor instead. func (*RefreshTokensUserSetting_RefreshToken) Descriptor() ([]byte, []int) { return file_store_user_setting_proto_rawDescGZIP(), []int{2, 0} } func (x *RefreshTokensUserSetting_RefreshToken) GetTokenId() string { if x != nil { return x.TokenId } return "" } func (x *RefreshTokensUserSetting_RefreshToken) GetExpiresAt() *timestamppb.Timestamp { if x != nil { return x.ExpiresAt } return nil } func (x *RefreshTokensUserSetting_RefreshToken) GetCreatedAt() *timestamppb.Timestamp { if x != nil { return x.CreatedAt } return nil } func (x *RefreshTokensUserSetting_RefreshToken) GetClientInfo() *RefreshTokensUserSetting_ClientInfo { if x != nil { return x.ClientInfo } return nil } func (x *RefreshTokensUserSetting_RefreshToken) GetDescription() string { if x != nil { return x.Description } return "" } type RefreshTokensUserSetting_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 *RefreshTokensUserSetting_ClientInfo) Reset() { *x = RefreshTokensUserSetting_ClientInfo{} mi := &file_store_user_setting_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RefreshTokensUserSetting_ClientInfo) String() string { return protoimpl.X.MessageStringOf(x) } func (*RefreshTokensUserSetting_ClientInfo) ProtoMessage() {} func (x *RefreshTokensUserSetting_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 RefreshTokensUserSetting_ClientInfo.ProtoReflect.Descriptor instead. func (*RefreshTokensUserSetting_ClientInfo) Descriptor() ([]byte, []int) { return file_store_user_setting_proto_rawDescGZIP(), []int{2, 1} } func (x *RefreshTokensUserSetting_ClientInfo) GetUserAgent() string { if x != nil { return x.UserAgent } return "" } func (x *RefreshTokensUserSetting_ClientInfo) GetIpAddress() string { if x != nil { return x.IpAddress } return "" } func (x *RefreshTokensUserSetting_ClientInfo) GetDeviceType() string { if x != nil { return x.DeviceType } return "" } func (x *RefreshTokensUserSetting_ClientInfo) GetOs() string { if x != nil { return x.Os } return "" } func (x *RefreshTokensUserSetting_ClientInfo) GetBrowser() string { if x != nil { return x.Browser } return "" } type PersonalAccessTokensUserSetting_PersonalAccessToken struct { state protoimpl.MessageState `protogen:"open.v1"` // Unique identifier for this token TokenId string `protobuf:"bytes,1,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"` // SHA-256 hash of the actual token TokenHash string `protobuf:"bytes,2,opt,name=token_hash,json=tokenHash,proto3" json:"token_hash,omitempty"` // User-provided description Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` // When the token expires (null = never) ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // When the token was created CreatedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // When the token was last used LastUsedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=last_used_at,json=lastUsedAt,proto3" json:"last_used_at,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) Reset() { *x = PersonalAccessTokensUserSetting_PersonalAccessToken{} mi := &file_store_user_setting_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) String() string { return protoimpl.X.MessageStringOf(x) } func (*PersonalAccessTokensUserSetting_PersonalAccessToken) ProtoMessage() {} func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) 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 PersonalAccessTokensUserSetting_PersonalAccessToken.ProtoReflect.Descriptor instead. func (*PersonalAccessTokensUserSetting_PersonalAccessToken) Descriptor() ([]byte, []int) { return file_store_user_setting_proto_rawDescGZIP(), []int{3, 0} } func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetTokenId() string { if x != nil { return x.TokenId } return "" } func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetTokenHash() string { if x != nil { return x.TokenHash } return "" } func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetDescription() string { if x != nil { return x.Description } return "" } func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetExpiresAt() *timestamppb.Timestamp { if x != nil { return x.ExpiresAt } return nil } func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetCreatedAt() *timestamppb.Timestamp { if x != nil { return x.CreatedAt } return nil } func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetLastUsedAt() *timestamppb.Timestamp { if x != nil { return x.LastUsedAt } return nil } 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\"\xcb\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\x12A\n" + "\tshortcuts\x18\x06 \x01(\v2!.memos.store.ShortcutsUserSettingH\x00R\tshortcuts\x12>\n" + "\bwebhooks\x18\a \x01(\v2 .memos.store.WebhooksUserSettingH\x00R\bwebhooks\x12N\n" + "\x0erefresh_tokens\x18\b \x01(\v2%.memos.store.RefreshTokensUserSettingH\x00R\rrefreshTokens\x12d\n" + "\x16personal_access_tokens\x18\t \x01(\v2,.memos.store.PersonalAccessTokensUserSettingH\x00R\x14personalAccessTokens\"t\n" + "\x03Key\x12\x13\n" + "\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" + "\aGENERAL\x10\x01\x12\r\n" + "\tSHORTCUTS\x10\x04\x12\f\n" + "\bWEBHOOKS\x10\x05\x12\x12\n" + "\x0eREFRESH_TOKENS\x10\x06\x12\x1a\n" + "\x16PERSONAL_ACCESS_TOKENS\x10\aB\a\n" + "\x05value\"k\n" + "\x12GeneralUserSetting\x12\x16\n" + "\x06locale\x18\x01 \x01(\tR\x06locale\x12'\n" + "\x0fmemo_visibility\x18\x02 \x01(\tR\x0ememoVisibility\x12\x14\n" + "\x05theme\x18\x03 \x01(\tR\x05theme\"\xa4\x04\n" + "\x18RefreshTokensUserSetting\x12Y\n" + "\x0erefresh_tokens\x18\x01 \x03(\v22.memos.store.RefreshTokensUserSetting.RefreshTokenR\rrefreshTokens\x1a\x94\x02\n" + "\fRefreshToken\x12\x19\n" + "\btoken_id\x18\x01 \x01(\tR\atokenId\x129\n" + "\n" + "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x129\n" + "\n" + "created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12Q\n" + "\vclient_info\x18\x04 \x01(\v20.memos.store.RefreshTokensUserSetting.ClientInfoR\n" + "clientInfo\x12 \n" + "\vdescription\x18\x05 \x01(\tR\vdescription\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\"\xa3\x03\n" + "\x1fPersonalAccessTokensUserSetting\x12X\n" + "\x06tokens\x18\x01 \x03(\v2@.memos.store.PersonalAccessTokensUserSetting.PersonalAccessTokenR\x06tokens\x1a\xa5\x02\n" + "\x13PersonalAccessToken\x12\x19\n" + "\btoken_id\x18\x01 \x01(\tR\atokenId\x12\x1d\n" + "\n" + "token_hash\x18\x02 \x01(\tR\ttokenHash\x12 \n" + "\vdescription\x18\x03 \x01(\tR\vdescription\x129\n" + "\n" + "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x129\n" + "\n" + "created_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12<\n" + "\flast_used_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" + "lastUsedAt\"\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 (*RefreshTokensUserSetting)(nil), // 3: memos.store.RefreshTokensUserSetting (*PersonalAccessTokensUserSetting)(nil), // 4: memos.store.PersonalAccessTokensUserSetting (*ShortcutsUserSetting)(nil), // 5: memos.store.ShortcutsUserSetting (*WebhooksUserSetting)(nil), // 6: memos.store.WebhooksUserSetting (*RefreshTokensUserSetting_RefreshToken)(nil), // 7: memos.store.RefreshTokensUserSetting.RefreshToken (*RefreshTokensUserSetting_ClientInfo)(nil), // 8: memos.store.RefreshTokensUserSetting.ClientInfo (*PersonalAccessTokensUserSetting_PersonalAccessToken)(nil), // 9: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken (*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 5, // 2: memos.store.UserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting 6, // 3: memos.store.UserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting 3, // 4: memos.store.UserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting 4, // 5: memos.store.UserSetting.personal_access_tokens:type_name -> memos.store.PersonalAccessTokensUserSetting 7, // 6: memos.store.RefreshTokensUserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting.RefreshToken 9, // 7: memos.store.PersonalAccessTokensUserSetting.tokens:type_name -> memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken 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.RefreshTokensUserSetting.RefreshToken.expires_at:type_name -> google.protobuf.Timestamp 12, // 11: memos.store.RefreshTokensUserSetting.RefreshToken.created_at:type_name -> google.protobuf.Timestamp 8, // 12: memos.store.RefreshTokensUserSetting.RefreshToken.client_info:type_name -> memos.store.RefreshTokensUserSetting.ClientInfo 12, // 13: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp 12, // 14: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp 12, // 15: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp 16, // [16:16] is the sub-list for method output_type 16, // [16:16] is the sub-list for method input_type 16, // [16:16] is the sub-list for extension type_name 16, // [16:16] is the sub-list for extension extendee 0, // [0:16] 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_Shortcuts)(nil), (*UserSetting_Webhooks)(nil), (*UserSetting_RefreshTokens)(nil), (*UserSetting_PersonalAccessTokens)(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 } ================================================ FILE: proto/store/attachment.proto ================================================ syntax = "proto3"; package memos.store; import "google/protobuf/timestamp.proto"; import "store/instance_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; } } ================================================ FILE: proto/store/idp.proto ================================================ 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; string uid = 6; } 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; } ================================================ FILE: proto/store/inbox.proto ================================================ syntax = "proto3"; package memos.store; option go_package = "gen/store"; message InboxMessage { message MemoCommentPayload { int32 memo_id = 1; int32 related_memo_id = 2; } // The type of the inbox message. Type type = 1; oneof payload { MemoCommentPayload memo_comment = 2; } enum Type { TYPE_UNSPECIFIED = 0; // Memo comment notification. MEMO_COMMENT = 1; } } ================================================ FILE: proto/store/instance_setting.proto ================================================ syntax = "proto3"; package memos.store; import "google/type/color.proto"; option go_package = "gen/store"; enum InstanceSettingKey { INSTANCE_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; // TAGS is the key for tag metadata. TAGS = 5; // NOTIFICATION is the key for notification transport settings. NOTIFICATION = 6; } message InstanceSetting { InstanceSettingKey key = 1; oneof value { InstanceBasicSetting basic_setting = 2; InstanceGeneralSetting general_setting = 3; InstanceStorageSetting storage_setting = 4; InstanceMemoRelatedSetting memo_related_setting = 5; InstanceTagsSetting tags_setting = 6; InstanceNotificationSetting notification_setting = 7; } } message InstanceBasicSetting { // The secret key for instance. Mainly used for session management. string secret_key = 1; // The current schema version of database. string schema_version = 2; } message InstanceGeneralSetting { // 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. InstanceCustomProfile 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 InstanceCustomProfile { string title = 1; string description = 2; string logo_url = 3; } message InstanceStorageSetting { 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 InstanceMemoRelatedSetting { // 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; // reactions is the list of reactions. repeated string reactions = 7; } message InstanceTagMetadata { // Background color for the tag label. google.type.Color background_color = 1; } message InstanceTagsSetting { map tags = 1; } message InstanceNotificationSetting { EmailSetting email = 1; message EmailSetting { bool enabled = 1; string smtp_host = 2; int32 smtp_port = 3; string smtp_username = 4; string smtp_password = 5; string from_email = 6; string from_name = 7; string reply_to = 8; bool use_tls = 9; bool use_ssl = 10; } } ================================================ FILE: proto/store/memo.proto ================================================ 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 title extracted from the first H1 heading, if present. string title = 5; } message Location { string placeholder = 1; double latitude = 2; double longitude = 3; } } ================================================ FILE: proto/store/user_setting.proto ================================================ 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; // The shortcuts of the user. SHORTCUTS = 4; // The webhooks of the user. WEBHOOKS = 5; // Refresh tokens for the user. REFRESH_TOKENS = 6; // Personal access tokens for the user. PERSONAL_ACCESS_TOKENS = 7; } int32 user_id = 1; Key key = 2; oneof value { GeneralUserSetting general = 3; ShortcutsUserSetting shortcuts = 6; WebhooksUserSetting webhooks = 7; RefreshTokensUserSetting refresh_tokens = 8; PersonalAccessTokensUserSetting personal_access_tokens = 9; } } message GeneralUserSetting { // The user's locale. string locale = 1; // The user's memo visibility setting. string memo_visibility = 2; // The user's theme preference. // This references a CSS file in the web/public/themes/ directory. string theme = 3; } message RefreshTokensUserSetting { message RefreshToken { // Unique identifier (matches 'tid' claim in JWT) string token_id = 1; // When the token expires google.protobuf.Timestamp expires_at = 2; // When the token was created google.protobuf.Timestamp created_at = 3; // Client information for session management UI ClientInfo client_info = 4; // Optional description string description = 5; } 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 RefreshToken refresh_tokens = 1; } message PersonalAccessTokensUserSetting { message PersonalAccessToken { // Unique identifier for this token string token_id = 1; // SHA-256 hash of the actual token string token_hash = 2; // User-provided description string description = 3; // When the token expires (null = never) google.protobuf.Timestamp expires_at = 4; // When the token was created google.protobuf.Timestamp created_at = 5; // When the token was last used google.protobuf.Timestamp last_used_at = 6; } repeated PersonalAccessToken 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; } ================================================ FILE: scripts/Dockerfile ================================================ FROM --platform=$BUILDPLATFORM golang:1.26.1-alpine AS backend WORKDIR /backend-build # Install build dependencies RUN apk add --no-cache git ca-certificates # Copy go mod files and download dependencies (cached layer) COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod \ go mod download # Copy source code (use .dockerignore to exclude unnecessary files) COPY . . # Please build frontend first, so that the static files are available. # Refer to `pnpm release` in package.json for the build command. ARG TARGETOS TARGETARCH VERSION COMMIT RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ go build \ -trimpath \ -ldflags="-s -w -extldflags '-static'" \ -tags netgo,osusergo \ -o memos \ ./cmd/memos # Use minimal Alpine with security updates FROM alpine:3.21 AS monolithic # Install runtime dependencies and create non-root user in single layer RUN apk add --no-cache tzdata ca-certificates su-exec && \ addgroup -g 10001 -S nonroot && \ adduser -u 10001 -S -G nonroot -h /var/opt/memos nonroot && \ mkdir -p /var/opt/memos /usr/local/memos && \ chown -R nonroot:nonroot /var/opt/memos # Copy binary and entrypoint to /usr/local/memos COPY --from=backend /backend-build/memos /usr/local/memos/memos COPY --from=backend --chmod=755 /backend-build/scripts/entrypoint.sh /usr/local/memos/entrypoint.sh # Run as root to fix permissions, entrypoint will drop to nonroot USER root # Set working directory to the writable volume WORKDIR /var/opt/memos # Data directory VOLUME /var/opt/memos ENV TZ="UTC" \ MEMOS_PORT="5230" EXPOSE 5230 ENTRYPOINT ["/usr/local/memos/entrypoint.sh", "/usr/local/memos/memos"] ================================================ FILE: scripts/build.sh ================================================ #!/bin/sh set -e # Change to repo root cd "$(dirname "$0")/../" OS=$(uname -s) # Determine output binary name case "$OS" in *CYGWIN*|*MINGW*|*MSYS*) OUTPUT="./build/memos.exe" ;; *) OUTPUT="./build/memos" ;; esac echo "Building for $OS..." # Ensure build directories exist and configure a writable Go build cache mkdir -p ./build/.gocache ./build/.gomodcache export GOCACHE="$(pwd)/build/.gocache" export GOMODCACHE="$(pwd)/build/.gomodcache" # Build the executable go build -o "$OUTPUT" ./cmd/memos echo "Build successful!" echo "To run the application, execute the following command:" echo "$OUTPUT" ================================================ FILE: scripts/compose.yaml ================================================ services: memos: image: neosmemo/memos:stable container_name: memos volumes: - ~/.memos/:/var/opt/memos ports: - 5230:5230 ================================================ FILE: scripts/entrypoint.sh ================================================ #!/usr/bin/env sh # Fix ownership of data directory for users upgrading from older versions # where files were created as root MEMOS_UID=${MEMOS_UID:-10001} MEMOS_GID=${MEMOS_GID:-10001} DATA_DIR="/var/opt/memos" if [ "$(id -u)" = "0" ]; then # Running as root, fix permissions and drop to nonroot if [ -d "$DATA_DIR" ]; then chown -R "$MEMOS_UID:$MEMOS_GID" "$DATA_DIR" 2>/dev/null || true fi exec su-exec "$MEMOS_UID:$MEMOS_GID" "$0" "$@" fi 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 if [ ! -r "$val_fileVar" ]; then echo "error: file '$val_fileVar' does not exist or is not readable" >&2 exit 1 fi val="$(cat "$val_fileVar")" fi export "$var"="$val" unset "$fileVar" } file_env "MEMOS_DSN" exec "$@" ================================================ FILE: scripts/entrypoint_test.sh ================================================ #!/usr/bin/env sh # Test script for entrypoint.sh file_env function # Run: ./scripts/entrypoint_test.sh set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" TEMP_DIR=$(mktemp -d) trap "rm -rf $TEMP_DIR" EXIT # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' # No Color pass_count=0 fail_count=0 pass() { echo "${GREEN}PASS${NC}: $1" pass_count=$((pass_count + 1)) } fail() { echo "${RED}FAIL${NC}: $1" fail_count=$((fail_count + 1)) } # Test 1: Direct env var works test_direct_env_var() { unset MEMOS_DSN MEMOS_DSN_FILE export MEMOS_DSN="direct_value" result=$("$SCRIPT_DIR/entrypoint.sh" sh -c 'echo $MEMOS_DSN' 2>&1) if [ "$result" = "direct_value" ]; then pass "Direct env var works" else fail "Direct env var: expected 'direct_value', got '$result'" fi unset MEMOS_DSN } # Test 2: File env var works with readable file test_file_env_var_readable() { unset MEMOS_DSN MEMOS_DSN_FILE echo "file_value" > "$TEMP_DIR/dsn_file" export MEMOS_DSN_FILE="$TEMP_DIR/dsn_file" result=$("$SCRIPT_DIR/entrypoint.sh" sh -c 'echo $MEMOS_DSN' 2>&1) if [ "$result" = "file_value" ]; then pass "File env var with readable file works" else fail "File env var readable: expected 'file_value', got '$result'" fi unset MEMOS_DSN_FILE } # Test 3: Error when file doesn't exist test_file_env_var_missing() { unset MEMOS_DSN MEMOS_DSN_FILE export MEMOS_DSN_FILE="$TEMP_DIR/nonexistent_file" if result=$("$SCRIPT_DIR/entrypoint.sh" sh -c 'echo $MEMOS_DSN' 2>&1); then fail "Missing file should fail, but succeeded with: $result" else if echo "$result" | grep -q "does not exist or is not readable"; then pass "Missing file returns error" else fail "Missing file error message unexpected: $result" fi fi unset MEMOS_DSN_FILE } # Test 4: Error when file is not readable test_file_env_var_unreadable() { unset MEMOS_DSN MEMOS_DSN_FILE echo "secret" > "$TEMP_DIR/unreadable_file" chmod 000 "$TEMP_DIR/unreadable_file" export MEMOS_DSN_FILE="$TEMP_DIR/unreadable_file" if result=$("$SCRIPT_DIR/entrypoint.sh" sh -c 'echo $MEMOS_DSN' 2>&1); then fail "Unreadable file should fail, but succeeded with: $result" else if echo "$result" | grep -q "does not exist or is not readable"; then pass "Unreadable file returns error" else fail "Unreadable file error message unexpected: $result" fi fi chmod 644 "$TEMP_DIR/unreadable_file" 2>/dev/null || true unset MEMOS_DSN_FILE } # Test 5: Error when both var and file are set test_both_set_error() { unset MEMOS_DSN MEMOS_DSN_FILE echo "file_value" > "$TEMP_DIR/dsn_file" export MEMOS_DSN="direct_value" export MEMOS_DSN_FILE="$TEMP_DIR/dsn_file" if result=$("$SCRIPT_DIR/entrypoint.sh" sh -c 'echo $MEMOS_DSN' 2>&1); then fail "Both set should fail, but succeeded with: $result" else if echo "$result" | grep -q "are set (but are exclusive)"; then pass "Both var and file set returns error" else fail "Both set error message unexpected: $result" fi fi unset MEMOS_DSN MEMOS_DSN_FILE } # Run all tests echo "Running entrypoint.sh tests..." echo "================================" test_direct_env_var test_file_env_var_readable test_file_env_var_missing test_file_env_var_unreadable test_both_set_error echo "================================" echo "Tests completed: ${GREEN}$pass_count passed${NC}, ${RED}$fail_count failed${NC}" if [ $fail_count -gt 0 ]; then exit 1 fi exit 0 ================================================ FILE: scripts/install.sh ================================================ #!/bin/sh set -eu REPO="${REPO:-usememos/memos}" BIN_NAME="memos" VERSION="${MEMOS_VERSION:-}" INSTALL_DIR="${MEMOS_INSTALL_DIR:-}" SKIP_CHECKSUM="${MEMOS_SKIP_CHECKSUM:-0}" QUIET="${MEMOS_INSTALL_QUIET:-0}" usage() { cat <<'EOF' Install Memos from GitHub Releases. Usage: install.sh [--version ] [--install-dir ] [--repo ] [--skip-checksum] Environment: MEMOS_VERSION Version to install. Accepts "0.28.1" or "v0.28.1". Defaults to latest release. MEMOS_INSTALL_DIR Directory to install the binary into. MEMOS_SKIP_CHECKSUM Set to 1 to skip checksum verification. MEMOS_INSTALL_QUIET Set to 1 to reduce log output. REPO GitHub repository in owner/name form. Defaults to usememos/memos. Examples: curl -fsSL https://raw.githubusercontent.com/usememos/memos/main/scripts/install.sh | sh curl -fsSL https://raw.githubusercontent.com/usememos/memos/main/scripts/install.sh | sh -s -- --version 0.28.1 EOF } log() { if [ "$QUIET" = "1" ]; then return fi printf '%s\n' "$*" } fail() { printf 'Error: %s\n' "$*" >&2 exit 1 } need_cmd() { command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1" } resolve_latest_version() { latest_tag="$( curl -fsSL \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${REPO}/releases/latest" | awk -F'"' '/"tag_name":/ { print $4; exit }' )" [ -n "$latest_tag" ] || fail "failed to resolve latest release tag" printf '%s\n' "${latest_tag#v}" } normalize_version() { version="$1" version="${version#v}" [ -n "$version" ] || fail "version cannot be empty" printf '%s\n' "$version" } detect_os() { os="$(uname -s | tr '[:upper:]' '[:lower:]')" case "$os" in linux) printf 'linux\n' ;; darwin) printf 'darwin\n' ;; *) fail "unsupported operating system: $os" ;; esac } detect_arch() { arch="$(uname -m)" case "$arch" in x86_64|amd64) printf 'amd64\n' ;; arm64|aarch64) printf 'arm64\n' ;; armv7l|armv7) printf 'armv7\n' ;; *) fail "unsupported architecture: $arch" ;; esac } resolve_install_dir() { if [ -n "$INSTALL_DIR" ]; then printf '%s\n' "$INSTALL_DIR" return fi if [ -w "/usr/local/bin" ]; then printf '/usr/local/bin\n' return fi if command -v sudo >/dev/null 2>&1; then printf '/usr/local/bin\n' return fi printf '%s/.local/bin\n' "$HOME" } download() { src="$1" dest="$2" if ! curl -fsSL "$src" -o "$dest"; then fail "failed to download ${src}" fi } download_optional() { src="$1" dest="$2" if curl -fsSL "$src" -o "$dest" 2>/dev/null; then return 0 fi rm -f "$dest" return 1 } verify_checksum() { archive_path="$1" checksum_path="$2" if [ "$SKIP_CHECKSUM" = "1" ]; then log "Skipping checksum verification" return fi if [ ! -f "$checksum_path" ]; then log "Warning: checksum file not found for this release; skipping verification" return fi archive_name="$(basename "$archive_path")" expected_line="$(grep " ${archive_name}\$" "$checksum_path" || true)" [ -n "$expected_line" ] || fail "checksum entry not found for ${archive_name}" if command -v sha256sum >/dev/null 2>&1; then ( cd "$(dirname "$archive_path")" printf '%s\n' "$expected_line" | sha256sum -c - ) return fi if command -v shasum >/dev/null 2>&1; then expected_sum="$(printf '%s' "$expected_line" | awk '{print $1}')" actual_sum="$(shasum -a 256 "$archive_path" | awk '{print $1}')" [ "$expected_sum" = "$actual_sum" ] || fail "checksum verification failed for ${archive_name}" return fi log "Warning: sha256sum/shasum not found; skipping checksum verification" } extract_archive() { archive_path="$1" dest_dir="$2" tar -xzf "$archive_path" -C "$dest_dir" } install_binary() { src="$1" dest_dir="$2" mkdir -p "$dest_dir" if [ -w "$dest_dir" ]; then install -m 755 "$src" "${dest_dir}/${BIN_NAME}" return fi if command -v sudo >/dev/null 2>&1; then sudo mkdir -p "$dest_dir" sudo install -m 755 "$src" "${dest_dir}/${BIN_NAME}" return fi fail "install directory is not writable: $dest_dir" } parse_args() { while [ "$#" -gt 0 ]; do case "$1" in --version) [ "$#" -ge 2 ] || fail "missing value for --version" VERSION="$2" shift 2 ;; --install-dir) [ "$#" -ge 2 ] || fail "missing value for --install-dir" INSTALL_DIR="$2" shift 2 ;; --repo) [ "$#" -ge 2 ] || fail "missing value for --repo" REPO="$2" shift 2 ;; --skip-checksum) SKIP_CHECKSUM="1" shift ;; --quiet) QUIET="1" shift ;; -h|--help) usage exit 0 ;; *) fail "unknown argument: $1" ;; esac done } main() { parse_args "$@" need_cmd curl need_cmd tar need_cmd install need_cmd uname need_cmd grep need_cmd awk need_cmd mktemp os="$(detect_os)" arch="$(detect_arch)" if [ -z "$VERSION" ]; then VERSION="$(resolve_latest_version)" fi VERSION="$(normalize_version "$VERSION")" install_dir="$(resolve_install_dir)" tag="v${VERSION}" asset_suffix="${arch}" if [ "$arch" = "armv7" ]; then asset_suffix="armv7" fi asset_name="${BIN_NAME}_${VERSION}_${os}_${asset_suffix}.tar.gz" checksums_name="checksums.txt" base_url="https://github.com/${REPO}/releases/download/${tag}" tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' EXIT INT TERM archive_path="${tmpdir}/${asset_name}" checksums_path="${tmpdir}/${checksums_name}" extract_dir="${tmpdir}/extract" mkdir -p "$extract_dir" log "Installing ${BIN_NAME} ${VERSION} for ${os}/${arch}" log "Downloading ${asset_name} from ${REPO}" download "${base_url}/${asset_name}" "$archive_path" if ! download_optional "${base_url}/${checksums_name}" "$checksums_path"; then log "Warning: ${checksums_name} is not published for ${tag}" fi verify_checksum "$archive_path" "$checksums_path" extract_archive "$archive_path" "$extract_dir" [ -f "${extract_dir}/${BIN_NAME}" ] || fail "archive did not contain ${BIN_NAME}" install_binary "${extract_dir}/${BIN_NAME}" "$install_dir" log "Installed ${BIN_NAME} to ${install_dir}/${BIN_NAME}" if ! printf '%s' ":$PATH:" | grep -q ":${install_dir}:"; then log "Add ${install_dir} to your PATH to run ${BIN_NAME} directly" fi } main "$@" ================================================ FILE: server/auth/authenticator.go ================================================ package auth import ( "context" "log/slog" "strings" "time" "github.com/pkg/errors" "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" ) // Authenticator provides shared authentication and authorization logic. // Used by gRPC interceptor, Connect interceptor, and file server to ensure // consistent authentication behavior across all API endpoints. // // Authentication methods: // - JWT access tokens: Short-lived tokens (15 minutes) for API access // - Personal Access Tokens (PAT): Long-lived tokens for programmatic access // // This struct is safe for concurrent use. type Authenticator struct { store *store.Store secret string } // NewAuthenticator creates a new Authenticator instance. func NewAuthenticator(store *store.Store, secret string) *Authenticator { return &Authenticator{ store: store, secret: secret, } } // AuthenticateByAccessTokenV2 validates a short-lived access token. // Returns claims without database query (stateless validation). func (a *Authenticator) AuthenticateByAccessTokenV2(accessToken string) (*UserClaims, error) { claims, err := ParseAccessTokenV2(accessToken, []byte(a.secret)) if err != nil { return nil, errors.Wrap(err, "invalid access token") } userID, err := util.ConvertStringToInt32(claims.Subject) if err != nil { return nil, errors.Wrap(err, "invalid user ID in token") } return &UserClaims{ UserID: userID, Username: claims.Username, Role: claims.Role, Status: claims.Status, }, nil } // AuthenticateByRefreshToken validates a refresh token against the database. func (a *Authenticator) AuthenticateByRefreshToken(ctx context.Context, refreshToken string) (*store.User, string, error) { claims, err := ParseRefreshToken(refreshToken, []byte(a.secret)) if err != nil { return nil, "", errors.Wrap(err, "invalid refresh token") } userID, err := util.ConvertStringToInt32(claims.Subject) if err != nil { return nil, "", errors.Wrap(err, "invalid user ID in token") } // Check token exists in database (revocation check) token, err := a.store.GetUserRefreshTokenByID(ctx, userID, claims.TokenID) if err != nil { return nil, "", errors.Wrap(err, "failed to get refresh token") } if token == nil { return nil, "", errors.New("refresh token revoked") } // Check token not expired if token.ExpiresAt != nil && token.ExpiresAt.AsTime().Before(time.Now()) { return nil, "", errors.New("refresh token expired") } // Get user user, err := a.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.New("user not found") } if user.RowStatus == store.Archived { return nil, "", errors.New("user is archived") } return user, claims.TokenID, nil } // AuthenticateByPAT validates a Personal Access Token. func (a *Authenticator) AuthenticateByPAT(ctx context.Context, token string) (*store.User, *storepb.PersonalAccessTokensUserSetting_PersonalAccessToken, error) { if !strings.HasPrefix(token, PersonalAccessTokenPrefix) { return nil, nil, errors.New("invalid PAT format") } tokenHash := HashPersonalAccessToken(token) result, err := a.store.GetUserByPATHash(ctx, tokenHash) if err != nil { return nil, nil, errors.Wrap(err, "invalid PAT") } // Check expiry if result.PAT.ExpiresAt != nil && result.PAT.ExpiresAt.AsTime().Before(time.Now()) { return nil, nil, errors.New("PAT expired") } // Check user status if result.User.RowStatus == store.Archived { return nil, nil, errors.New("user is archived") } return result.User, result.PAT, nil } // AuthResult contains the result of an authentication attempt. type AuthResult struct { User *store.User // Set for PAT authentication Claims *UserClaims // Set for Access Token V2 (stateless) AccessToken string // Non-empty if authenticated via JWT } // AuthenticateToUser resolves the current request to a *store.User, checking the // Authorization header first (access token or PAT), then falling back to the // refresh token cookie. Returns (nil, nil) when no credentials are present. func (a *Authenticator) AuthenticateToUser(ctx context.Context, authHeader, cookieHeader string) (*store.User, error) { // Try Bearer token first. if authHeader != "" { token := ExtractBearerToken(authHeader) if token != "" { if !strings.HasPrefix(token, PersonalAccessTokenPrefix) { claims, err := a.AuthenticateByAccessTokenV2(token) if err == nil && claims != nil { return a.store.GetUser(ctx, &store.FindUser{ID: &claims.UserID}) } } else { user, _, err := a.AuthenticateByPAT(ctx, token) if err == nil { return user, nil } } } } // Fallback: refresh token cookie. if cookieHeader != "" { refreshToken := ExtractRefreshTokenFromCookie(cookieHeader) if refreshToken != "" { user, _, err := a.AuthenticateByRefreshToken(ctx, refreshToken) return user, err } } return nil, nil } // Authenticate tries to authenticate using the provided credentials. // Priority: 1. Access Token V2, 2. PAT // Returns nil if no valid credentials are provided. func (a *Authenticator) Authenticate(ctx context.Context, authHeader string) *AuthResult { token := ExtractBearerToken(authHeader) // Try Access Token V2 (stateless) if token != "" && !strings.HasPrefix(token, PersonalAccessTokenPrefix) { claims, err := a.AuthenticateByAccessTokenV2(token) if err == nil && claims != nil { return &AuthResult{ Claims: claims, AccessToken: token, } } } // Try PAT if token != "" && strings.HasPrefix(token, PersonalAccessTokenPrefix) { user, pat, err := a.AuthenticateByPAT(ctx, token) if err == nil && user != nil { // Update last used (fire-and-forget with logging) go func() { if err := a.store.UpdatePATLastUsed(context.Background(), user.ID, pat.TokenId, timestamppb.Now()); err != nil { slog.Warn("failed to update PAT last used time", "error", err, "userID", user.ID) } }() return &AuthResult{User: user, AccessToken: token} } } return nil } ================================================ FILE: server/auth/context.go ================================================ package auth import ( "context" "github.com/usememos/memos/store" ) // ContextKey is the key type for context values. // Using a custom type prevents collisions with other packages. type ContextKey int const ( // UserIDContextKey stores the authenticated user's ID. // Set for all authenticated requests. // Use GetUserID(ctx) to retrieve this value. UserIDContextKey ContextKey = iota // AccessTokenContextKey stores the JWT token for token-based auth. // Only set when authenticated via Bearer token. AccessTokenContextKey // UserClaimsContextKey stores the claims from access token. UserClaimsContextKey // RefreshTokenIDContextKey stores the refresh token ID. RefreshTokenIDContextKey ) // GetUserID retrieves the authenticated user's ID from the context. // Returns 0 if no user ID is set (unauthenticated request). func GetUserID(ctx context.Context) int32 { if v, ok := ctx.Value(UserIDContextKey).(int32); ok { return v } return 0 } // GetAccessToken retrieves the JWT access token from the context. // Returns empty string if not authenticated via bearer token. func GetAccessToken(ctx context.Context) string { if v, ok := ctx.Value(AccessTokenContextKey).(string); ok { return v } return "" } // SetUserInContext sets the authenticated user's information in the context. // This is a simpler alternative to AuthorizeAndSetContext for cases where // authorization is handled separately (e.g., HTTP middleware). // // Parameters: // - user: The authenticated user // - accessToken: Set if authenticated via JWT token (empty string otherwise) func SetUserInContext(ctx context.Context, user *store.User, accessToken string) context.Context { ctx = context.WithValue(ctx, UserIDContextKey, user.ID) if accessToken != "" { ctx = context.WithValue(ctx, AccessTokenContextKey, accessToken) } return ctx } // UserClaims represents authenticated user info from access token. type UserClaims struct { UserID int32 Username string Role string Status string } // GetUserClaims retrieves the user claims from context. // Returns nil if not authenticated via access token. func GetUserClaims(ctx context.Context) *UserClaims { if v, ok := ctx.Value(UserClaimsContextKey).(*UserClaims); ok { return v } return nil } // SetUserClaimsInContext sets the user claims in context. func SetUserClaimsInContext(ctx context.Context, claims *UserClaims) context.Context { return context.WithValue(ctx, UserClaimsContextKey, claims) } // ApplyToContext sets the authenticated identity from an AuthResult into the context. // This is the canonical way to propagate auth state after a successful Authenticate call. // Safe to call with a nil result (no-op). func ApplyToContext(ctx context.Context, result *AuthResult) context.Context { if result == nil { return ctx } if result.Claims != nil { ctx = SetUserClaimsInContext(ctx, result.Claims) ctx = context.WithValue(ctx, UserIDContextKey, result.Claims.UserID) } else if result.User != nil { ctx = SetUserInContext(ctx, result.User, result.AccessToken) } return ctx } ================================================ FILE: server/auth/extract.go ================================================ package auth import ( "net/http" "strings" ) // ExtractBearerToken extracts the JWT token from an Authorization header value. // Expected format: "Bearer {token}" // Returns empty string if no valid bearer token is found. func ExtractBearerToken(authHeader string) string { if authHeader == "" { return "" } parts := strings.Fields(authHeader) if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") { return "" } return parts[1] } // ExtractRefreshTokenFromCookie extracts the refresh token from cookie header. func ExtractRefreshTokenFromCookie(cookieHeader string) string { if cookieHeader == "" { return "" } req := &http.Request{Header: http.Header{"Cookie": []string{cookieHeader}}} cookie, err := req.Cookie(RefreshTokenCookieName) if err != nil { return "" } return cookie.Value } ================================================ FILE: server/auth/token.go ================================================ // Package auth provides authentication and authorization for the Memos server. // // This package is used by: // - server/router/api/v1: gRPC and Connect API interceptors // - server/router/fileserver: HTTP file server authentication // // Authentication methods supported: // - JWT access tokens: Short-lived tokens (15 minutes) for API access // - JWT refresh tokens: Long-lived tokens (30 days) for obtaining new access tokens // - Personal Access Tokens (PAT): Long-lived tokens for programmatic access package auth import ( "crypto/sha256" "encoding/hex" "fmt" "time" "github.com/golang-jwt/jwt/v5" "github.com/pkg/errors" "github.com/usememos/memos/internal/util" ) const ( // Issuer is the issuer claim in JWT tokens. // This identifies tokens as issued by Memos. Issuer = "memos" // KeyID is the key identifier used in JWT header. // Version "v1" allows for future key rotation while maintaining backward compatibility. // If signing mechanism changes, add "v2", "v3", etc. and verify both versions. KeyID = "v1" // AccessTokenAudienceName is the audience claim for JWT access tokens. // This ensures tokens are only used for API access, not other purposes. AccessTokenAudienceName = "user.access-token" // AccessTokenDuration is the lifetime of access tokens (15 minutes). AccessTokenDuration = 15 * time.Minute // RefreshTokenDuration is the lifetime of refresh tokens (30 days). RefreshTokenDuration = 30 * 24 * time.Hour // RefreshTokenAudienceName is the audience claim for refresh tokens. RefreshTokenAudienceName = "user.refresh-token" // RefreshTokenCookieName is the cookie name for refresh tokens. RefreshTokenCookieName = "memos_refresh" // PersonalAccessTokenPrefix is the prefix for PAT tokens. PersonalAccessTokenPrefix = "memos_pat_" ) // ClaimsMessage represents the claims structure in a JWT token. // // JWT Claims include: // - name: Username (custom claim) // - iss: Issuer = "memos" // - aud: Audience = "user.access-token" // - sub: Subject = user ID // - iat: Issued at time // - exp: Expiration time (optional, may be empty for never-expiring tokens). type ClaimsMessage struct { Name string `json:"name"` // Username jwt.RegisteredClaims } // AccessTokenClaims contains claims for short-lived access tokens. // These tokens are validated by signature only (stateless). type AccessTokenClaims struct { Type string `json:"type"` // "access" Role string `json:"role"` // User role Status string `json:"status"` // User status Username string `json:"username"` // Username for display jwt.RegisteredClaims } // RefreshTokenClaims contains claims for long-lived refresh tokens. // These tokens are validated against the database for revocation. type RefreshTokenClaims struct { Type string `json:"type"` // "refresh" TokenID string `json:"tid"` // Token ID for revocation lookup jwt.RegisteredClaims } // GenerateAccessToken generates a JWT access token for a user. // // Parameters: // - username: The user's username (stored in "name" claim) // - userID: The user's ID (stored in "sub" claim) // - expirationTime: When the token expires (pass zero time for no expiration) // - secret: Server secret used to sign the token // // Returns a signed JWT string or an error. 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 with the given claims. // // Token structure: // Header: {"alg": "HS256", "kid": "v1", "typ": "JWT"} // Claims: {"name": username, "iss": "memos", "aud": [audience], "sub": userID, "iat": now, "exp": expiry} // Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret). 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 } // GenerateAccessTokenV2 generates a short-lived access token with user claims. func GenerateAccessTokenV2(userID int32, username, role, status string, secret []byte) (string, time.Time, error) { expiresAt := time.Now().Add(AccessTokenDuration) claims := &AccessTokenClaims{ Type: "access", Role: role, Status: status, Username: username, RegisteredClaims: jwt.RegisteredClaims{ Issuer: Issuer, Audience: jwt.ClaimStrings{AccessTokenAudienceName}, Subject: fmt.Sprint(userID), IssuedAt: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(expiresAt), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token.Header["kid"] = KeyID tokenString, err := token.SignedString(secret) if err != nil { return "", time.Time{}, err } return tokenString, expiresAt, nil } // GenerateRefreshToken generates a long-lived refresh token. func GenerateRefreshToken(userID int32, tokenID string, secret []byte) (string, time.Time, error) { expiresAt := time.Now().Add(RefreshTokenDuration) claims := &RefreshTokenClaims{ Type: "refresh", TokenID: tokenID, RegisteredClaims: jwt.RegisteredClaims{ Issuer: Issuer, Audience: jwt.ClaimStrings{RefreshTokenAudienceName}, Subject: fmt.Sprint(userID), IssuedAt: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(expiresAt), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token.Header["kid"] = KeyID tokenString, err := token.SignedString(secret) if err != nil { return "", time.Time{}, err } return tokenString, expiresAt, nil } // GeneratePersonalAccessToken generates a random PAT string. func GeneratePersonalAccessToken() string { randomStr, err := util.RandomString(32) if err != nil { // Fallback to UUID if RandomString fails return PersonalAccessTokenPrefix + util.GenUUID() } return PersonalAccessTokenPrefix + randomStr } // HashPersonalAccessToken returns SHA-256 hash of a PAT. func HashPersonalAccessToken(token string) string { hash := sha256.Sum256([]byte(token)) return hex.EncodeToString(hash[:]) } // verifyJWTKeyFunc returns a jwt.Keyfunc that validates the signing method and key ID. func verifyJWTKeyFunc(secret []byte) jwt.Keyfunc { return func(t *jwt.Token) (any, error) { if t.Method.Alg() != jwt.SigningMethodHS256.Name { return nil, errors.Errorf("unexpected signing method: %v", t.Header["alg"]) } kid, ok := t.Header["kid"].(string) if !ok || kid != KeyID { return nil, errors.Errorf("unexpected kid: %v", t.Header["kid"]) } return secret, nil } } // ParseAccessTokenV2 parses and validates a short-lived access token. func ParseAccessTokenV2(tokenString string, secret []byte) (*AccessTokenClaims, error) { claims := &AccessTokenClaims{} _, err := jwt.ParseWithClaims(tokenString, claims, verifyJWTKeyFunc(secret), jwt.WithIssuer(Issuer), jwt.WithAudience(AccessTokenAudienceName), ) if err != nil { return nil, err } if claims.Type != "access" { return nil, errors.New("invalid token type: expected access token") } return claims, nil } // ParseRefreshToken parses and validates a refresh token. func ParseRefreshToken(tokenString string, secret []byte) (*RefreshTokenClaims, error) { claims := &RefreshTokenClaims{} _, err := jwt.ParseWithClaims(tokenString, claims, verifyJWTKeyFunc(secret), jwt.WithIssuer(Issuer), jwt.WithAudience(RefreshTokenAudienceName), ) if err != nil { return nil, err } if claims.Type != "refresh" { return nil, errors.New("invalid token type: expected refresh token") } return claims, nil } ================================================ FILE: server/auth/token_test.go ================================================ package auth import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGenerateAccessTokenV2(t *testing.T) { secret := []byte("test-secret") t.Run("generates valid access token", func(t *testing.T) { token, expiresAt, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret) require.NoError(t, err) assert.NotEmpty(t, token) assert.True(t, expiresAt.After(time.Now())) assert.True(t, expiresAt.Before(time.Now().Add(AccessTokenDuration+time.Minute))) }) t.Run("generates different tokens for same user", func(t *testing.T) { token1, _, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret) require.NoError(t, err) time.Sleep(2 * time.Second) // Ensure different timestamps (tokens have 1s precision) token2, _, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret) require.NoError(t, err) assert.NotEqual(t, token1, token2, "tokens should be different due to different timestamps") }) } func TestParseAccessTokenV2(t *testing.T) { secret := []byte("test-secret") t.Run("parses valid access token", func(t *testing.T) { token, _, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret) require.NoError(t, err) claims, err := ParseAccessTokenV2(token, secret) require.NoError(t, err) assert.Equal(t, "1", claims.Subject) assert.Equal(t, "testuser", claims.Username) assert.Equal(t, "USER", claims.Role) assert.Equal(t, "ACTIVE", claims.Status) assert.Equal(t, "access", claims.Type) }) t.Run("fails with wrong secret", func(t *testing.T) { token, _, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret) require.NoError(t, err) wrongSecret := []byte("wrong-secret") _, err = ParseAccessTokenV2(token, wrongSecret) assert.Error(t, err) }) t.Run("fails with invalid token", func(t *testing.T) { _, err := ParseAccessTokenV2("invalid-token", secret) assert.Error(t, err) }) t.Run("fails with refresh token", func(t *testing.T) { // Generate a refresh token and try to parse it as access token // Should fail because audience mismatch is caught before type check refreshToken, _, err := GenerateRefreshToken(1, "token-id", secret) require.NoError(t, err) _, err = ParseAccessTokenV2(refreshToken, secret) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid audience") }) t.Run("parses token with different roles", func(t *testing.T) { roles := []string{"USER", "ADMIN"} for _, role := range roles { token, _, err := GenerateAccessTokenV2(1, "testuser", role, "ACTIVE", secret) require.NoError(t, err) claims, err := ParseAccessTokenV2(token, secret) require.NoError(t, err) assert.Equal(t, role, claims.Role) } }) } func TestGenerateRefreshToken(t *testing.T) { secret := []byte("test-secret") t.Run("generates valid refresh token", func(t *testing.T) { token, expiresAt, err := GenerateRefreshToken(1, "token-id-123", secret) require.NoError(t, err) assert.NotEmpty(t, token) assert.True(t, expiresAt.After(time.Now().Add(29*24*time.Hour))) }) t.Run("generates different tokens for different token IDs", func(t *testing.T) { token1, _, err := GenerateRefreshToken(1, "token-id-1", secret) require.NoError(t, err) token2, _, err := GenerateRefreshToken(1, "token-id-2", secret) require.NoError(t, err) assert.NotEqual(t, token1, token2) }) } func TestParseRefreshToken(t *testing.T) { secret := []byte("test-secret") t.Run("parses valid refresh token", func(t *testing.T) { token, _, err := GenerateRefreshToken(1, "token-id-123", secret) require.NoError(t, err) claims, err := ParseRefreshToken(token, secret) require.NoError(t, err) assert.Equal(t, "1", claims.Subject) assert.Equal(t, "token-id-123", claims.TokenID) assert.Equal(t, "refresh", claims.Type) }) t.Run("fails with wrong secret", func(t *testing.T) { token, _, err := GenerateRefreshToken(1, "token-id-123", secret) require.NoError(t, err) wrongSecret := []byte("wrong-secret") _, err = ParseRefreshToken(token, wrongSecret) assert.Error(t, err) }) t.Run("fails with invalid token", func(t *testing.T) { _, err := ParseRefreshToken("invalid-token", secret) assert.Error(t, err) }) t.Run("fails with access token", func(t *testing.T) { // Generate an access token and try to parse it as refresh token // Should fail because audience mismatch is caught before type check accessToken, _, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret) require.NoError(t, err) _, err = ParseRefreshToken(accessToken, secret) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid audience") }) } func TestGeneratePersonalAccessToken(t *testing.T) { t.Run("generates token with correct prefix", func(t *testing.T) { token := GeneratePersonalAccessToken() assert.NotEmpty(t, token) assert.True(t, len(token) > len(PersonalAccessTokenPrefix)) assert.Equal(t, PersonalAccessTokenPrefix, token[:len(PersonalAccessTokenPrefix)]) }) t.Run("generates unique tokens", func(t *testing.T) { token1 := GeneratePersonalAccessToken() token2 := GeneratePersonalAccessToken() assert.NotEqual(t, token1, token2) }) t.Run("generates token of sufficient length", func(t *testing.T) { token := GeneratePersonalAccessToken() // Prefix is "memos_pat_" (10 chars) + 32 random chars = at least 42 chars assert.True(t, len(token) >= 42, "token should be at least 42 characters") }) } func TestHashPersonalAccessToken(t *testing.T) { t.Run("generates SHA-256 hash", func(t *testing.T) { token := "memos_pat_abc123" hash := HashPersonalAccessToken(token) assert.NotEmpty(t, hash) assert.Len(t, hash, 64, "SHA-256 hex should be 64 characters") }) t.Run("same input produces same hash", func(t *testing.T) { token := "memos_pat_abc123" hash1 := HashPersonalAccessToken(token) hash2 := HashPersonalAccessToken(token) assert.Equal(t, hash1, hash2) }) t.Run("different inputs produce different hashes", func(t *testing.T) { token1 := "memos_pat_abc123" token2 := "memos_pat_xyz789" hash1 := HashPersonalAccessToken(token1) hash2 := HashPersonalAccessToken(token2) assert.NotEqual(t, hash1, hash2) }) t.Run("hash is deterministic", func(t *testing.T) { token := GeneratePersonalAccessToken() hash1 := HashPersonalAccessToken(token) hash2 := HashPersonalAccessToken(token) assert.Equal(t, hash1, hash2) }) } func TestAccessTokenV2Integration(t *testing.T) { secret := []byte("test-secret") t.Run("full lifecycle: generate, parse, validate", func(t *testing.T) { userID := int32(42) username := "john_doe" role := "ADMIN" status := "ACTIVE" // Generate token token, expiresAt, err := GenerateAccessTokenV2(userID, username, role, status, secret) require.NoError(t, err) assert.NotEmpty(t, token) // Parse token claims, err := ParseAccessTokenV2(token, secret) require.NoError(t, err) // Validate claims assert.Equal(t, "42", claims.Subject) assert.Equal(t, username, claims.Username) assert.Equal(t, role, claims.Role) assert.Equal(t, status, claims.Status) assert.Equal(t, "access", claims.Type) assert.Equal(t, Issuer, claims.Issuer) assert.NotNil(t, claims.IssuedAt) assert.NotNil(t, claims.ExpiresAt) // Validate expiration assert.True(t, claims.ExpiresAt.Equal(expiresAt) || claims.ExpiresAt.Before(expiresAt)) }) } func TestRefreshTokenIntegration(t *testing.T) { secret := []byte("test-secret") t.Run("full lifecycle: generate, parse, validate", func(t *testing.T) { userID := int32(42) tokenID := "unique-token-id-456" // Generate token token, expiresAt, err := GenerateRefreshToken(userID, tokenID, secret) require.NoError(t, err) assert.NotEmpty(t, token) // Parse token claims, err := ParseRefreshToken(token, secret) require.NoError(t, err) // Validate claims assert.Equal(t, "42", claims.Subject) assert.Equal(t, tokenID, claims.TokenID) assert.Equal(t, "refresh", claims.Type) assert.Equal(t, Issuer, claims.Issuer) assert.NotNil(t, claims.IssuedAt) assert.NotNil(t, claims.ExpiresAt) // Validate expiration assert.True(t, claims.ExpiresAt.Equal(expiresAt) || claims.ExpiresAt.Before(expiresAt)) }) } func TestPersonalAccessTokenIntegration(t *testing.T) { t.Run("full lifecycle: generate, hash, verify", func(t *testing.T) { // Generate token token := GeneratePersonalAccessToken() assert.NotEmpty(t, token) assert.True(t, len(token) > len(PersonalAccessTokenPrefix)) // Hash token hash := HashPersonalAccessToken(token) assert.Len(t, hash, 64) // Verify same token produces same hash hashAgain := HashPersonalAccessToken(token) assert.Equal(t, hash, hashAgain) // Verify different token produces different hash token2 := GeneratePersonalAccessToken() hash2 := HashPersonalAccessToken(token2) assert.NotEqual(t, hash, hash2) }) } func TestTokenExpiration(t *testing.T) { secret := []byte("test-secret") t.Run("access token expires after AccessTokenDuration", func(t *testing.T) { _, expiresAt, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret) require.NoError(t, err) expectedExpiry := time.Now().Add(AccessTokenDuration) delta := expiresAt.Sub(expectedExpiry) assert.True(t, delta < time.Second, "expiration should be within 1 second of expected") }) t.Run("refresh token expires after RefreshTokenDuration", func(t *testing.T) { _, expiresAt, err := GenerateRefreshToken(1, "token-id", secret) require.NoError(t, err) expectedExpiry := time.Now().Add(RefreshTokenDuration) delta := expiresAt.Sub(expectedExpiry) assert.True(t, delta < time.Second, "expiration should be within 1 second of expected") }) } ================================================ FILE: server/router/api/v1/acl_config.go ================================================ package v1 // PublicMethods defines API endpoints that don't require authentication. // All other endpoints require a valid session or access token. // // This is the SINGLE SOURCE OF TRUTH for public endpoints. // Both Connect interceptor and gRPC-Gateway interceptor use this map. // // Format: Full gRPC procedure path as returned by req.Spec().Procedure (Connect) // or info.FullMethod (gRPC interceptor). var PublicMethods = map[string]struct{}{ // Auth Service - login/token endpoints must be accessible without auth "/memos.api.v1.AuthService/SignIn": {}, "/memos.api.v1.AuthService/RefreshToken": {}, // Token refresh uses cookie, must be accessible when access token expired // Instance Service - needed before login to show instance info "/memos.api.v1.InstanceService/GetInstanceProfile": {}, "/memos.api.v1.InstanceService/GetInstanceSetting": {}, // User Service - public user profiles and stats "/memos.api.v1.UserService/CreateUser": {}, // Allow first user registration "/memos.api.v1.UserService/GetUser": {}, "/memos.api.v1.UserService/GetUserAvatar": {}, "/memos.api.v1.UserService/GetUserStats": {}, "/memos.api.v1.UserService/ListAllUserStats": {}, "/memos.api.v1.UserService/SearchUsers": {}, // Identity Provider Service - SSO buttons on login page "/memos.api.v1.IdentityProviderService/ListIdentityProviders": {}, // Memo Service - public memos (visibility filtering done in service layer) "/memos.api.v1.MemoService/GetMemo": {}, "/memos.api.v1.MemoService/ListMemos": {}, "/memos.api.v1.MemoService/ListMemoComments": {}, // Memo sharing - share-token endpoints require no authentication "/memos.api.v1.MemoService/GetMemoByShare": {}, } // IsPublicMethod checks if a procedure path is public (no authentication required). // Returns true for public methods, false for protected methods. func IsPublicMethod(procedure string) bool { _, ok := PublicMethods[procedure] return ok } ================================================ FILE: server/router/api/v1/acl_config_test.go ================================================ package v1 import ( "testing" "github.com/stretchr/testify/assert" ) // TestPublicMethodsArePublic verifies that methods in PublicMethods are recognized as public. func TestPublicMethodsArePublic(t *testing.T) { publicMethods := []string{ // Auth Service "/memos.api.v1.AuthService/SignIn", "/memos.api.v1.AuthService/RefreshToken", // Instance Service "/memos.api.v1.InstanceService/GetInstanceProfile", "/memos.api.v1.InstanceService/GetInstanceSetting", // User Service "/memos.api.v1.UserService/CreateUser", "/memos.api.v1.UserService/GetUser", "/memos.api.v1.UserService/GetUserAvatar", "/memos.api.v1.UserService/GetUserStats", "/memos.api.v1.UserService/ListAllUserStats", "/memos.api.v1.UserService/SearchUsers", // Identity Provider Service "/memos.api.v1.IdentityProviderService/ListIdentityProviders", // Memo Service "/memos.api.v1.MemoService/GetMemo", "/memos.api.v1.MemoService/ListMemos", } for _, method := range publicMethods { t.Run(method, func(t *testing.T) { assert.True(t, IsPublicMethod(method), "Expected %s to be public", method) }) } } // TestProtectedMethodsRequireAuth verifies that non-public methods are recognized as protected. func TestProtectedMethodsRequireAuth(t *testing.T) { protectedMethods := []string{ // Auth Service - logout and get current user require auth "/memos.api.v1.AuthService/SignOut", "/memos.api.v1.AuthService/GetCurrentUser", // Instance Service - admin operations "/memos.api.v1.InstanceService/UpdateInstanceSetting", // User Service - modification operations "/memos.api.v1.UserService/ListUsers", "/memos.api.v1.UserService/UpdateUser", "/memos.api.v1.UserService/DeleteUser", // Memo Service - write operations "/memos.api.v1.MemoService/CreateMemo", "/memos.api.v1.MemoService/UpdateMemo", "/memos.api.v1.MemoService/DeleteMemo", // Attachment Service - write operations "/memos.api.v1.AttachmentService/CreateAttachment", "/memos.api.v1.AttachmentService/DeleteAttachment", // Shortcut Service "/memos.api.v1.ShortcutService/CreateShortcut", "/memos.api.v1.ShortcutService/ListShortcuts", "/memos.api.v1.ShortcutService/UpdateShortcut", "/memos.api.v1.ShortcutService/DeleteShortcut", } for _, method := range protectedMethods { t.Run(method, func(t *testing.T) { assert.False(t, IsPublicMethod(method), "Expected %s to require auth", method) }) } } // TestUnknownMethodsRequireAuth verifies that unknown methods default to requiring auth. func TestUnknownMethodsRequireAuth(t *testing.T) { unknownMethods := []string{ "/unknown.Service/Method", "/memos.api.v1.UnknownService/Method", "", "invalid", } for _, method := range unknownMethods { t.Run(method, func(t *testing.T) { assert.False(t, IsPublicMethod(method), "Unknown method %q should require auth", method) }) } } ================================================ FILE: server/router/api/v1/attachment_exif_test.go ================================================ package v1 import ( "bytes" "image" "image/color" "image/jpeg" "testing" "github.com/disintegration/imaging" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestShouldStripExif(t *testing.T) { t.Parallel() tests := []struct { name string mimeType string expected bool }{ { name: "JPEG should strip EXIF", mimeType: "image/jpeg", expected: true, }, { name: "JPG should strip EXIF", mimeType: "image/jpg", expected: true, }, { name: "TIFF should strip EXIF", mimeType: "image/tiff", expected: true, }, { name: "WebP should strip EXIF", mimeType: "image/webp", expected: true, }, { name: "HEIC should strip EXIF", mimeType: "image/heic", expected: true, }, { name: "HEIF should strip EXIF", mimeType: "image/heif", expected: true, }, { name: "PNG should not strip EXIF", mimeType: "image/png", expected: false, }, { name: "GIF should not strip EXIF", mimeType: "image/gif", expected: false, }, { name: "text file should not strip EXIF", mimeType: "text/plain", expected: false, }, { name: "PDF should not strip EXIF", mimeType: "application/pdf", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() result := shouldStripExif(tt.mimeType) assert.Equal(t, tt.expected, result) }) } } func TestStripImageExif(t *testing.T) { t.Parallel() // Create a simple test image img := image.NewRGBA(image.Rect(0, 0, 100, 100)) // Fill with red color for y := 0; y < 100; y++ { for x := 0; x < 100; x++ { img.Set(x, y, color.RGBA{R: 255, G: 0, B: 0, A: 255}) } } // Encode as JPEG var buf bytes.Buffer err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}) require.NoError(t, err) originalData := buf.Bytes() t.Run("strip JPEG metadata", func(t *testing.T) { t.Parallel() strippedData, err := stripImageExif(originalData, "image/jpeg") require.NoError(t, err) assert.NotEmpty(t, strippedData) // Verify it's still a valid image decodedImg, err := imaging.Decode(bytes.NewReader(strippedData)) require.NoError(t, err) assert.Equal(t, 100, decodedImg.Bounds().Dx()) assert.Equal(t, 100, decodedImg.Bounds().Dy()) }) t.Run("strip JPG metadata (alternate extension)", func(t *testing.T) { t.Parallel() strippedData, err := stripImageExif(originalData, "image/jpg") require.NoError(t, err) assert.NotEmpty(t, strippedData) // Verify it's still a valid image decodedImg, err := imaging.Decode(bytes.NewReader(strippedData)) require.NoError(t, err) assert.NotNil(t, decodedImg) }) t.Run("strip PNG metadata", func(t *testing.T) { t.Parallel() // Encode as PNG first var pngBuf bytes.Buffer err := imaging.Encode(&pngBuf, img, imaging.PNG) require.NoError(t, err) strippedData, err := stripImageExif(pngBuf.Bytes(), "image/png") require.NoError(t, err) assert.NotEmpty(t, strippedData) // Verify it's still a valid image decodedImg, err := imaging.Decode(bytes.NewReader(strippedData)) require.NoError(t, err) assert.Equal(t, 100, decodedImg.Bounds().Dx()) assert.Equal(t, 100, decodedImg.Bounds().Dy()) }) t.Run("handle WebP format by converting to JPEG", func(t *testing.T) { t.Parallel() // WebP format will be converted to JPEG strippedData, err := stripImageExif(originalData, "image/webp") require.NoError(t, err) assert.NotEmpty(t, strippedData) // Verify it's a valid image decodedImg, err := imaging.Decode(bytes.NewReader(strippedData)) require.NoError(t, err) assert.NotNil(t, decodedImg) }) t.Run("handle HEIC format by converting to JPEG", func(t *testing.T) { t.Parallel() strippedData, err := stripImageExif(originalData, "image/heic") require.NoError(t, err) assert.NotEmpty(t, strippedData) // Verify it's a valid image decodedImg, err := imaging.Decode(bytes.NewReader(strippedData)) require.NoError(t, err) assert.NotNil(t, decodedImg) }) t.Run("return error for invalid image data", func(t *testing.T) { t.Parallel() invalidData := []byte("not an image") _, err := stripImageExif(invalidData, "image/jpeg") assert.Error(t, err) assert.Contains(t, err.Error(), "failed to decode image") }) t.Run("return error for empty image data", func(t *testing.T) { t.Parallel() emptyData := []byte{} _, err := stripImageExif(emptyData, "image/jpeg") assert.Error(t, err) }) } ================================================ FILE: server/router/api/v1/attachment_service.go ================================================ package v1 import ( "bytes" "context" "encoding/binary" "fmt" "io" "log/slog" "mime" "net/http" "os" "path/filepath" "regexp" "strings" "time" "github.com/disintegration/imaging" "github.com/pkg/errors" "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/profile" "github.com/usememos/memos/internal/util" "github.com/usememos/memos/plugin/filter" "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" // defaultJPEGQuality is the JPEG quality used when re-encoding images for EXIF stripping. // Quality 95 maintains visual quality while ensuring metadata is removed. defaultJPEGQuality = 95 ) var SupportedThumbnailMimeTypes = []string{ "image/png", "image/jpeg", } // exifCapableImageTypes defines image formats that may contain EXIF metadata. // These formats will have their EXIF metadata stripped on upload for privacy. var exifCapableImageTypes = map[string]bool{ "image/jpeg": true, "image/jpg": true, "image/tiff": true, "image/webp": true, "image/heic": true, "image/heif": true, } func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.CreateAttachmentRequest) (*v1pb.Attachment, error) { user, err := s.fetchCurrentUser(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, "user not authenticated") } // 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 !validateFilename(request.Attachment.Filename) { return nil, status.Errorf(codes.InvalidArgument, "filename contains invalid characters or format") } if request.Attachment.Type == "" { ext := filepath.Ext(request.Attachment.Filename) mimeType := mime.TypeByExtension(ext) if mimeType == "" { mimeType = http.DetectContentType(request.Attachment.Content) } // ParseMediaType to strip parameters mediaType, _, err := mime.ParseMediaType(mimeType) if err == nil { request.Attachment.Type = mediaType } } if request.Attachment.Type == "" { request.Attachment.Type = "application/octet-stream" } if !isValidMimeType(request.Attachment.Type) { return nil, status.Errorf(codes.InvalidArgument, "invalid MIME type format") } attachmentUID, err := ValidateAndGenerateUID(request.AttachmentId) if err != nil { return nil, err } create := &store.Attachment{ UID: attachmentUID, CreatorID: user.ID, Filename: request.Attachment.Filename, Type: request.Attachment.Type, } instanceStorageSetting, err := s.Store.GetInstanceStorageSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance storage setting: %v", err) } size := binary.Size(request.Attachment.Content) uploadSizeLimit := int(instanceStorageSetting.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 // Strip EXIF metadata from images for privacy protection. // This removes sensitive information like GPS location, device details, etc. if shouldStripExif(create.Type) { if strippedBlob, err := stripImageExif(create.Blob, create.Type); err != nil { // Log warning but continue with original image to ensure uploads don't fail. slog.Warn("failed to strip EXIF metadata from image", slog.String("type", create.Type), slog.String("filename", create.Filename), slog.String("error", err.Error())) } else { create.Blob = strippedBlob create.Size = int64(len(strippedBlob)) } } 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 convertAttachmentFromStore(attachment), nil } func (s *APIV1Service) ListAttachments(ctx context.Context, request *v1pb.ListAttachmentsRequest) (*v1pb.ListAttachmentsResponse, error) { user, err := s.fetchCurrentUser(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, "user not authenticated") } // 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, } // Parse filter if provided if request.Filter != "" { if err := s.validateAttachmentFilter(ctx, request.Filter); err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) } findAttachment.Filters = append(findAttachment.Filters, request.Filter) } attachments, err := s.Store.ListAttachments(ctx, findAttachment) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list attachments: %v", err) } response := &v1pb.ListAttachmentsResponse{} for _, attachment := range attachments { response.Attachments = append(response.Attachments, convertAttachmentFromStore(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") } // Check access permission based on linked memo visibility. if err := s.checkAttachmentAccess(ctx, attachment); err != nil { return nil, err } return convertAttachmentFromStore(attachment), 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") } user, err := s.fetchCurrentUser(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, "user not authenticated") } 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") } // Only the creator or admin can update the attachment. if attachment.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } currentTs := time.Now().Unix() update := &store.UpdateAttachment{ ID: attachment.ID, UpdatedTs: ¤tTs, } for _, field := range request.UpdateMask.Paths { if field == "filename" { if !validateFilename(request.Attachment.Filename) { return nil, status.Errorf(codes.InvalidArgument, "filename contains invalid characters or format") } 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.fetchCurrentUser(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, "user not authenticated") } 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 convertAttachmentFromStore(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.MemoUID != nil && *attachment.MemoUID != "" { memoName := fmt.Sprintf("%s%s", MemoNamePrefix, *attachment.MemoUID) attachmentMessage.Memo = &memoName } if attachment.StorageType == storepb.AttachmentStorageType_EXTERNAL || attachment.StorageType == storepb.AttachmentStorageType_S3 { attachmentMessage.ExternalLink = attachment.Reference } return attachmentMessage } // SaveAttachmentBlob saves the blob of attachment based on the storage config. func SaveAttachmentBlob(ctx context.Context, profile *profile.Profile, stores *store.Store, create *store.Attachment) error { instanceStorageSetting, err := stores.GetInstanceStorageSetting(ctx) if err != nil { return errors.Wrap(err, "Failed to find instance storage setting") } if instanceStorageSetting.StorageType == storepb.InstanceStorageSetting_LOCAL { filepathTemplate := "assets/{timestamp}_{filename}" if instanceStorageSetting.FilepathTemplate != "" { filepathTemplate = instanceStorageSetting.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") } // 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 instanceStorageSetting.StorageType == storepb.InstanceStorageSetting_S3 { s3Config := instanceStorageSetting.S3Config if s3Config == nil { return errors.Errorf("No activated external storage found") } s3Client, err := s3.NewClient(ctx, s3Config) if err != nil { return errors.Wrap(err, "Failed to create s3 client") } filepathTemplate := instanceStorageSetting.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 S3 storage, download the file from S3. if attachment.StorageType == storepb.AttachmentStorageType_S3 { if attachment.Payload == nil { return nil, errors.New("attachment payload is missing") } s3Object := attachment.Payload.GetS3Object() if s3Object == nil { return nil, errors.New("S3 object payload is missing") } if s3Object.S3Config == nil { return nil, errors.New("S3 config is missing") } if s3Object.Key == "" { return nil, errors.New("S3 object key is missing") } s3Client, err := s3.NewClient(context.Background(), s3Object.S3Config) if err != nil { return nil, errors.Wrap(err, "failed to create S3 client") } blob, err := s3Client.GetObject(context.Background(), s3Object.Key) if err != nil { return nil, errors.Wrap(err, "failed to get object from S3") } return blob, nil } // For database storage, return the blob from the database. return attachment.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() default: return s } }) return path } func validateFilename(filename string) bool { // Reject path traversal attempts and make sure no additional directories are created if !filepath.IsLocal(filename) || strings.ContainsAny(filename, "/\\") { return false } // Reject filenames starting or ending with spaces or periods if strings.HasPrefix(filename, " ") || strings.HasSuffix(filename, " ") || strings.HasPrefix(filename, ".") || strings.HasSuffix(filename, ".") { return false } return true } func isValidMimeType(mimeType string) bool { // Reject empty or excessively long MIME types if mimeType == "" || len(mimeType) > 255 { return false } // MIME type must match the pattern: type/subtype // Allow common characters in MIME types per RFC 2045 matched, _ := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}$`, mimeType) return matched } func (s *APIV1Service) validateAttachmentFilter(ctx context.Context, filterStr string) error { if filterStr == "" { return errors.New("filter cannot be empty") } engine, err := filter.DefaultAttachmentEngine() if err != nil { return err } var dialect filter.DialectName switch s.Profile.Driver { case "mysql": dialect = filter.DialectMySQL case "postgres": dialect = filter.DialectPostgres default: dialect = filter.DialectSQLite } if _, err := engine.CompileToStatement(ctx, filterStr, filter.RenderOptions{Dialect: dialect}); err != nil { return errors.Wrap(err, "failed to compile filter") } return nil } // checkAttachmentAccess verifies the user has permission to access the attachment. // For unlinked attachments (no memo), only the creator can access. // For linked attachments, access follows the memo's visibility rules. func (s *APIV1Service) checkAttachmentAccess(ctx context.Context, attachment *store.Attachment) error { user, _ := s.fetchCurrentUser(ctx) // For unlinked attachments, only the creator can access. if attachment.MemoID == nil { if user == nil { return status.Errorf(codes.Unauthenticated, "user not authenticated") } if attachment.CreatorID != user.ID && !isSuperUser(user) { return status.Errorf(codes.PermissionDenied, "permission denied") } return nil } // For linked attachments, check memo visibility. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: attachment.MemoID}) if err != nil { return status.Errorf(codes.Internal, "failed to get memo: %v", err) } if memo == nil { return status.Errorf(codes.NotFound, "memo not found") } if memo.Visibility == store.Public { return nil } if user == nil { return status.Errorf(codes.Unauthenticated, "user not authenticated") } if memo.Visibility == store.Private && memo.CreatorID != user.ID && !isSuperUser(user) { return status.Errorf(codes.PermissionDenied, "permission denied") } return nil } // shouldStripExif checks if the MIME type is an image format that may contain EXIF metadata. // Returns true for formats like JPEG, TIFF, WebP, HEIC, and HEIF which commonly contain // privacy-sensitive metadata such as GPS coordinates, camera settings, and device information. func shouldStripExif(mimeType string) bool { return exifCapableImageTypes[mimeType] } // stripImageExif removes EXIF metadata from image files by decoding and re-encoding them. // This prevents exposure of sensitive metadata such as GPS location, camera details, and timestamps. // // The function preserves the correct image orientation by applying EXIF orientation tags // during decoding before stripping all metadata. Images are re-encoded with high quality // to minimize visual degradation. // // Supported formats: // - JPEG/JPG: Re-encoded as JPEG with quality 95 // - PNG: Re-encoded as PNG (lossless) // - TIFF/WebP/HEIC/HEIF: Re-encoded as JPEG with quality 95 // // Returns the cleaned image data without any EXIF metadata, or an error if processing fails. func stripImageExif(imageData []byte, mimeType string) ([]byte, error) { // Decode image with automatic EXIF orientation correction. // This ensures the image displays correctly after metadata removal. img, err := imaging.Decode(bytes.NewReader(imageData), imaging.AutoOrientation(true)) if err != nil { return nil, errors.Wrap(err, "failed to decode image") } // Re-encode the image without EXIF metadata. var buf bytes.Buffer var encodeErr error if mimeType == "image/png" { // Preserve PNG format for lossless encoding encodeErr = imaging.Encode(&buf, img, imaging.PNG) } else { // For JPEG, TIFF, WebP, HEIC, HEIF - re-encode as JPEG. // This ensures EXIF is stripped and provides good compression. encodeErr = imaging.Encode(&buf, img, imaging.JPEG, imaging.JPEGQuality(defaultJPEGQuality)) } if encodeErr != nil { return nil, errors.Wrap(encodeErr, "failed to encode image") } return buf.Bytes(), nil } ================================================ FILE: server/router/api/v1/auth_service.go ================================================ package v1 import ( "context" "fmt" "log/slog" "regexp" "strings" "time" "github.com/pkg/errors" "golang.org/x/crypto/bcrypt" "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/server/auth" "github.com/usememos/memos/store" ) const ( unmatchedUsernameAndPasswordError = "unmatched username and password" ) // GetCurrentUser returns the authenticated user's information. // Validates the access token and returns user details. // // Authentication: Required (access token). // Returns: User information. func (s *APIV1Service) GetCurrentUser(ctx context.Context, _ *v1pb.GetCurrentUserRequest) (*v1pb.GetCurrentUserResponse, error) { user, err := s.fetchCurrentUser(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") } return &v1pb.GetCurrentUserResponse{ User: convertUserFromStore(user), }, nil } // SignIn authenticates a user with credentials and returns tokens. // On success, returns an access token and sets a refresh token cookie. // // Supports two authentication methods: // 1. Password-based authentication (username + password). // 2. SSO authentication (OAuth2 authorization code). // // Authentication: Not required (public endpoint). // Returns: User info, access token, and token expiry. func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest) (*v1pb.SignInResponse, error) { var existingUser *store.User // Authentication Method 1: Password-based authentication 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) } instanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance general setting, error: %v", err) } // Check if the password auth in is allowed. if instanceGeneralSetting.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 { // Authentication Method 2: SSO (OAuth2) authentication idpUID, err := ExtractIdentityProviderUIDFromName(ssoCredentials.IdpName) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err) } identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{ UID: &idpUID, }) 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) } // Pass code_verifier for PKCE support (empty string if not provided for backward compatibility) token, err := oauth2IdentityProvider.ExchangeToken(ctx, ssoCredentials.RedirectUri, ssoCredentials.Code, ssoCredentials.CodeVerifier) 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. instanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance general setting, error: %v", err) } if instanceGeneralSetting.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) } accessToken, accessExpiresAt, err := s.doSignIn(ctx, existingUser) if err != nil { return nil, status.Errorf(codes.Internal, "failed to sign in: %v", err) } return &v1pb.SignInResponse{ User: convertUserFromStore(existingUser), AccessToken: accessToken, AccessTokenExpiresAt: timestamppb.New(accessExpiresAt), }, nil } // doSignIn performs the actual sign-in operation by creating a session and setting the cookie. // // This function: // 1. Generates refresh token and access token. // 2. Stores refresh token metadata in user_setting. // 3. Sets refresh token as HttpOnly cookie. // 4. Returns access token and its expiry time. func (s *APIV1Service) doSignIn(ctx context.Context, user *store.User) (string, time.Time, error) { // Generate refresh token tokenID := util.GenUUID() refreshToken, refreshExpiresAt, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(s.Secret)) if err != nil { return "", time.Time{}, status.Errorf(codes.Internal, "failed to generate refresh token: %v", err) } // Store refresh token metadata clientInfo := s.extractClientInfo(ctx) refreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: tokenID, ExpiresAt: timestamppb.New(refreshExpiresAt), CreatedAt: timestamppb.Now(), ClientInfo: clientInfo, } if err := s.Store.AddUserRefreshToken(ctx, user.ID, refreshTokenRecord); err != nil { slog.Error("failed to store refresh token", "error", err) } // Set refresh token cookie refreshCookie := s.buildRefreshTokenCookie(ctx, refreshToken, refreshExpiresAt) if err := SetResponseHeader(ctx, "Set-Cookie", refreshCookie); err != nil { return "", time.Time{}, status.Errorf(codes.Internal, "failed to set refresh token cookie: %v", err) } // Generate access token accessToken, accessExpiresAt, err := auth.GenerateAccessTokenV2( user.ID, user.Username, string(user.Role), string(user.RowStatus), []byte(s.Secret), ) if err != nil { return "", time.Time{}, status.Errorf(codes.Internal, "failed to generate access token: %v", err) } return accessToken, accessExpiresAt, nil } // SignOut terminates the user's authentication. // Revokes the refresh token and clears the authentication cookie. // // Authentication: Required (access token). // Returns: Empty response on success. func (s *APIV1Service) SignOut(ctx context.Context, _ *v1pb.SignOutRequest) (*emptypb.Empty, error) { // Get user from access token claims claims := auth.GetUserClaims(ctx) if claims != nil { // Revoke refresh token if we can identify it refreshToken := "" if md, ok := metadata.FromIncomingContext(ctx); ok { if cookies := md.Get("cookie"); len(cookies) > 0 { refreshToken = auth.ExtractRefreshTokenFromCookie(cookies[0]) } } if refreshToken != "" { refreshClaims, err := auth.ParseRefreshToken(refreshToken, []byte(s.Secret)) if err == nil { // Remove refresh token from user_setting by token_id _ = s.Store.RemoveUserRefreshToken(ctx, claims.UserID, refreshClaims.TokenID) } } } // Clear refresh token cookie 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 } // RefreshToken exchanges a valid refresh token for a new access token. // // This endpoint implements refresh token rotation with sliding window sessions: // 1. Extracts the refresh token from the HttpOnly cookie (memos_refresh) // 2. Validates the refresh token against the database (checking expiry and revocation) // 3. Rotates the refresh token: generates a new one with fresh 30-day expiry // 4. Generates a new short-lived access token (15 minutes) // 5. Sets the new refresh token as HttpOnly cookie // 6. Returns the new access token and its expiry time // // Token rotation provides: // - Sliding window sessions: active users stay logged in indefinitely // - Better security: stolen refresh tokens become invalid after legitimate refresh // // Authentication: Requires valid refresh token in cookie (public endpoint) // Returns: New access token and expiry timestamp. func (s *APIV1Service) RefreshToken(ctx context.Context, _ *v1pb.RefreshTokenRequest) (*v1pb.RefreshTokenResponse, error) { // Extract refresh token from cookie refreshToken := "" if md, ok := metadata.FromIncomingContext(ctx); ok { if cookies := md.Get("cookie"); len(cookies) > 0 { refreshToken = auth.ExtractRefreshTokenFromCookie(cookies[0]) } } if refreshToken == "" { return nil, status.Errorf(codes.Unauthenticated, "refresh token not found") } // Validate refresh token and get old token ID for rotation authenticator := auth.NewAuthenticator(s.Store, s.Secret) user, oldTokenID, err := authenticator.AuthenticateByRefreshToken(ctx, refreshToken) if err != nil { return nil, status.Errorf(codes.Unauthenticated, "invalid refresh token: %v", err) } // --- Refresh Token Rotation --- // Generate new refresh token with fresh 30-day expiry (sliding window) newTokenID := util.GenUUID() newRefreshToken, newRefreshExpiresAt, err := auth.GenerateRefreshToken(user.ID, newTokenID, []byte(s.Secret)) if err != nil { return nil, status.Errorf(codes.Internal, "failed to generate refresh token: %v", err) } // Store new refresh token (add before remove to handle race conditions) clientInfo := s.extractClientInfo(ctx) newRefreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: newTokenID, ExpiresAt: timestamppb.New(newRefreshExpiresAt), CreatedAt: timestamppb.Now(), ClientInfo: clientInfo, } if err := s.Store.AddUserRefreshToken(ctx, user.ID, newRefreshTokenRecord); err != nil { return nil, status.Errorf(codes.Internal, "failed to store refresh token: %v", err) } // Remove old refresh token if err := s.Store.RemoveUserRefreshToken(ctx, user.ID, oldTokenID); err != nil { // Log but don't fail - old token will expire naturally slog.Warn("failed to remove old refresh token", "error", err, "userID", user.ID, "tokenID", oldTokenID) } // Set new refresh token cookie newRefreshCookie := s.buildRefreshTokenCookie(ctx, newRefreshToken, newRefreshExpiresAt) if err := SetResponseHeader(ctx, "Set-Cookie", newRefreshCookie); err != nil { return nil, status.Errorf(codes.Internal, "failed to set refresh token cookie: %v", err) } // --- End Rotation --- // Generate new access token accessToken, expiresAt, err := auth.GenerateAccessTokenV2( user.ID, user.Username, string(user.Role), string(user.RowStatus), []byte(s.Secret), ) if err != nil { return nil, status.Errorf(codes.Internal, "failed to generate access token: %v", err) } return &v1pb.RefreshTokenResponse{ AccessToken: accessToken, ExpiresAt: timestamppb.New(expiresAt), }, nil } func (s *APIV1Service) clearAuthCookies(ctx context.Context) error { // Clear refresh token cookie refreshCookie := s.buildRefreshTokenCookie(ctx, "", time.Time{}) if err := SetResponseHeader(ctx, "Set-Cookie", refreshCookie); err != nil { return errors.Wrap(err, "failed to set refresh cookie") } return nil } func (*APIV1Service) buildRefreshTokenCookie(ctx context.Context, refreshToken string, expireTime time.Time) string { attrs := []string{ fmt.Sprintf("%s=%s", auth.RefreshTokenCookieName, refreshToken), "Path=/", "HttpOnly", } if expireTime.IsZero() { attrs = append(attrs, "Expires=Thu, 01 Jan 1970 00:00:00 GMT") } else { // RFC 6265 requires cookie expiration dates to use GMT timezone // Convert to UTC and format with explicit "GMT" to ensure browser compatibility attrs = append(attrs, "Expires="+expireTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")) } // Try to determine if the request is HTTPS by checking the origin header // Default to non-HTTPS (Lax SameSite) if metadata is not available isHTTPS := false if md, ok := metadata.FromIncomingContext(ctx); ok { for _, v := range md.Get("origin") { if strings.HasPrefix(v, "https://") { isHTTPS = true break } } } if isHTTPS { attrs = append(attrs, "SameSite=Lax", "Secure") } else { attrs = append(attrs, "SameSite=Lax") } return strings.Join(attrs, "; ") } func (s *APIV1Service) fetchCurrentUser(ctx context.Context) (*store.User, error) { userID := auth.GetUserID(ctx) if userID == 0 { 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 } // extractClientInfo extracts comprehensive client information from the request context. // // This function parses metadata from the gRPC context to extract: // - User Agent: Raw user agent string for detailed parsing // - IP Address: Client IP from X-Forwarded-For or X-Real-IP headers // - Device Type: "mobile", "tablet", or "desktop" (parsed from user agent) // - Operating System: OS name and version (e.g., "iOS 17.1", "Windows 10/11") // - Browser: Browser name and version (e.g., "Chrome 120.0.0.0") // // This information enables users to: // - See all active sessions with device details // - Identify suspicious login attempts // - Revoke specific sessions from unknown devices. func (s *APIV1Service) extractClientInfo(ctx context.Context) *storepb.RefreshTokensUserSetting_ClientInfo { clientInfo := &storepb.RefreshTokensUserSetting_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. // // Detection logic: // - Device Type: Checks for keywords like "mobile", "tablet", "ipad" // - OS: Pattern matches for iOS, Android, Windows, macOS, Linux, Chrome OS // - Browser: Identifies Edge, Chrome, Firefox, Safari, Opera // // Note: This is a simplified parser. For production use with high accuracy requirements, // consider using a dedicated user agent parsing library. func (*APIV1Service) parseUserAgent(userAgent string, clientInfo *storepb.RefreshTokensUserSetting_ClientInfo) { if userAgent == "" { return } userAgent = strings.ToLower(userAgent) // Detect device type if strings.Contains(userAgent, "ipad") || strings.Contains(userAgent, "tablet") { 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 { 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" } } ================================================ FILE: server/router/api/v1/auth_service_client_info_test.go ================================================ 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.RefreshTokensUserSetting_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.RefreshTokensUserSetting_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.Log("---") // 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") } }) } } ================================================ FILE: server/router/api/v1/common.go ================================================ 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_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 } ================================================ FILE: server/router/api/v1/connect_handler.go ================================================ package v1 import ( "net/http" "connectrpc.com/connect" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/usememos/memos/proto/gen/api/v1/apiv1connect" ) // ConnectServiceHandler wraps APIV1Service to implement Connect handler interfaces. // It adapts the existing gRPC service implementations to work with Connect's // request/response wrapper types. // // This wrapper pattern allows us to: // - Reuse existing gRPC service implementations // - Support both native gRPC and Connect protocols // - Maintain a single source of truth for business logic. type ConnectServiceHandler struct { *APIV1Service } // NewConnectServiceHandler creates a new Connect service handler. func NewConnectServiceHandler(svc *APIV1Service) *ConnectServiceHandler { return &ConnectServiceHandler{APIV1Service: svc} } // RegisterConnectHandlers registers all Connect service handlers on the given mux. func (s *ConnectServiceHandler) RegisterConnectHandlers(mux *http.ServeMux, opts ...connect.HandlerOption) { // Register all service handlers handlers := []struct { path string handler http.Handler }{ wrap(apiv1connect.NewInstanceServiceHandler(s, opts...)), wrap(apiv1connect.NewAuthServiceHandler(s, opts...)), wrap(apiv1connect.NewUserServiceHandler(s, opts...)), wrap(apiv1connect.NewMemoServiceHandler(s, opts...)), wrap(apiv1connect.NewAttachmentServiceHandler(s, opts...)), wrap(apiv1connect.NewShortcutServiceHandler(s, opts...)), wrap(apiv1connect.NewIdentityProviderServiceHandler(s, opts...)), } for _, h := range handlers { mux.Handle(h.path, h.handler) } } // wrap converts (path, handler) return value to a struct for cleaner iteration. func wrap(path string, handler http.Handler) struct { path string handler http.Handler } { return struct { path string handler http.Handler }{path, handler} } // convertGRPCError converts gRPC status errors to Connect errors. // This preserves the error code semantics between the two protocols. func convertGRPCError(err error) error { if err == nil { return nil } if st, ok := status.FromError(err); ok { return connect.NewError(grpcCodeToConnectCode(st.Code()), err) } return connect.NewError(connect.CodeInternal, err) } // grpcCodeToConnectCode converts gRPC status codes to Connect error codes. // gRPC and Connect use the same error code semantics, so this is a direct cast. // See: https://connectrpc.com/docs/protocol/#error-codes func grpcCodeToConnectCode(code codes.Code) connect.Code { return connect.Code(code) } ================================================ FILE: server/router/api/v1/connect_interceptors.go ================================================ package v1 import ( "context" "errors" "fmt" "log/slog" "reflect" "runtime/debug" "connectrpc.com/connect" pkgerrors "github.com/pkg/errors" "google.golang.org/grpc/metadata" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) // MetadataInterceptor converts Connect HTTP headers to gRPC metadata. // // This ensures service methods can use metadata.FromIncomingContext() to access // headers like User-Agent, X-Forwarded-For, etc., regardless of whether the // request came via Connect RPC or gRPC-Gateway. type MetadataInterceptor struct{} // NewMetadataInterceptor creates a new metadata interceptor. func NewMetadataInterceptor() *MetadataInterceptor { return &MetadataInterceptor{} } func (*MetadataInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { // Convert HTTP headers to gRPC metadata header := req.Header() md := metadata.MD{} // Copy important headers for client info extraction if ua := header.Get("User-Agent"); ua != "" { md.Set("user-agent", ua) } if xff := header.Get("X-Forwarded-For"); xff != "" { md.Set("x-forwarded-for", xff) } if xri := header.Get("X-Real-Ip"); xri != "" { md.Set("x-real-ip", xri) } // Forward Cookie header for authentication methods that need it (e.g., RefreshToken) if cookie := header.Get("Cookie"); cookie != "" { md.Set("cookie", cookie) } // Set metadata in context so services can use metadata.FromIncomingContext() ctx = metadata.NewIncomingContext(ctx, md) // Execute the request resp, err := next(ctx, req) // Prevent browser caching of API responses to avoid stale data issues // See: https://github.com/usememos/memos/issues/5470 if !isNilAnyResponse(resp) && resp.Header() != nil { resp.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") resp.Header().Set("Pragma", "no-cache") resp.Header().Set("Expires", "0") } return resp, err } } func isNilAnyResponse(resp connect.AnyResponse) bool { if resp == nil { return true } val := reflect.ValueOf(resp) return val.Kind() == reflect.Ptr && val.IsNil() } func (*MetadataInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { return next } func (*MetadataInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return next } // LoggingInterceptor logs Connect RPC requests with appropriate log levels. // // Log levels: // - INFO: Successful requests and expected client errors (not found, permission denied, etc.) // - ERROR: Server errors (internal, unavailable, etc.) type LoggingInterceptor struct { logStacktrace bool } // NewLoggingInterceptor creates a new logging interceptor. func NewLoggingInterceptor(logStacktrace bool) *LoggingInterceptor { return &LoggingInterceptor{logStacktrace: logStacktrace} } func (in *LoggingInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { resp, err := next(ctx, req) in.log(req.Spec().Procedure, err) return resp, err } } func (*LoggingInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { return next // No-op for server-side interceptor } func (*LoggingInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return next // Streaming not used in this service } func (in *LoggingInterceptor) log(procedure string, err error) { level, msg := in.classifyError(err) attrs := []slog.Attr{slog.String("method", procedure)} if err != nil { attrs = append(attrs, slog.String("error", err.Error())) if in.logStacktrace { attrs = append(attrs, slog.String("stacktrace", fmt.Sprintf("%+v", err))) } } slog.LogAttrs(context.Background(), level, msg, attrs...) } func (*LoggingInterceptor) classifyError(err error) (slog.Level, string) { if err == nil { return slog.LevelInfo, "OK" } var connectErr *connect.Error if !pkgerrors.As(err, &connectErr) { return slog.LevelError, "unknown error" } // Client errors (expected, log at INFO) switch connectErr.Code() { case connect.CodeCanceled, connect.CodeInvalidArgument, connect.CodeNotFound, connect.CodeAlreadyExists, connect.CodePermissionDenied, connect.CodeUnauthenticated, connect.CodeResourceExhausted, connect.CodeFailedPrecondition, connect.CodeAborted, connect.CodeOutOfRange: return slog.LevelInfo, "client error" default: // Server errors return slog.LevelError, "server error" } } // RecoveryInterceptor recovers from panics in Connect handlers and returns an internal error. type RecoveryInterceptor struct { logStacktrace bool } // NewRecoveryInterceptor creates a new recovery interceptor. func NewRecoveryInterceptor(logStacktrace bool) *RecoveryInterceptor { return &RecoveryInterceptor{logStacktrace: logStacktrace} } func (in *RecoveryInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (resp connect.AnyResponse, err error) { defer func() { if r := recover(); r != nil { in.logPanic(req.Spec().Procedure, r) err = connect.NewError(connect.CodeInternal, pkgerrors.New("internal server error")) } }() return next(ctx, req) } } func (*RecoveryInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { return next } func (*RecoveryInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return next } func (in *RecoveryInterceptor) logPanic(procedure string, panicValue any) { attrs := []slog.Attr{ slog.String("method", procedure), slog.Any("panic", panicValue), } if in.logStacktrace { attrs = append(attrs, slog.String("stacktrace", string(debug.Stack()))) } slog.LogAttrs(context.Background(), slog.LevelError, "panic recovered in Connect handler", attrs...) } // AuthInterceptor handles authentication for Connect handlers. // // It enforces authentication for all endpoints except those listed in PublicMethods. // Role-based authorization (admin checks) remains in the service layer. type AuthInterceptor struct { authenticator *auth.Authenticator } // NewAuthInterceptor creates a new auth interceptor. func NewAuthInterceptor(store *store.Store, secret string) *AuthInterceptor { return &AuthInterceptor{ authenticator: auth.NewAuthenticator(store, secret), } } func (in *AuthInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { header := req.Header() authHeader := header.Get("Authorization") result := in.authenticator.Authenticate(ctx, authHeader) // Enforce authentication for non-public methods if result == nil && !IsPublicMethod(req.Spec().Procedure) { return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("authentication required")) } ctx = auth.ApplyToContext(ctx, result) return next(ctx, req) } } func (*AuthInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { return next } func (*AuthInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return next } ================================================ FILE: server/router/api/v1/connect_services.go ================================================ package v1 import ( "context" "connectrpc.com/connect" "google.golang.org/protobuf/types/known/emptypb" v1pb "github.com/usememos/memos/proto/gen/api/v1" ) // This file contains all Connect service handler method implementations. // Each method delegates to the underlying gRPC service implementation, // converting between Connect and gRPC request/response types. // InstanceService func (s *ConnectServiceHandler) GetInstanceProfile(ctx context.Context, req *connect.Request[v1pb.GetInstanceProfileRequest]) (*connect.Response[v1pb.InstanceProfile], error) { resp, err := s.APIV1Service.GetInstanceProfile(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) GetInstanceSetting(ctx context.Context, req *connect.Request[v1pb.GetInstanceSettingRequest]) (*connect.Response[v1pb.InstanceSetting], error) { resp, err := s.APIV1Service.GetInstanceSetting(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) UpdateInstanceSetting(ctx context.Context, req *connect.Request[v1pb.UpdateInstanceSettingRequest]) (*connect.Response[v1pb.InstanceSetting], error) { resp, err := s.APIV1Service.UpdateInstanceSetting(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } // AuthService // // Auth service methods need special handling for response headers (cookies). // We use connectWithHeaderCarrier helper to inject a header carrier into the context, // which allows the service to set headers in a protocol-agnostic way. func (s *ConnectServiceHandler) GetCurrentUser(ctx context.Context, req *connect.Request[v1pb.GetCurrentUserRequest]) (*connect.Response[v1pb.GetCurrentUserResponse], error) { return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.GetCurrentUserResponse, error) { return s.APIV1Service.GetCurrentUser(ctx, req.Msg) }) } func (s *ConnectServiceHandler) SignIn(ctx context.Context, req *connect.Request[v1pb.SignInRequest]) (*connect.Response[v1pb.SignInResponse], error) { return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.SignInResponse, error) { return s.APIV1Service.SignIn(ctx, req.Msg) }) } func (s *ConnectServiceHandler) SignOut(ctx context.Context, req *connect.Request[v1pb.SignOutRequest]) (*connect.Response[emptypb.Empty], error) { return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*emptypb.Empty, error) { return s.APIV1Service.SignOut(ctx, req.Msg) }) } func (s *ConnectServiceHandler) RefreshToken(ctx context.Context, req *connect.Request[v1pb.RefreshTokenRequest]) (*connect.Response[v1pb.RefreshTokenResponse], error) { return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.RefreshTokenResponse, error) { return s.APIV1Service.RefreshToken(ctx, req.Msg) }) } // UserService func (s *ConnectServiceHandler) ListUsers(ctx context.Context, req *connect.Request[v1pb.ListUsersRequest]) (*connect.Response[v1pb.ListUsersResponse], error) { resp, err := s.APIV1Service.ListUsers(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) GetUser(ctx context.Context, req *connect.Request[v1pb.GetUserRequest]) (*connect.Response[v1pb.User], error) { resp, err := s.APIV1Service.GetUser(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) CreateUser(ctx context.Context, req *connect.Request[v1pb.CreateUserRequest]) (*connect.Response[v1pb.User], error) { resp, err := s.APIV1Service.CreateUser(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) UpdateUser(ctx context.Context, req *connect.Request[v1pb.UpdateUserRequest]) (*connect.Response[v1pb.User], error) { resp, err := s.APIV1Service.UpdateUser(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) DeleteUser(ctx context.Context, req *connect.Request[v1pb.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.DeleteUser(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListAllUserStats(ctx context.Context, req *connect.Request[v1pb.ListAllUserStatsRequest]) (*connect.Response[v1pb.ListAllUserStatsResponse], error) { resp, err := s.APIV1Service.ListAllUserStats(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) GetUserStats(ctx context.Context, req *connect.Request[v1pb.GetUserStatsRequest]) (*connect.Response[v1pb.UserStats], error) { resp, err := s.APIV1Service.GetUserStats(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) GetUserSetting(ctx context.Context, req *connect.Request[v1pb.GetUserSettingRequest]) (*connect.Response[v1pb.UserSetting], error) { resp, err := s.APIV1Service.GetUserSetting(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) UpdateUserSetting(ctx context.Context, req *connect.Request[v1pb.UpdateUserSettingRequest]) (*connect.Response[v1pb.UserSetting], error) { resp, err := s.APIV1Service.UpdateUserSetting(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListUserSettings(ctx context.Context, req *connect.Request[v1pb.ListUserSettingsRequest]) (*connect.Response[v1pb.ListUserSettingsResponse], error) { resp, err := s.APIV1Service.ListUserSettings(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1pb.ListPersonalAccessTokensRequest]) (*connect.Response[v1pb.ListPersonalAccessTokensResponse], error) { resp, err := s.APIV1Service.ListPersonalAccessTokens(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) CreatePersonalAccessToken(ctx context.Context, req *connect.Request[v1pb.CreatePersonalAccessTokenRequest]) (*connect.Response[v1pb.CreatePersonalAccessTokenResponse], error) { resp, err := s.APIV1Service.CreatePersonalAccessToken(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) DeletePersonalAccessToken(ctx context.Context, req *connect.Request[v1pb.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.DeletePersonalAccessToken(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListUserWebhooks(ctx context.Context, req *connect.Request[v1pb.ListUserWebhooksRequest]) (*connect.Response[v1pb.ListUserWebhooksResponse], error) { resp, err := s.APIV1Service.ListUserWebhooks(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) CreateUserWebhook(ctx context.Context, req *connect.Request[v1pb.CreateUserWebhookRequest]) (*connect.Response[v1pb.UserWebhook], error) { resp, err := s.APIV1Service.CreateUserWebhook(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) UpdateUserWebhook(ctx context.Context, req *connect.Request[v1pb.UpdateUserWebhookRequest]) (*connect.Response[v1pb.UserWebhook], error) { resp, err := s.APIV1Service.UpdateUserWebhook(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) DeleteUserWebhook(ctx context.Context, req *connect.Request[v1pb.DeleteUserWebhookRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.DeleteUserWebhook(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListUserNotifications(ctx context.Context, req *connect.Request[v1pb.ListUserNotificationsRequest]) (*connect.Response[v1pb.ListUserNotificationsResponse], error) { resp, err := s.APIV1Service.ListUserNotifications(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) UpdateUserNotification(ctx context.Context, req *connect.Request[v1pb.UpdateUserNotificationRequest]) (*connect.Response[v1pb.UserNotification], error) { resp, err := s.APIV1Service.UpdateUserNotification(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) DeleteUserNotification(ctx context.Context, req *connect.Request[v1pb.DeleteUserNotificationRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.DeleteUserNotification(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } // MemoService func (s *ConnectServiceHandler) CreateMemo(ctx context.Context, req *connect.Request[v1pb.CreateMemoRequest]) (*connect.Response[v1pb.Memo], error) { resp, err := s.APIV1Service.CreateMemo(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListMemos(ctx context.Context, req *connect.Request[v1pb.ListMemosRequest]) (*connect.Response[v1pb.ListMemosResponse], error) { resp, err := s.APIV1Service.ListMemos(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) GetMemo(ctx context.Context, req *connect.Request[v1pb.GetMemoRequest]) (*connect.Response[v1pb.Memo], error) { resp, err := s.APIV1Service.GetMemo(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) UpdateMemo(ctx context.Context, req *connect.Request[v1pb.UpdateMemoRequest]) (*connect.Response[v1pb.Memo], error) { resp, err := s.APIV1Service.UpdateMemo(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) DeleteMemo(ctx context.Context, req *connect.Request[v1pb.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.DeleteMemo(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) SetMemoAttachments(ctx context.Context, req *connect.Request[v1pb.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.SetMemoAttachments(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListMemoAttachments(ctx context.Context, req *connect.Request[v1pb.ListMemoAttachmentsRequest]) (*connect.Response[v1pb.ListMemoAttachmentsResponse], error) { resp, err := s.APIV1Service.ListMemoAttachments(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) SetMemoRelations(ctx context.Context, req *connect.Request[v1pb.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.SetMemoRelations(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListMemoRelations(ctx context.Context, req *connect.Request[v1pb.ListMemoRelationsRequest]) (*connect.Response[v1pb.ListMemoRelationsResponse], error) { resp, err := s.APIV1Service.ListMemoRelations(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) CreateMemoComment(ctx context.Context, req *connect.Request[v1pb.CreateMemoCommentRequest]) (*connect.Response[v1pb.Memo], error) { resp, err := s.APIV1Service.CreateMemoComment(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListMemoComments(ctx context.Context, req *connect.Request[v1pb.ListMemoCommentsRequest]) (*connect.Response[v1pb.ListMemoCommentsResponse], error) { resp, err := s.APIV1Service.ListMemoComments(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListMemoReactions(ctx context.Context, req *connect.Request[v1pb.ListMemoReactionsRequest]) (*connect.Response[v1pb.ListMemoReactionsResponse], error) { resp, err := s.APIV1Service.ListMemoReactions(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) UpsertMemoReaction(ctx context.Context, req *connect.Request[v1pb.UpsertMemoReactionRequest]) (*connect.Response[v1pb.Reaction], error) { resp, err := s.APIV1Service.UpsertMemoReaction(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) DeleteMemoReaction(ctx context.Context, req *connect.Request[v1pb.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.DeleteMemoReaction(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) CreateMemoShare(ctx context.Context, req *connect.Request[v1pb.CreateMemoShareRequest]) (*connect.Response[v1pb.MemoShare], error) { resp, err := s.APIV1Service.CreateMemoShare(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListMemoShares(ctx context.Context, req *connect.Request[v1pb.ListMemoSharesRequest]) (*connect.Response[v1pb.ListMemoSharesResponse], error) { resp, err := s.APIV1Service.ListMemoShares(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) DeleteMemoShare(ctx context.Context, req *connect.Request[v1pb.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.DeleteMemoShare(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) GetMemoByShare(ctx context.Context, req *connect.Request[v1pb.GetMemoByShareRequest]) (*connect.Response[v1pb.Memo], error) { resp, err := s.APIV1Service.GetMemoByShare(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } // AttachmentService func (s *ConnectServiceHandler) CreateAttachment(ctx context.Context, req *connect.Request[v1pb.CreateAttachmentRequest]) (*connect.Response[v1pb.Attachment], error) { resp, err := s.APIV1Service.CreateAttachment(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) ListAttachments(ctx context.Context, req *connect.Request[v1pb.ListAttachmentsRequest]) (*connect.Response[v1pb.ListAttachmentsResponse], error) { resp, err := s.APIV1Service.ListAttachments(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) GetAttachment(ctx context.Context, req *connect.Request[v1pb.GetAttachmentRequest]) (*connect.Response[v1pb.Attachment], error) { resp, err := s.APIV1Service.GetAttachment(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) UpdateAttachment(ctx context.Context, req *connect.Request[v1pb.UpdateAttachmentRequest]) (*connect.Response[v1pb.Attachment], error) { resp, err := s.APIV1Service.UpdateAttachment(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) DeleteAttachment(ctx context.Context, req *connect.Request[v1pb.DeleteAttachmentRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.DeleteAttachment(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } // ShortcutService func (s *ConnectServiceHandler) ListShortcuts(ctx context.Context, req *connect.Request[v1pb.ListShortcutsRequest]) (*connect.Response[v1pb.ListShortcutsResponse], error) { resp, err := s.APIV1Service.ListShortcuts(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) GetShortcut(ctx context.Context, req *connect.Request[v1pb.GetShortcutRequest]) (*connect.Response[v1pb.Shortcut], error) { resp, err := s.APIV1Service.GetShortcut(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) CreateShortcut(ctx context.Context, req *connect.Request[v1pb.CreateShortcutRequest]) (*connect.Response[v1pb.Shortcut], error) { resp, err := s.APIV1Service.CreateShortcut(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) UpdateShortcut(ctx context.Context, req *connect.Request[v1pb.UpdateShortcutRequest]) (*connect.Response[v1pb.Shortcut], error) { resp, err := s.APIV1Service.UpdateShortcut(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) DeleteShortcut(ctx context.Context, req *connect.Request[v1pb.DeleteShortcutRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.DeleteShortcut(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } // IdentityProviderService func (s *ConnectServiceHandler) ListIdentityProviders(ctx context.Context, req *connect.Request[v1pb.ListIdentityProvidersRequest]) (*connect.Response[v1pb.ListIdentityProvidersResponse], error) { resp, err := s.APIV1Service.ListIdentityProviders(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) GetIdentityProvider(ctx context.Context, req *connect.Request[v1pb.GetIdentityProviderRequest]) (*connect.Response[v1pb.IdentityProvider], error) { resp, err := s.APIV1Service.GetIdentityProvider(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) CreateIdentityProvider(ctx context.Context, req *connect.Request[v1pb.CreateIdentityProviderRequest]) (*connect.Response[v1pb.IdentityProvider], error) { resp, err := s.APIV1Service.CreateIdentityProvider(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) UpdateIdentityProvider(ctx context.Context, req *connect.Request[v1pb.UpdateIdentityProviderRequest]) (*connect.Response[v1pb.IdentityProvider], error) { resp, err := s.APIV1Service.UpdateIdentityProvider(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } func (s *ConnectServiceHandler) DeleteIdentityProvider(ctx context.Context, req *connect.Request[v1pb.DeleteIdentityProviderRequest]) (*connect.Response[emptypb.Empty], error) { resp, err := s.APIV1Service.DeleteIdentityProvider(ctx, req.Msg) if err != nil { return nil, convertGRPCError(err) } return connect.NewResponse(resp), nil } ================================================ FILE: server/router/api/v1/header_carrier.go ================================================ package v1 import ( "context" "connectrpc.com/connect" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) // headerCarrierKey is the context key for storing headers to be set in the response. type headerCarrierKey struct{} // HeaderCarrier stores headers that need to be set in the response. // // Problem: The codebase supports two protocols simultaneously: // - Native gRPC: Uses grpc.SetHeader() to set response headers // - Connect-RPC: Uses connect.Response.Header().Set() to set response headers // // Solution: HeaderCarrier provides a protocol-agnostic way to set headers. // - Service methods call SetResponseHeader() regardless of protocol // - For gRPC requests: SetResponseHeader uses grpc.SetHeader directly // - For Connect requests: SetResponseHeader stores headers in HeaderCarrier // - Connect wrappers extract headers from HeaderCarrier and apply to response // // This allows service methods to work with both protocols without knowing which one is being used. type HeaderCarrier struct { headers map[string]string } // newHeaderCarrier creates a new header carrier. func newHeaderCarrier() *HeaderCarrier { return &HeaderCarrier{ headers: make(map[string]string), } } // Set adds a header to the carrier. func (h *HeaderCarrier) Set(key, value string) { h.headers[key] = value } // Get retrieves a header from the carrier. func (h *HeaderCarrier) Get(key string) string { return h.headers[key] } // All returns all headers. func (h *HeaderCarrier) All() map[string]string { return h.headers } // WithHeaderCarrier adds a header carrier to the context. func WithHeaderCarrier(ctx context.Context) context.Context { return context.WithValue(ctx, headerCarrierKey{}, newHeaderCarrier()) } // GetHeaderCarrier retrieves the header carrier from the context. // Returns nil if no carrier is present. func GetHeaderCarrier(ctx context.Context) *HeaderCarrier { if carrier, ok := ctx.Value(headerCarrierKey{}).(*HeaderCarrier); ok { return carrier } return nil } // SetResponseHeader sets a header in the response. // // This function works for both gRPC and Connect protocols: // - For gRPC: Uses grpc.SetHeader to set headers in gRPC metadata // - For Connect: Stores in HeaderCarrier for Connect wrapper to apply later // // The protocol is automatically detected based on whether a HeaderCarrier // exists in the context (injected by Connect wrappers). func SetResponseHeader(ctx context.Context, key, value string) error { // Try Connect first (check if we have a header carrier) if carrier := GetHeaderCarrier(ctx); carrier != nil { carrier.Set(key, value) return nil } // Fall back to gRPC return grpc.SetHeader(ctx, metadata.New(map[string]string{ key: value, })) } // connectWithHeaderCarrier is a helper for Connect service wrappers that need to set response headers. // // It injects a HeaderCarrier into the context, calls the service method, // and applies any headers from the carrier to the Connect response. // // The generic parameter T is the non-pointer protobuf message type (e.g., v1pb.CreateSessionResponse), // while fn returns *T (the pointer type) as is standard for protobuf messages. // // Usage in Connect wrappers: // // func (s *ConnectServiceHandler) CreateSession(ctx context.Context, req *connect.Request[v1pb.CreateSessionRequest]) (*connect.Response[v1pb.CreateSessionResponse], error) { // return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.CreateSessionResponse, error) { // return s.APIV1Service.CreateSession(ctx, req.Msg) // }) // } func connectWithHeaderCarrier[T any](ctx context.Context, fn func(context.Context) (*T, error)) (*connect.Response[T], error) { // Inject header carrier for Connect protocol ctx = WithHeaderCarrier(ctx) // Call the service method resp, err := fn(ctx) if err != nil { return nil, convertGRPCError(err) } // Create Connect response connectResp := connect.NewResponse(resp) // Apply any headers set via the header carrier if carrier := GetHeaderCarrier(ctx); carrier != nil { for key, value := range carrier.All() { connectResp.Header().Set(key, value) } } return connectResp, nil } ================================================ FILE: server/router/api/v1/health_service.go ================================================ package v1 import ( "context" "google.golang.org/grpc/codes" "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/status" ) func (s *APIV1Service) Check(ctx context.Context, _ *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) { // Check if database is initialized by verifying instance basic setting exists instanceBasicSetting, err := s.Store.GetInstanceBasicSetting(ctx) if err != nil { return nil, status.Errorf(codes.Unavailable, "database not initialized: %v", err) } // Verify schema version is set (empty means database not properly initialized) if instanceBasicSetting.SchemaVersion == "" { return nil, status.Errorf(codes.Unavailable, "schema version not set") } return &grpc_health_v1.HealthCheckResponse{Status: grpc_health_v1.HealthCheckResponse_SERVING}, nil } ================================================ FILE: server/router/api/v1/idp_service.go ================================================ 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.fetchCurrentUser(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") } if currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } idpUID, err := ValidateAndGenerateUID(request.IdentityProviderId) if err != nil { return nil, err } storeIdp := convertIdentityProviderToStore(request.IdentityProvider) storeIdp.Uid = idpUID identityProvider, err := s.Store.CreateIdentityProvider(ctx, storeIdp) 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{}, } // Default to lowest-privilege role, update later based on real role currentUserRole := store.RoleUser currentUser, err := s.fetchCurrentUser(ctx) if err == nil && currentUser != nil { currentUserRole = currentUser.Role } for _, identityProvider := range identityProviders { identityProviderConverted := convertIdentityProviderFromStore(identityProvider) response.IdentityProviders = append(response.IdentityProviders, redactIdentityProviderResponse(identityProviderConverted, currentUserRole)) } return response, nil } func (s *APIV1Service) GetIdentityProvider(ctx context.Context, request *v1pb.GetIdentityProviderRequest) (*v1pb.IdentityProvider, error) { uid, err := ExtractIdentityProviderUIDFromName(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{ UID: &uid, }) 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") } // Default to lowest-privilege role, update later based on real role currentUserRole := store.RoleUser currentUser, err := s.fetchCurrentUser(ctx) if err == nil && currentUser != nil { currentUserRole = currentUser.Role } identityProviderConverted := convertIdentityProviderFromStore(identityProvider) return redactIdentityProviderResponse(identityProviderConverted, currentUserRole), nil } func (s *APIV1Service) UpdateIdentityProvider(ctx context.Context, request *v1pb.UpdateIdentityProviderRequest) (*v1pb.IdentityProvider, error) { currentUser, err := s.fetchCurrentUser(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") } if currentUser.Role != store.RoleAdmin { 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") } uid, err := ExtractIdentityProviderUIDFromName(request.IdentityProvider.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err) } // Look up the IdP by UID to get the internal ID for update. existing, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{UID: &uid}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get identity provider, error: %+v", err) } if existing == nil { return nil, status.Errorf(codes.NotFound, "identity provider not found") } update := &store.UpdateIdentityProviderV1{ ID: existing.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) default: // Ignore unsupported fields } } 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) { currentUser, err := s.fetchCurrentUser(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") } if currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } uid, err := ExtractIdentityProviderUIDFromName(request.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err) } // Look up the IdP by UID to get the internal ID for deletion. identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{UID: &uid}) 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: identityProvider.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%s", IdentityProviderNamePrefix, identityProvider.Uid), 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 { temp := &storepb.IdentityProvider{ 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 } func redactIdentityProviderResponse(identityProvider *v1pb.IdentityProvider, userRole store.Role) *v1pb.IdentityProvider { if userRole != store.RoleAdmin { if identityProvider.Type == v1pb.IdentityProvider_OAUTH2 { identityProvider.Config.GetOauth2Config().ClientSecret = "" } } return identityProvider } ================================================ FILE: server/router/api/v1/instance_service.go ================================================ package v1 import ( "context" "fmt" "math" "strings" "github.com/pkg/errors" colorpb "google.golang.org/genproto/googleapis/type/color" "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" ) // GetInstanceProfile returns the instance profile. func (s *APIV1Service) GetInstanceProfile(ctx context.Context, _ *v1pb.GetInstanceProfileRequest) (*v1pb.InstanceProfile, error) { admin, err := s.GetInstanceAdmin(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance admin: %v", err) } instanceProfile := &v1pb.InstanceProfile{ Version: s.Profile.Version, Demo: s.Profile.Demo, InstanceUrl: s.Profile.InstanceURL, Admin: admin, // nil when not initialized } return instanceProfile, nil } func (s *APIV1Service) GetInstanceSetting(ctx context.Context, request *v1pb.GetInstanceSettingRequest) (*v1pb.InstanceSetting, error) { instanceSettingKeyString, err := ExtractInstanceSettingKeyFromName(request.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid instance setting name: %v", err) } instanceSettingKey := storepb.InstanceSettingKey(storepb.InstanceSettingKey_value[instanceSettingKeyString]) // Get instance setting from store with default value. switch instanceSettingKey { case storepb.InstanceSettingKey_BASIC: _, err = s.Store.GetInstanceBasicSetting(ctx) case storepb.InstanceSettingKey_GENERAL: _, err = s.Store.GetInstanceGeneralSetting(ctx) case storepb.InstanceSettingKey_MEMO_RELATED: _, err = s.Store.GetInstanceMemoRelatedSetting(ctx) case storepb.InstanceSettingKey_STORAGE: _, err = s.Store.GetInstanceStorageSetting(ctx) case storepb.InstanceSettingKey_TAGS: _, err = s.Store.GetInstanceTagsSetting(ctx) case storepb.InstanceSettingKey_NOTIFICATION: _, err = s.Store.GetInstanceNotificationSetting(ctx) default: return nil, status.Errorf(codes.InvalidArgument, "unsupported instance setting key: %v", instanceSettingKey) } if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance setting: %v", err) } instanceSetting, err := s.Store.GetInstanceSetting(ctx, &store.FindInstanceSetting{ Name: instanceSettingKey.String(), }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance setting: %v", err) } if instanceSetting == nil { return nil, status.Errorf(codes.NotFound, "instance setting not found") } // For storage setting, only admin can get it. if instanceSetting.Key == storepb.InstanceSettingKey_STORAGE { user, err := s.fetchCurrentUser(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, "user not authenticated") } if user.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } } return convertInstanceSettingFromStore(instanceSetting), nil } func (s *APIV1Service) UpdateInstanceSetting(ctx context.Context, request *v1pb.UpdateInstanceSettingRequest) (*v1pb.InstanceSetting, error) { user, err := s.fetchCurrentUser(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, "user not authenticated") } if user.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } // TODO: Apply update_mask if specified _ = request.UpdateMask if err := validateInstanceSetting(request.Setting); err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid instance setting: %v", err) } updateSetting := convertInstanceSettingToStore(request.Setting) instanceSetting, err := s.Store.UpsertInstanceSetting(ctx, updateSetting) if err != nil { return nil, status.Errorf(codes.Internal, "failed to upsert instance setting: %v", err) } return convertInstanceSettingFromStore(instanceSetting), nil } func convertInstanceSettingFromStore(setting *storepb.InstanceSetting) *v1pb.InstanceSetting { instanceSetting := &v1pb.InstanceSetting{ Name: fmt.Sprintf("instance/settings/%s", setting.Key.String()), } switch setting.Value.(type) { case *storepb.InstanceSetting_GeneralSetting: instanceSetting.Value = &v1pb.InstanceSetting_GeneralSetting_{ GeneralSetting: convertInstanceGeneralSettingFromStore(setting.GetGeneralSetting()), } case *storepb.InstanceSetting_StorageSetting: instanceSetting.Value = &v1pb.InstanceSetting_StorageSetting_{ StorageSetting: convertInstanceStorageSettingFromStore(setting.GetStorageSetting()), } case *storepb.InstanceSetting_MemoRelatedSetting: instanceSetting.Value = &v1pb.InstanceSetting_MemoRelatedSetting_{ MemoRelatedSetting: convertInstanceMemoRelatedSettingFromStore(setting.GetMemoRelatedSetting()), } case *storepb.InstanceSetting_TagsSetting: instanceSetting.Value = &v1pb.InstanceSetting_TagsSetting_{ TagsSetting: convertInstanceTagsSettingFromStore(setting.GetTagsSetting()), } case *storepb.InstanceSetting_NotificationSetting: instanceSetting.Value = &v1pb.InstanceSetting_NotificationSetting_{ NotificationSetting: convertInstanceNotificationSettingFromStore(setting.GetNotificationSetting()), } default: // Leave Value unset for unsupported setting variants. } return instanceSetting } func convertInstanceSettingToStore(setting *v1pb.InstanceSetting) *storepb.InstanceSetting { settingKeyString, _ := ExtractInstanceSettingKeyFromName(setting.Name) instanceSetting := &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey(storepb.InstanceSettingKey_value[settingKeyString]), Value: &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: convertInstanceGeneralSettingToStore(setting.GetGeneralSetting()), }, } switch instanceSetting.Key { case storepb.InstanceSettingKey_GENERAL: instanceSetting.Value = &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: convertInstanceGeneralSettingToStore(setting.GetGeneralSetting()), } case storepb.InstanceSettingKey_STORAGE: instanceSetting.Value = &storepb.InstanceSetting_StorageSetting{ StorageSetting: convertInstanceStorageSettingToStore(setting.GetStorageSetting()), } case storepb.InstanceSettingKey_MEMO_RELATED: instanceSetting.Value = &storepb.InstanceSetting_MemoRelatedSetting{ MemoRelatedSetting: convertInstanceMemoRelatedSettingToStore(setting.GetMemoRelatedSetting()), } case storepb.InstanceSettingKey_TAGS: instanceSetting.Value = &storepb.InstanceSetting_TagsSetting{ TagsSetting: convertInstanceTagsSettingToStore(setting.GetTagsSetting()), } case storepb.InstanceSettingKey_NOTIFICATION: instanceSetting.Value = &storepb.InstanceSetting_NotificationSetting{ NotificationSetting: convertInstanceNotificationSettingToStore(setting.GetNotificationSetting()), } default: // Keep the default GeneralSetting value } return instanceSetting } func convertInstanceGeneralSettingFromStore(setting *storepb.InstanceGeneralSetting) *v1pb.InstanceSetting_GeneralSetting { if setting == nil { return nil } generalSetting := &v1pb.InstanceSetting_GeneralSetting{ 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.InstanceSetting_GeneralSetting_CustomProfile{ Title: setting.CustomProfile.Title, Description: setting.CustomProfile.Description, LogoUrl: setting.CustomProfile.LogoUrl, } } return generalSetting } func convertInstanceGeneralSettingToStore(setting *v1pb.InstanceSetting_GeneralSetting) *storepb.InstanceGeneralSetting { if setting == nil { return nil } generalSetting := &storepb.InstanceGeneralSetting{ 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.InstanceCustomProfile{ Title: setting.CustomProfile.Title, Description: setting.CustomProfile.Description, LogoUrl: setting.CustomProfile.LogoUrl, } } return generalSetting } func convertInstanceStorageSettingFromStore(settingpb *storepb.InstanceStorageSetting) *v1pb.InstanceSetting_StorageSetting { if settingpb == nil { return nil } setting := &v1pb.InstanceSetting_StorageSetting{ StorageType: v1pb.InstanceSetting_StorageSetting_StorageType(settingpb.StorageType), FilepathTemplate: settingpb.FilepathTemplate, UploadSizeLimitMb: settingpb.UploadSizeLimitMb, } if settingpb.S3Config != nil { setting.S3Config = &v1pb.InstanceSetting_StorageSetting_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 convertInstanceStorageSettingToStore(setting *v1pb.InstanceSetting_StorageSetting) *storepb.InstanceStorageSetting { if setting == nil { return nil } settingpb := &storepb.InstanceStorageSetting{ StorageType: storepb.InstanceStorageSetting_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 convertInstanceMemoRelatedSettingFromStore(setting *storepb.InstanceMemoRelatedSetting) *v1pb.InstanceSetting_MemoRelatedSetting { if setting == nil { return nil } return &v1pb.InstanceSetting_MemoRelatedSetting{ DisplayWithUpdateTime: setting.DisplayWithUpdateTime, ContentLengthLimit: setting.ContentLengthLimit, EnableDoubleClickEdit: setting.EnableDoubleClickEdit, Reactions: setting.Reactions, } } func convertInstanceMemoRelatedSettingToStore(setting *v1pb.InstanceSetting_MemoRelatedSetting) *storepb.InstanceMemoRelatedSetting { if setting == nil { return nil } return &storepb.InstanceMemoRelatedSetting{ DisplayWithUpdateTime: setting.DisplayWithUpdateTime, ContentLengthLimit: setting.ContentLengthLimit, EnableDoubleClickEdit: setting.EnableDoubleClickEdit, Reactions: setting.Reactions, } } func convertInstanceTagsSettingFromStore(setting *storepb.InstanceTagsSetting) *v1pb.InstanceSetting_TagsSetting { if setting == nil { return nil } tags := make(map[string]*v1pb.InstanceSetting_TagMetadata, len(setting.Tags)) for tag, metadata := range setting.Tags { tags[tag] = &v1pb.InstanceSetting_TagMetadata{ BackgroundColor: metadata.GetBackgroundColor(), } } return &v1pb.InstanceSetting_TagsSetting{ Tags: tags, } } func convertInstanceTagsSettingToStore(setting *v1pb.InstanceSetting_TagsSetting) *storepb.InstanceTagsSetting { if setting == nil { return nil } tags := make(map[string]*storepb.InstanceTagMetadata, len(setting.Tags)) for tag, metadata := range setting.Tags { tags[tag] = &storepb.InstanceTagMetadata{ BackgroundColor: metadata.GetBackgroundColor(), } } return &storepb.InstanceTagsSetting{ Tags: tags, } } func convertInstanceNotificationSettingFromStore(setting *storepb.InstanceNotificationSetting) *v1pb.InstanceSetting_NotificationSetting { if setting == nil { return nil } notificationSetting := &v1pb.InstanceSetting_NotificationSetting{} if setting.Email != nil { notificationSetting.Email = &v1pb.InstanceSetting_NotificationSetting_EmailSetting{ Enabled: setting.Email.Enabled, SmtpHost: setting.Email.SmtpHost, SmtpPort: setting.Email.SmtpPort, SmtpUsername: setting.Email.SmtpUsername, SmtpPassword: setting.Email.SmtpPassword, FromEmail: setting.Email.FromEmail, FromName: setting.Email.FromName, ReplyTo: setting.Email.ReplyTo, UseTls: setting.Email.UseTls, UseSsl: setting.Email.UseSsl, } } return notificationSetting } func convertInstanceNotificationSettingToStore(setting *v1pb.InstanceSetting_NotificationSetting) *storepb.InstanceNotificationSetting { if setting == nil { return nil } notificationSetting := &storepb.InstanceNotificationSetting{} if setting.Email != nil { notificationSetting.Email = &storepb.InstanceNotificationSetting_EmailSetting{ Enabled: setting.Email.Enabled, SmtpHost: setting.Email.SmtpHost, SmtpPort: setting.Email.SmtpPort, SmtpUsername: setting.Email.SmtpUsername, SmtpPassword: setting.Email.SmtpPassword, FromEmail: setting.Email.FromEmail, FromName: setting.Email.FromName, ReplyTo: setting.Email.ReplyTo, UseTls: setting.Email.UseTls, UseSsl: setting.Email.UseSsl, } } return notificationSetting } func validateInstanceSetting(setting *v1pb.InstanceSetting) error { key, err := ExtractInstanceSettingKeyFromName(setting.Name) if err != nil { return err } if key != storepb.InstanceSettingKey_TAGS.String() { return nil } return validateInstanceTagsSetting(setting.GetTagsSetting()) } func validateInstanceTagsSetting(setting *v1pb.InstanceSetting_TagsSetting) error { if setting == nil { return errors.New("tags setting is required") } for tag, metadata := range setting.Tags { if strings.TrimSpace(tag) == "" { return errors.New("tag key cannot be empty") } if metadata == nil { return errors.Errorf("tag metadata is required for %q", tag) } if metadata.GetBackgroundColor() == nil { return errors.Errorf("background_color is required for %q", tag) } if err := validateInstanceColor(metadata.GetBackgroundColor()); err != nil { return errors.Wrapf(err, "background_color for %q", tag) } } return nil } func validateInstanceColor(color *colorpb.Color) error { if err := validateInstanceColorComponent("red", color.GetRed()); err != nil { return err } if err := validateInstanceColorComponent("green", color.GetGreen()); err != nil { return err } if err := validateInstanceColorComponent("blue", color.GetBlue()); err != nil { return err } if alpha := color.GetAlpha(); alpha != nil { if err := validateInstanceColorComponent("alpha", alpha.GetValue()); err != nil { return err } } return nil } func validateInstanceColorComponent(name string, value float32) error { if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) { return errors.Errorf("%s must be a finite number", name) } if value < 0 || value > 1 { return errors.Errorf("%s must be between 0 and 1", name) } return nil } func (s *APIV1Service) GetInstanceAdmin(ctx context.Context) (*v1pb.User, error) { adminUserType := store.RoleAdmin user, err := s.Store.GetUser(ctx, &store.FindUser{ Role: &adminUserType, }) if err != nil { return nil, errors.Wrapf(err, "failed to find admin") } if user == nil { return nil, nil } return convertUserFromStore(user), nil } ================================================ FILE: server/router/api/v1/memo_attachment_service.go ================================================ 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) { user, err := s.fetchCurrentUser(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, "user not authenticated") } 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") } if memo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") } if memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } 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) } if tempAttachment == nil { return nil, status.Errorf(codes.NotFound, "attachment not found: %s", attachmentUID) } 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) } if memo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") } // Check memo visibility. if memo.Visibility != store.Public { user, err := s.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } if memo.Visibility == store.Private && memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } } 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, convertAttachmentFromStore(attachment)) } return response, nil } ================================================ FILE: server/router/api/v1/memo_relation_service.go ================================================ 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) { user, err := s.fetchCurrentUser(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, "user not authenticated") } 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") } if memo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") } if memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } 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.fetchCurrentUser(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, status.Errorf(codes.Internal, "failed to list memo relations: %v", 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, status.Errorf(codes.Internal, "failed to list related memo relations: %v", 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, status.Errorf(codes.Internal, "failed to get memo: %v", err) } memoSnippet, err := s.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, status.Errorf(codes.Internal, "failed to get related memo: %v", err) } relatedMemoSnippet, err := s.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_COMMENT: return store.MemoRelationComment default: return store.MemoRelationReference } } ================================================ FILE: server/router/api/v1/memo_service.go ================================================ package v1 import ( "context" "fmt" "log/slog" "strings" "time" "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/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.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user") } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } memoUID, err := ValidateAndGenerateUID(request.MemoId) if err != nil { return nil, err } create := &store.Memo{ UID: memoUID, CreatorID: user.ID, Content: request.Memo.Content, Visibility: convertVisibilityToStore(request.Memo.Visibility), } instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance memo related setting") } // Handle display_time first: if provided, use it to set the appropriate timestamp // based on the instance setting (similar to UpdateMemo logic) // Note: explicit create_time/update_time below will override this if provided if request.Memo.DisplayTime != nil && request.Memo.DisplayTime.IsValid() { displayTs := request.Memo.DisplayTime.AsTime().Unix() if instanceMemoRelatedSetting.DisplayWithUpdateTime { create.UpdatedTs = displayTs } else { create.CreatedTs = displayTs } } // Set custom timestamps if provided in the request // These take precedence over display_time if request.Memo.CreateTime != nil && request.Memo.CreateTime.IsValid() { createdTs := request.Memo.CreateTime.AsTime().Unix() create.CreatedTs = createdTs } if request.Memo.UpdateTime != nil && request.Memo.UpdateTime.IsValid() { updatedTs := request.Memo.UpdateTime.AsTime().Unix() create.UpdatedTs = updatedTs } 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, s.MarkdownService); 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 { // Check for unique constraint violation (AIP-133 compliance) errMsg := err.Error() if strings.Contains(errMsg, "UNIQUE constraint failed") || strings.Contains(errMsg, "duplicate key") || strings.Contains(errMsg, "Duplicate entry") { return nil, status.Errorf(codes.AlreadyExists, "memo with ID %q already exists", memoUID) } return nil, err } attachments := []*store.Attachment{} 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") } a, err := s.Store.ListAttachments(ctx, &store.FindAttachment{ MemoID: &memo.ID, }) if err != nil { return nil, errors.Wrap(err, "failed to get memo attachments") } attachments = a } 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") } } relations, err := s.loadMemoRelations(ctx, memo) if err != nil { return nil, errors.Wrap(err, "failed to load memo relations") } memoMessage, err := s.convertMemoFromStore(ctx, memo, nil, attachments, relations) 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)) } // Broadcast live refresh event. s.SSEHub.Broadcast(&SSEEvent{ Type: SSEEventMemoCreated, Name: memoMessage.Name, }) 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, } currentUser, err := s.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user") } if request.State == v1pb.State_ARCHIVED { state := store.Archived memoFind.RowStatus = &state // Archived memos are only visible to their creator. if currentUser == nil { return &v1pb.ListMemosResponse{}, nil } memoFind.CreatorID = ¤tUser.ID } 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.Filters = append(memoFind.Filters, request.Filter) } if currentUser == nil { memoFind.VisibilityList = []store.Visibility{store.Public} } else { if memoFind.CreatorID == nil { filter := fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID) memoFind.Filters = append(memoFind.Filters, filter) } else if *memoFind.CreatorID != currentUser.ID { memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} } } instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance memo related setting") } if instanceMemoRelatedSetting.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) } } if len(memos) == 0 { response := &v1pb.ListMemosResponse{ Memos: memoMessages, NextPageToken: nextPageToken, } return response, nil } reactionMap := make(map[string][]*store.Reaction) contentIDs := make([]string, 0, len(memos)) attachmentMap := make(map[int32][]*store.Attachment) memoIDs := make([]int32, 0, len(memos)) for _, m := range memos { contentIDs = append(contentIDs, fmt.Sprintf("%s%s", MemoNamePrefix, m.UID)) memoIDs = append(memoIDs, m.ID) } // REACTIONS reactions, err := s.Store.ListReactions(ctx, &store.FindReaction{ContentIDList: contentIDs}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list reactions") } for _, reaction := range reactions { reactionMap[reaction.ContentID] = append(reactionMap[reaction.ContentID], reaction) } // ATTACHMENTS attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoIDList: memoIDs}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list attachments") } for _, attachment := range attachments { attachmentMap[*attachment.MemoID] = append(attachmentMap[*attachment.MemoID], attachment) } // RELATIONS (batch load to avoid N+1) relationMap, err := s.batchConvertMemoRelations(ctx, memos) if err != nil { return nil, status.Errorf(codes.Internal, "failed to batch load memo relations") } for _, memo := range memos { memoName := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID) reactions := reactionMap[memoName] attachments := attachmentMap[memo.ID] relations := relationMap[memo.ID] memoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, relations) 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") } // Archived memos are only visible to their creator. if memo.RowStatus == store.Archived { user, err := s.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user") } if user == nil || memo.CreatorID != user.ID { return nil, status.Errorf(codes.NotFound, "memo not found") } } if memo.Visibility != store.Public { user, err := s.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user") } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } if memo.Visibility == store.Private && memo.CreatorID != user.ID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } } reactions, err := s.Store.ListReactions(ctx, &store.FindReaction{ ContentID: &request.Name, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list reactions") } attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{ MemoID: &memo.ID, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list attachments") } relations, err := s.loadMemoRelations(ctx, memo) if err != nil { return nil, errors.Wrap(err, "failed to load memo relations") } memoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, relations) 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, status.Errorf(codes.Internal, "failed to get memo: %v", err) } if memo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") } user, err := s.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user") } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } // 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, s.MarkdownService); 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" { visibility := convertVisibilityToStore(request.Memo.Visibility) 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.GetInstanceMemoRelatedSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance 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") } reactions, err := s.Store.ListReactions(ctx, &store.FindReaction{ ContentID: &request.Memo.Name, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list reactions") } attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{ MemoID: &memo.ID, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list attachments") } relations, err := s.loadMemoRelations(ctx, memo) if err != nil { return nil, errors.Wrap(err, "failed to load memo relations") } memoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, relations) 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)) } // Broadcast live refresh event. s.SSEHub.Broadcast(&SSEEvent{ Type: SSEEventMemoUpdated, Name: memoMessage.Name, }) 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.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user") } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } // Only the creator or admin can update the memo. if memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } reactions, err := s.Store.ListReactions(ctx, &store.FindReaction{ ContentID: &request.Name, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list reactions") } attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{ MemoID: &memo.ID, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list attachments") } deleteRelations, _ := s.loadMemoRelations(ctx, memo) if memoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, deleteRelations); 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)) } } // Delete memo comments first (store.DeleteMemo handles their relations and attachments) 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 the memo (store.DeleteMemo handles relation and attachment cleanup) if err = s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: memo.ID}); err != nil { return nil, status.Errorf(codes.Internal, "failed to delete memo") } // Broadcast live refresh event. s.SSEHub.Broadcast(&SSEEvent{ Type: SSEEventMemoDeleted, Name: request.Name, }) 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") } if relatedMemo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") } // Check memo visibility before allowing comment. user, err := s.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user") } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } if relatedMemo.Visibility == store.Private && relatedMemo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } // Create the memo comment first. memoComment, err := s.CreateMemo(ctx, &v1pb.CreateMemoRequest{ Memo: request.Comment, MemoId: request.CommentId, }) 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 { if _, err := s.Store.CreateInbox(ctx, &store.Inbox{ SenderID: creatorID, ReceiverID: relatedMemo.CreatorID, Status: store.UNREAD, Message: &storepb.InboxMessage{ Type: storepb.InboxMessage_MEMO_COMMENT, Payload: &storepb.InboxMessage_MemoComment{ MemoComment: &storepb.InboxMessage_MemoCommentPayload{ MemoId: memo.ID, RelatedMemoId: relatedMemo.ID, }, }, }, }); err != nil { return nil, status.Errorf(codes.Internal, "failed to create inbox") } } if err := s.DispatchMemoCommentCreatedWebhook(ctx, memoComment, relatedMemo.CreatorID); err != nil { slog.Warn("Failed to dispatch memo comment created webhook", slog.Any("err", err)) } // Broadcast live refresh event for the parent memo so subscribers see the new comment. s.SSEHub.Broadcast(&SSEEvent{ Type: SSEEventMemoCommentCreated, Name: request.Name, }) 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.fetchCurrentUser(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") } if len(memoRelations) == 0 { response := &v1pb.ListMemoCommentsResponse{ Memos: []*v1pb.Memo{}, } return response, nil } memoRelationIDs := make([]int32, 0, len(memoRelations)) for _, m := range memoRelations { memoRelationIDs = append(memoRelationIDs, m.MemoID) } memos, err := s.Store.ListMemos(ctx, &store.FindMemo{IDList: memoRelationIDs}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list memos") } memoIDToNameMap := make(map[int32]string) contentIDs := make([]string, 0, len(memos)) memoIDsForAttachments := make([]int32, 0, len(memos)) for _, memo := range memos { memoName := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID) memoIDToNameMap[memo.ID] = memoName contentIDs = append(contentIDs, memoName) memoIDsForAttachments = append(memoIDsForAttachments, memo.ID) } reactions, err := s.Store.ListReactions(ctx, &store.FindReaction{ContentIDList: contentIDs}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list reactions") } memoReactionsMap := make(map[string][]*store.Reaction) for _, reaction := range reactions { memoReactionsMap[reaction.ContentID] = append(memoReactionsMap[reaction.ContentID], reaction) } attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoIDList: memoIDsForAttachments}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list attachments") } attachmentMap := make(map[int32][]*store.Attachment) for _, attachment := range attachments { attachmentMap[*attachment.MemoID] = append(attachmentMap[*attachment.MemoID], attachment) } // RELATIONS (batch load to avoid N+1) relationMap, err := s.batchConvertMemoRelations(ctx, memos) if err != nil { return nil, status.Errorf(codes.Internal, "failed to batch load memo relations") } var memosResponse []*v1pb.Memo for _, m := range memos { memoName := memoIDToNameMap[m.ID] reactions := memoReactionsMap[memoName] attachments := attachmentMap[m.ID] relations := relationMap[m.ID] memoMessage, err := s.convertMemoFromStore(ctx, m, reactions, attachments, relations) if err != nil { return nil, errors.Wrap(err, "failed to convert memo") } memosResponse = append(memosResponse, memoMessage) } response := &v1pb.ListMemoCommentsResponse{ Memos: memosResponse, } return response, nil } func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) { instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx) if err != nil { return 0, status.Errorf(codes.Internal, "failed to get instance memo related setting") } return int(instanceMemoRelatedSetting.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") } // DispatchMemoCommentCreatedWebhook dispatches webhook to the related memo owner when a comment is created. func (s *APIV1Service) DispatchMemoCommentCreatedWebhook(ctx context.Context, commentMemo *v1pb.Memo, relatedMemoCreatorID int32) error { webhooks, err := s.Store.GetUserWebhooks(ctx, relatedMemoCreatorID) if err != nil { return err } for _, hook := range webhooks { payload, err := convertMemoToWebhookPayload(commentMemo) if err != nil { return errors.Wrap(err, "failed to convert memo to webhook payload") } payload.ActivityType = "memos.memo.comment.created" payload.URL = hook.Url webhook.PostAsync(payload) } return nil } 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 (s *APIV1Service) getMemoContentSnippet(content string) (string, error) { // Use goldmark service for snippet generation snippet, err := s.MarkdownService.GenerateSnippet([]byte(content), 64) if err != nil { return "", errors.Wrap(err, "failed to generate snippet") } return snippet, nil } // parseMemoOrderBy parses the order_by field and sets the appropriate ordering in memoFind. // Follows AIP-132: supports comma-separated list of fields with optional "desc" suffix. // Example: "pinned desc, display_time desc" or "create_time asc". func (*APIV1Service) parseMemoOrderBy(orderBy string, memoFind *store.FindMemo) error { if strings.TrimSpace(orderBy) == "" { return errors.New("empty order_by") } // Split by comma to support multiple sort fields per AIP-132. fields := strings.Split(orderBy, ",") // Track if we've seen pinned field. hasPinned := false for _, field := range fields { parts := strings.Fields(strings.TrimSpace(field)) if len(parts) == 0 { continue } fieldName := parts[0] fieldDirection := "desc" // default per AIP-132 (we use desc as default for time fields) if len(parts) > 1 { fieldDirection = strings.ToLower(parts[1]) if fieldDirection != "asc" && fieldDirection != "desc" { return errors.Errorf("invalid order direction: %s, must be 'asc' or 'desc'", parts[1]) } } switch fieldName { case "pinned": hasPinned = true memoFind.OrderByPinned = true // Note: pinned is always DESC (true first) regardless of direction specified. case "display_time", "create_time", "name": // Only set if this is the first time field we encounter. if !memoFind.OrderByUpdatedTs { memoFind.OrderByTimeAsc = fieldDirection == "asc" } case "update_time": memoFind.OrderByUpdatedTs = true memoFind.OrderByTimeAsc = fieldDirection == "asc" default: return errors.Errorf("unsupported order field: %s, supported fields are: pinned, display_time, create_time, update_time, name", fieldName) } } // If only pinned was specified, still need to set a default time ordering. if hasPinned && !memoFind.OrderByUpdatedTs && len(fields) == 1 { memoFind.OrderByTimeAsc = false // default to desc } return nil } ================================================ FILE: server/router/api/v1/memo_service_converter.go ================================================ package v1 import ( "context" "fmt" "time" "github.com/pkg/errors" "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) convertMemoFromStore(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation) (*v1pb.Memo, error) { displayTs := memo.CreatedTs instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx) if err != nil { return nil, errors.Wrap(err, "failed to get instance memo related setting") } if instanceMemoRelatedSetting.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.ParentUID != nil { parentName := fmt.Sprintf("%s%s", MemoNamePrefix, *memo.ParentUID) memoMessage.Parent = &parentName } memoMessage.Reactions = []*v1pb.Reaction{} for _, reaction := range reactions { reactionResponse := convertReactionFromStore(reaction) memoMessage.Reactions = append(memoMessage.Reactions, reactionResponse) } if relations != nil { memoMessage.Relations = relations } else { memoMessage.Relations = []*v1pb.MemoRelation{} } memoMessage.Attachments = []*v1pb.Attachment{} for _, attachment := range attachments { attachmentResponse := convertAttachmentFromStore(attachment) memoMessage.Attachments = append(memoMessage.Attachments, attachmentResponse) } snippet, err := s.getMemoContentSnippet(memo.Content) if err != nil { return nil, errors.Wrap(err, "failed to get memo content snippet") } memoMessage.Snippet = snippet return memoMessage, nil } // batchConvertMemoRelations batch-loads relations for a list of memos and returns // a map from memo ID to its converted relations. This avoids N+1 queries when listing memos. func (s *APIV1Service) batchConvertMemoRelations(ctx context.Context, memos []*store.Memo) (map[int32][]*v1pb.MemoRelation, error) { if len(memos) == 0 { return map[int32][]*v1pb.MemoRelation{}, nil } currentUser, err := s.fetchCurrentUser(ctx) if err != nil { return nil, errors.Wrap(err, "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) } memoIDs := make([]int32, len(memos)) memoIDSet := make(map[int32]bool, len(memos)) for i, m := range memos { memoIDs[i] = m.ID memoIDSet[m.ID] = true } // Single batch query to get all relations involving any of these memos. allRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoIDList: memoIDs, MemoFilter: &memoFilter, }) if err != nil { return nil, errors.Wrap(err, "failed to batch list memo relations") } // Collect all memo IDs referenced in relations that we need to resolve. neededIDs := make(map[int32]bool) for _, r := range allRelations { neededIDs[r.MemoID] = true neededIDs[r.RelatedMemoID] = true } // Build ID→UID map from the memos we already have. memoIDToUID := make(map[int32]string, len(memos)) memoIDToContent := make(map[int32]string, len(memos)) for _, m := range memos { memoIDToUID[m.ID] = m.UID memoIDToContent[m.ID] = m.Content delete(neededIDs, m.ID) } // Batch fetch any additional memos referenced by relations that we don't already have. if len(neededIDs) > 0 { extraIDs := make([]int32, 0, len(neededIDs)) for id := range neededIDs { extraIDs = append(extraIDs, id) } extraMemos, err := s.Store.ListMemos(ctx, &store.FindMemo{IDList: extraIDs}) if err != nil { return nil, errors.Wrap(err, "failed to batch fetch related memos") } for _, m := range extraMemos { memoIDToUID[m.ID] = m.UID memoIDToContent[m.ID] = m.Content } } // Build the result map: memo ID → its relations (both directions). result := make(map[int32][]*v1pb.MemoRelation, len(memos)) for _, r := range allRelations { memoUID, ok1 := memoIDToUID[r.MemoID] relatedUID, ok2 := memoIDToUID[r.RelatedMemoID] if !ok1 || !ok2 { continue } memoSnippet, _ := s.getMemoContentSnippet(memoIDToContent[r.MemoID]) relatedSnippet, _ := s.getMemoContentSnippet(memoIDToContent[r.RelatedMemoID]) relation := &v1pb.MemoRelation{ Memo: &v1pb.MemoRelation_Memo{ Name: fmt.Sprintf("%s%s", MemoNamePrefix, memoUID), Snippet: memoSnippet, }, RelatedMemo: &v1pb.MemoRelation_Memo{ Name: fmt.Sprintf("%s%s", MemoNamePrefix, relatedUID), Snippet: relatedSnippet, }, Type: convertMemoRelationTypeFromStore(r.Type), } // Add to the memo that owns this relation (both directions). if memoIDSet[r.MemoID] { result[r.MemoID] = append(result[r.MemoID], relation) } if memoIDSet[r.RelatedMemoID] { result[r.RelatedMemoID] = append(result[r.RelatedMemoID], relation) } } return result, nil } // loadMemoRelations loads relations for a single memo and converts them to API format. func (s *APIV1Service) loadMemoRelations(ctx context.Context, memo *store.Memo) ([]*v1pb.MemoRelation, error) { relationMap, err := s.batchConvertMemoRelations(ctx, []*store.Memo{memo}) if err != nil { return nil, err } return relationMap[memo.ID], 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, Title: property.Title, } } 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_PROTECTED: return store.Protected case v1pb.Visibility_PUBLIC: return store.Public default: return store.Private } } ================================================ FILE: server/router/api/v1/memo_service_filter.go ================================================ package v1 ================================================ FILE: server/router/api/v1/memo_share_service.go ================================================ 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" "github.com/lithammer/shortuuid/v4" "github.com/pkg/errors" v1pb "github.com/usememos/memos/proto/gen/api/v1" "github.com/usememos/memos/store" ) // CreateMemoShare creates an opaque share link for a memo. // Only the memo's creator or an admin may call this. func (s *APIV1Service) CreateMemoShare(ctx context.Context, request *v1pb.CreateMemoShareRequest) (*v1pb.MemoShare, error) { user, err := s.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user") } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } memoUID, err := ExtractMemoUIDFromName(request.Parent) 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") } if memo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") } if memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } var expiresTs *int64 if request.MemoShare != nil && request.MemoShare.ExpireTime != nil { ts := request.MemoShare.ExpireTime.AsTime().Unix() if ts <= time.Now().Unix() { return nil, status.Errorf(codes.InvalidArgument, "expire_time must be in the future") } expiresTs = &ts } // Generate a URL-safe token using shortuuid (base57-encoded UUID v4, 22 chars, 122-bit entropy). ms, err := s.Store.CreateMemoShare(ctx, &store.MemoShare{ UID: shortuuid.New(), MemoID: memo.ID, CreatorID: user.ID, ExpiresTs: expiresTs, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to create memo share") } return convertMemoShareFromStore(ms, memo.UID), nil } // ListMemoShares lists all share links for a memo. // Only the memo's creator or an admin may call this. func (s *APIV1Service) ListMemoShares(ctx context.Context, request *v1pb.ListMemoSharesRequest) (*v1pb.ListMemoSharesResponse, error) { user, err := s.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user") } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } memoUID, err := ExtractMemoUIDFromName(request.Parent) 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") } if memo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") } if memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } shares, err := s.Store.ListMemoShares(ctx, &store.FindMemoShare{MemoID: &memo.ID}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list memo shares") } response := &v1pb.ListMemoSharesResponse{} for _, ms := range shares { response.MemoShares = append(response.MemoShares, convertMemoShareFromStore(ms, memo.UID)) } return response, nil } // DeleteMemoShare revokes a share link. // Only the memo's creator or an admin may call this. func (s *APIV1Service) DeleteMemoShare(ctx context.Context, request *v1pb.DeleteMemoShareRequest) (*emptypb.Empty, error) { user, err := s.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user") } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } // name format: memos/{memoUID}/shares/{shareToken} tokens, err := GetNameParentTokens(request.Name, MemoNamePrefix, MemoShareNamePrefix) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid share name: %v", err) } memoUID, shareToken := tokens[0], tokens[1] memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get memo") } if memo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") } if memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } ms, err := s.Store.GetMemoShare(ctx, &store.FindMemoShare{UID: &shareToken}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get memo share") } if ms == nil || ms.MemoID != memo.ID { return nil, status.Errorf(codes.NotFound, "memo share not found") } if err := s.Store.DeleteMemoShare(ctx, &store.DeleteMemoShare{UID: &shareToken}); err != nil { return nil, status.Errorf(codes.Internal, "failed to delete memo share") } return &emptypb.Empty{}, nil } // GetMemoByShare resolves a share token to its memo. No authentication required. // Returns NOT_FOUND for invalid or expired tokens (no information leakage). func (s *APIV1Service) GetMemoByShare(ctx context.Context, request *v1pb.GetMemoByShareRequest) (*v1pb.Memo, error) { ms, err := s.getActiveMemoShare(ctx, request.ShareId) if err != nil { return nil, err } memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &ms.MemoID}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get memo") } // Treat archived or missing memos the same as an invalid token — no information leakage. if memo == nil || memo.RowStatus == store.Archived { return nil, status.Errorf(codes.NotFound, "not found") } reactions, err := s.Store.ListReactions(ctx, &store.FindReaction{ ContentID: stringPointer(fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)), }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list reactions") } attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoID: &memo.ID}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list attachments") } relations, err := s.batchConvertMemoRelations(ctx, []*store.Memo{memo}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to load memo relations") } memoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, relations[memo.ID]) if err != nil { return nil, errors.Wrap(err, "failed to convert memo") } return memoMessage, nil } // isMemoShareExpired returns true if the share has a defined expiry that has already passed. func isMemoShareExpired(ms *store.MemoShare) bool { return ms.ExpiresTs != nil && time.Now().Unix() > *ms.ExpiresTs } func (s *APIV1Service) getActiveMemoShare(ctx context.Context, shareID string) (*store.MemoShare, error) { ms, err := s.Store.GetMemoShare(ctx, &store.FindMemoShare{UID: &shareID}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get memo share") } if ms == nil || isMemoShareExpired(ms) { return nil, status.Errorf(codes.NotFound, "not found") } return ms, nil } func stringPointer(s string) *string { return &s } // convertMemoShareFromStore converts a store MemoShare to the proto MemoShare message. // name format: memos/{memoUID}/shares/{shareToken}. func convertMemoShareFromStore(ms *store.MemoShare, memoUID string) *v1pb.MemoShare { name := fmt.Sprintf("%s%s/%s%s", MemoNamePrefix, memoUID, MemoShareNamePrefix, ms.UID) pb := &v1pb.MemoShare{ Name: name, CreateTime: timestamppb.New(time.Unix(ms.CreatedTs, 0)), } if ms.ExpiresTs != nil { pb.ExpireTime = timestamppb.New(time.Unix(*ms.ExpiresTs, 0)) } return pb } ================================================ FILE: server/router/api/v1/reaction_service.go ================================================ 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) { // Extract memo UID and check visibility. 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) } if memo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") } // Check memo visibility. if memo.Visibility != store.Public { user, err := s.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user") } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } if memo.Visibility == store.Private && memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } } 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 := convertReactionFromStore(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.fetchCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user") } if user == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } // Extract memo UID and check visibility before allowing reaction. memoUID, err := ExtractMemoUIDFromName(request.Reaction.ContentId) 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) } if memo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") } // Check memo visibility. if memo.Visibility == store.Private && memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } 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 := convertReactionFromStore(reaction) // Broadcast live refresh event (reaction belongs to a memo). s.SSEHub.Broadcast(&SSEEvent{ Type: SSEEventReactionUpserted, Name: request.Reaction.ContentId, }) return reactionMessage, nil } func (s *APIV1Service) DeleteMemoReaction(ctx context.Context, request *v1pb.DeleteMemoReactionRequest) (*emptypb.Empty, error) { user, err := s.fetchCurrentUser(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, "user not authenticated") } _, reactionID, err := ExtractMemoReactionIDFromName(request.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid reaction name: %v", err) } // Get reaction and check ownership. reaction, err := s.Store.GetReaction(ctx, &store.FindReaction{ ID: &reactionID, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get reaction") } if reaction == nil { // Return permission denied to avoid revealing if reaction exists. return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if reaction.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if err := s.Store.DeleteReaction(ctx, &store.DeleteReaction{ ID: reactionID, }); err != nil { return nil, status.Errorf(codes.Internal, "failed to delete reaction") } // Broadcast live refresh event (reaction belongs to a memo). s.SSEHub.Broadcast(&SSEEvent{ Type: SSEEventReactionDeleted, Name: reaction.ContentID, }) return &emptypb.Empty{}, nil } func convertReactionFromStore(reaction *store.Reaction) *v1pb.Reaction { reactionUID := fmt.Sprintf("%d", reaction.ID) // Generate nested resource name: memos/{memo}/reactions/{reaction} // reaction.ContentID already contains "memos/{memo}" return &v1pb.Reaction{ Name: fmt.Sprintf("%s/%s%s", reaction.ContentID, ReactionNamePrefix, reactionUID), Creator: fmt.Sprintf("%s%d", UserNamePrefix, reaction.CreatorID), ContentId: reaction.ContentID, ReactionType: reaction.ReactionType, CreateTime: timestamppb.New(time.Unix(reaction.CreatedTs, 0)), } } ================================================ FILE: server/router/api/v1/resource_name.go ================================================ package v1 import ( "fmt" "strings" "github.com/lithammer/shortuuid/v4" "github.com/pkg/errors" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/usememos/memos/internal/base" "github.com/usememos/memos/internal/util" ) const ( InstanceSettingNamePrefix = "instance/settings/" UserNamePrefix = "users/" MemoNamePrefix = "memos/" MemoShareNamePrefix = "shares/" AttachmentNamePrefix = "attachments/" ReactionNamePrefix = "reactions/" InboxNamePrefix = "inboxes/" IdentityProviderNamePrefix = "identity-providers/" 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 ExtractInstanceSettingKeyFromName(name string) (string, error) { const prefix = "instance/settings/" if !strings.HasPrefix(name, prefix) { return "", errors.Errorf("invalid instance setting name: expected prefix %q, got %q", prefix, name) } settingKey := strings.TrimPrefix(name, prefix) if settingKey == "" { return "", errors.Errorf("invalid instance setting name: empty setting key in %q", name) } // Ensure there are no additional path segments if strings.Contains(settingKey, "/") { return "", errors.Errorf("invalid instance 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 } // extractUserIdentifierFromName extracts the identifier (ID or username) from a user resource name. // Supports: "users/101" or "users/steven" // Returns the identifier string (e.g., "101" or "steven"). func extractUserIdentifierFromName(name string) string { tokens, err := GetNameParentTokens(name, UserNamePrefix) if err != nil || len(tokens) == 0 { return "" } return tokens[0] } // 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 } // ExtractMemoReactionIDFromName returns the memo UID and reaction ID from a resource name. // e.g., "memos/abc/reactions/123" -> ("abc", 123). func ExtractMemoReactionIDFromName(name string) (string, int32, error) { tokens, err := GetNameParentTokens(name, MemoNamePrefix, ReactionNamePrefix) if err != nil { return "", 0, err } memoUID := tokens[0] reactionID, err := util.ConvertStringToInt32(tokens[1]) if err != nil { return "", 0, errors.Errorf("invalid reaction ID %q", tokens[1]) } return memoUID, reactionID, 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 ExtractIdentityProviderUIDFromName(name string) (string, error) { tokens, err := GetNameParentTokens(name, IdentityProviderNamePrefix) if err != nil { return "", err } return tokens[0], nil } // ValidateAndGenerateUID validates a user-provided UID or generates a new one. // If provided is empty, a new shortuuid is generated. // If provided is non-empty, it is validated against base.UIDMatcher. func ValidateAndGenerateUID(provided string) (string, error) { uid := strings.TrimSpace(provided) if uid == "" { return shortuuid.New(), nil } if !base.UIDMatcher.MatchString(uid) { return "", status.Errorf(codes.InvalidArgument, "invalid ID format: must be 1-32 characters, alphanumeric and hyphens only, cannot start or end with hyphen") } return uid, nil } ================================================ FILE: server/router/api/v1/shortcut_service.go ================================================ 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.fetchCurrentUser(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, status.Errorf(codes.Internal, "failed to get user setting: %v", 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.fetchCurrentUser(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.fetchCurrentUser(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, status.Errorf(codes.Internal, "failed to upsert user setting: %v", 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.fetchCurrentUser(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.fetchCurrentUser(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, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err) } return &emptypb.Empty{}, nil } func (s *APIV1Service) validateFilter(ctx context.Context, filterStr string) error { if filterStr == "" { return errors.New("filter cannot be empty") } engine, err := filter.DefaultEngine() if err != nil { return err } var dialect filter.DialectName switch s.Profile.Driver { case "mysql": dialect = filter.DialectMySQL case "postgres": dialect = filter.DialectPostgres default: dialect = filter.DialectSQLite } if _, err := engine.CompileToStatement(ctx, filterStr, filter.RenderOptions{Dialect: dialect}); err != nil { return errors.Wrap(err, "failed to compile filter") } return nil } ================================================ FILE: server/router/api/v1/sse_handler.go ================================================ package v1 import ( "fmt" "log/slog" "net/http" "time" "github.com/labstack/echo/v5" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) const ( // sseHeartbeatInterval is the interval between heartbeat pings to keep the connection alive. sseHeartbeatInterval = 30 * time.Second ) // RegisterSSERoutes registers the SSE endpoint on the given Echo instance. func RegisterSSERoutes(echoServer *echo.Echo, hub *SSEHub, storeInstance *store.Store, secret string) { authenticator := auth.NewAuthenticator(storeInstance, secret) echoServer.GET("/api/v1/sse", func(c *echo.Context) error { return handleSSE(c, hub, authenticator) }) } // handleSSE handles the SSE connection for live memo refresh. // Authentication is done via Bearer token in the Authorization header. func handleSSE(c *echo.Context, hub *SSEHub, authenticator *auth.Authenticator) error { // Authenticate the request. authHeader := c.Request().Header.Get("Authorization") result := authenticator.Authenticate(c.Request().Context(), authHeader) if result == nil { return c.JSON(http.StatusUnauthorized, map[string]string{"error": "authentication required"}) } // Set SSE headers. w := c.Response() w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") // Disable nginx buffering w.WriteHeader(http.StatusOK) // Flush headers immediately. if f, ok := w.(http.Flusher); ok { f.Flush() } // Subscribe to the hub. client := hub.Subscribe() defer hub.Unsubscribe(client) // Create a ticker for heartbeat pings. heartbeat := time.NewTicker(sseHeartbeatInterval) defer heartbeat.Stop() ctx := c.Request().Context() slog.Debug("SSE client connected") for { select { case <-ctx.Done(): // Client disconnected. slog.Debug("SSE client disconnected") return nil case data, ok := <-client.events: if !ok { // Channel closed, client was unsubscribed. return nil } // Write SSE event. if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil { return nil } if f, ok := w.(http.Flusher); ok { f.Flush() } case <-heartbeat.C: // Send a heartbeat comment to keep the connection alive. if _, err := fmt.Fprint(w, ": heartbeat\n\n"); err != nil { return nil } if f, ok := w.(http.Flusher); ok { f.Flush() } } } } ================================================ FILE: server/router/api/v1/sse_hub.go ================================================ package v1 import ( "encoding/json" "log/slog" "sync" ) // SSEEventType represents the type of change event. type SSEEventType string const ( SSEEventMemoCreated SSEEventType = "memo.created" SSEEventMemoUpdated SSEEventType = "memo.updated" SSEEventMemoDeleted SSEEventType = "memo.deleted" SSEEventMemoCommentCreated SSEEventType = "memo.comment.created" SSEEventReactionUpserted SSEEventType = "reaction.upserted" SSEEventReactionDeleted SSEEventType = "reaction.deleted" ) // SSEEvent represents a change event sent to SSE clients. type SSEEvent struct { Type SSEEventType `json:"type"` // Name is the affected resource name (e.g., "memos/xxxx"). // For reaction events, this is the memo resource name that the reaction belongs to. Name string `json:"name"` } // JSON returns the JSON representation of the event. // Returns nil if marshaling fails (error is logged). func (e *SSEEvent) JSON() []byte { data, err := json.Marshal(e) if err != nil { slog.Error("failed to marshal SSE event", "err", err, "event", e) return nil } return data } // SSEClient represents a single SSE connection. type SSEClient struct { events chan []byte } // SSEHub manages SSE client connections and broadcasts events. // It is safe for concurrent use. type SSEHub struct { mu sync.RWMutex clients map[*SSEClient]struct{} } // NewSSEHub creates a new SSE hub. func NewSSEHub() *SSEHub { return &SSEHub{ clients: make(map[*SSEClient]struct{}), } } // Subscribe registers a new client and returns it. // The caller must call Unsubscribe when done. func (h *SSEHub) Subscribe() *SSEClient { c := &SSEClient{ // Buffer a few events so a slow client doesn't block broadcasting. events: make(chan []byte, 32), } h.mu.Lock() h.clients[c] = struct{}{} h.mu.Unlock() return c } // Unsubscribe removes a client and closes its channel. func (h *SSEHub) Unsubscribe(c *SSEClient) { h.mu.Lock() if _, ok := h.clients[c]; ok { delete(h.clients, c) close(c.events) } h.mu.Unlock() } // Broadcast sends an event to all connected clients. // Slow clients that have a full buffer will have the event dropped // to avoid blocking the broadcaster. func (h *SSEHub) Broadcast(event *SSEEvent) { data := event.JSON() if len(data) == 0 { return } h.mu.RLock() defer h.mu.RUnlock() for c := range h.clients { select { case c.events <- data: default: // Drop event for slow client to avoid blocking. } } } ================================================ FILE: server/router/api/v1/sse_hub_test.go ================================================ package v1 import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSSEHub_SubscribeUnsubscribe(t *testing.T) { hub := NewSSEHub() client := hub.Subscribe() require.NotNil(t, client) require.NotNil(t, client.events) // Unsubscribe removes the client and closes the channel. hub.Unsubscribe(client) // Channel should be closed. _, ok := <-client.events assert.False(t, ok, "channel should be closed after Unsubscribe") } func TestSSEHub_Broadcast(t *testing.T) { hub := NewSSEHub() client := hub.Subscribe() defer hub.Unsubscribe(client) event := &SSEEvent{Type: SSEEventMemoCreated, Name: "memos/123"} hub.Broadcast(event) select { case data := <-client.events: assert.Contains(t, string(data), `"type":"memo.created"`) assert.Contains(t, string(data), `"name":"memos/123"`) case <-time.After(time.Second): t.Fatal("expected to receive event within 1s") } } func TestSSEHub_BroadcastMultipleClients(t *testing.T) { hub := NewSSEHub() c1 := hub.Subscribe() defer hub.Unsubscribe(c1) c2 := hub.Subscribe() defer hub.Unsubscribe(c2) event := &SSEEvent{Type: SSEEventMemoDeleted, Name: "memos/456"} hub.Broadcast(event) for _, ch := range []chan []byte{c1.events, c2.events} { select { case data := <-ch: assert.Contains(t, string(data), "memo.deleted") assert.Contains(t, string(data), "memos/456") case <-time.After(time.Second): t.Fatal("expected to receive event within 1s") } } } func TestSSEEvent_JSON(t *testing.T) { e := &SSEEvent{Type: SSEEventMemoUpdated, Name: "memos/789"} data := e.JSON() require.NotEmpty(t, data) assert.Contains(t, string(data), `"type":"memo.updated"`) assert.Contains(t, string(data), `"name":"memos/789"`) } ================================================ FILE: server/router/api/v1/test/attachment_service_test.go ================================================ package test import ( "context" "testing" "github.com/stretchr/testify/require" v1pb "github.com/usememos/memos/proto/gen/api/v1" ) func TestCreateAttachment(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() ctx := context.Background() user, err := ts.CreateRegularUser(ctx, "test_user") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) // Test case 1: Create attachment with empty type but known extension t.Run("EmptyType_KnownExtension", func(t *testing.T) { attachment, err := ts.Service.CreateAttachment(userCtx, &v1pb.CreateAttachmentRequest{ Attachment: &v1pb.Attachment{ Filename: "test.png", Content: []byte("fake png content"), }, }) require.NoError(t, err) require.Equal(t, "image/png", attachment.Type) }) // Test case 2: Create attachment with empty type and unknown extension, but detectable content t.Run("EmptyType_UnknownExtension_ContentSniffing", func(t *testing.T) { // PNG magic header: 89 50 4E 47 0D 0A 1A 0A pngContent := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} attachment, err := ts.Service.CreateAttachment(userCtx, &v1pb.CreateAttachmentRequest{ Attachment: &v1pb.Attachment{ Filename: "test.unknown", Content: pngContent, }, }) require.NoError(t, err) require.Equal(t, "image/png", attachment.Type) }) // Test case 3: Empty type, unknown extension, random content -> fallback to application/octet-stream t.Run("EmptyType_Fallback", func(t *testing.T) { randomContent := []byte{0x00, 0x01, 0x02, 0x03} attachment, err := ts.Service.CreateAttachment(userCtx, &v1pb.CreateAttachmentRequest{ Attachment: &v1pb.Attachment{ Filename: "test.data", Content: randomContent, }, }) require.NoError(t, err) require.Equal(t, "application/octet-stream", attachment.Type) }) } ================================================ FILE: server/router/api/v1/test/auth_test.go ================================================ package test import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "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/server/auth" "github.com/usememos/memos/store" ) func TestAuthenticatorAccessTokenV2(t *testing.T) { ctx := context.Background() t.Run("authenticates valid access token v2", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create a test user user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Generate access token v2 token, _, err := auth.GenerateAccessTokenV2( user.ID, user.Username, string(user.Role), string(user.RowStatus), []byte(ts.Secret), ) require.NoError(t, err) // Authenticate authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) claims, err := authenticator.AuthenticateByAccessTokenV2(token) require.NoError(t, err) assert.NotNil(t, claims) assert.Equal(t, user.ID, claims.UserID) assert.Equal(t, user.Username, claims.Username) assert.Equal(t, string(user.Role), claims.Role) assert.Equal(t, string(user.RowStatus), claims.Status) }) t.Run("fails with invalid token", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) _, err := authenticator.AuthenticateByAccessTokenV2("invalid-token") assert.Error(t, err) }) t.Run("fails with wrong secret", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Generate token with one secret token, _, err := auth.GenerateAccessTokenV2( user.ID, user.Username, string(user.Role), string(user.RowStatus), []byte("secret-1"), ) require.NoError(t, err) // Try to authenticate with different secret authenticator := auth.NewAuthenticator(ts.Store, "secret-2") _, err = authenticator.AuthenticateByAccessTokenV2(token) assert.Error(t, err) }) } func TestAuthenticatorRefreshToken(t *testing.T) { ctx := context.Background() t.Run("authenticates valid refresh token", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create a test user user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Create refresh token record in store tokenID := util.GenUUID() refreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: tokenID, ExpiresAt: timestamppb.New(time.Now().Add(auth.RefreshTokenDuration)), CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserRefreshToken(ctx, user.ID, refreshTokenRecord) require.NoError(t, err) // Generate refresh token JWT token, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret)) require.NoError(t, err) // Authenticate authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) authenticatedUser, returnedTokenID, err := authenticator.AuthenticateByRefreshToken(ctx, token) require.NoError(t, err) assert.NotNil(t, authenticatedUser) assert.Equal(t, user.ID, authenticatedUser.ID) assert.Equal(t, tokenID, returnedTokenID) }) t.Run("fails with revoked token", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) tokenID := util.GenUUID() // Generate refresh token JWT but don't store it in database (simulates revocation) token, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret)) require.NoError(t, err) // Try to authenticate authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) _, _, err = authenticator.AuthenticateByRefreshToken(ctx, token) assert.Error(t, err) assert.Contains(t, err.Error(), "revoked") }) t.Run("fails with expired token", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Create expired refresh token record in store tokenID := util.GenUUID() expiredToken := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: tokenID, ExpiresAt: timestamppb.New(time.Now().Add(-1 * time.Hour)), // Expired CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserRefreshToken(ctx, user.ID, expiredToken) require.NoError(t, err) // Generate refresh token JWT (JWT itself isn't expired yet) token, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret)) require.NoError(t, err) // Try to authenticate authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) _, _, err = authenticator.AuthenticateByRefreshToken(ctx, token) assert.Error(t, err) assert.Contains(t, err.Error(), "expired") }) t.Run("fails with archived user", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Create valid refresh token tokenID := util.GenUUID() refreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: tokenID, ExpiresAt: timestamppb.New(time.Now().Add(auth.RefreshTokenDuration)), CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserRefreshToken(ctx, user.ID, refreshTokenRecord) require.NoError(t, err) token, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret)) require.NoError(t, err) // Archive the user archivedStatus := store.Archived _, err = ts.Store.UpdateUser(ctx, &store.UpdateUser{ ID: user.ID, RowStatus: &archivedStatus, }) require.NoError(t, err) // Try to authenticate authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) _, _, err = authenticator.AuthenticateByRefreshToken(ctx, token) assert.Error(t, err) assert.Contains(t, err.Error(), "archived") }) } func TestAuthenticatorPAT(t *testing.T) { ctx := context.Background() t.Run("authenticates valid PAT", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create a test user user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Generate PAT token := auth.GeneratePersonalAccessToken() tokenHash := auth.HashPersonalAccessToken(token) tokenID := util.GenUUID() // Store PAT in database patRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID, TokenHash: tokenHash, Description: "Test PAT", CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, patRecord) require.NoError(t, err) // Authenticate authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) authenticatedUser, pat, err := authenticator.AuthenticateByPAT(ctx, token) require.NoError(t, err) assert.NotNil(t, authenticatedUser) assert.NotNil(t, pat) assert.Equal(t, user.ID, authenticatedUser.ID) assert.Equal(t, tokenID, pat.TokenId) }) t.Run("fails with invalid PAT format", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) _, _, err := authenticator.AuthenticateByPAT(ctx, "invalid-token-without-prefix") assert.Error(t, err) assert.Contains(t, err.Error(), "invalid PAT format") }) t.Run("fails with non-existent PAT", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Generate a PAT but don't store it token := auth.GeneratePersonalAccessToken() authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) _, _, err := authenticator.AuthenticateByPAT(ctx, token) assert.Error(t, err) }) t.Run("fails with expired PAT", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Generate and store expired PAT token := auth.GeneratePersonalAccessToken() tokenHash := auth.HashPersonalAccessToken(token) tokenID := util.GenUUID() expiredPAT := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID, TokenHash: tokenHash, Description: "Expired PAT", ExpiresAt: timestamppb.New(time.Now().Add(-1 * time.Hour)), // Expired CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, expiredPAT) require.NoError(t, err) // Try to authenticate authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) _, _, err = authenticator.AuthenticateByPAT(ctx, token) assert.Error(t, err) assert.Contains(t, err.Error(), "expired") }) t.Run("succeeds with non-expiring PAT", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Generate and store PAT without expiration token := auth.GeneratePersonalAccessToken() tokenHash := auth.HashPersonalAccessToken(token) tokenID := util.GenUUID() patRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID, TokenHash: tokenHash, Description: "Never-expiring PAT", ExpiresAt: nil, // No expiration CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, patRecord) require.NoError(t, err) // Authenticate authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) authenticatedUser, pat, err := authenticator.AuthenticateByPAT(ctx, token) require.NoError(t, err) assert.NotNil(t, authenticatedUser) assert.NotNil(t, pat) assert.Nil(t, pat.ExpiresAt) }) t.Run("fails with archived user", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Generate and store PAT token := auth.GeneratePersonalAccessToken() tokenHash := auth.HashPersonalAccessToken(token) tokenID := util.GenUUID() patRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID, TokenHash: tokenHash, Description: "Test PAT", CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, patRecord) require.NoError(t, err) // Archive the user archivedStatus := store.Archived _, err = ts.Store.UpdateUser(ctx, &store.UpdateUser{ ID: user.ID, RowStatus: &archivedStatus, }) require.NoError(t, err) // Try to authenticate authenticator := auth.NewAuthenticator(ts.Store, ts.Secret) _, _, err = authenticator.AuthenticateByPAT(ctx, token) assert.Error(t, err) assert.Contains(t, err.Error(), "archived") }) } func TestStoreRefreshTokenMethods(t *testing.T) { ctx := context.Background() t.Run("adds and retrieves refresh token", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) tokenID := util.GenUUID() token := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: tokenID, ExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)), CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserRefreshToken(ctx, user.ID, token) require.NoError(t, err) // Retrieve tokens tokens, err := ts.Store.GetUserRefreshTokens(ctx, user.ID) require.NoError(t, err) assert.Len(t, tokens, 1) assert.Equal(t, tokenID, tokens[0].TokenId) }) t.Run("retrieves specific refresh token by ID", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) tokenID := util.GenUUID() token := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: tokenID, ExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)), CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserRefreshToken(ctx, user.ID, token) require.NoError(t, err) // Retrieve specific token retrievedToken, err := ts.Store.GetUserRefreshTokenByID(ctx, user.ID, tokenID) require.NoError(t, err) assert.NotNil(t, retrievedToken) assert.Equal(t, tokenID, retrievedToken.TokenId) }) t.Run("removes refresh token", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) tokenID := util.GenUUID() token := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: tokenID, ExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)), CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserRefreshToken(ctx, user.ID, token) require.NoError(t, err) // Remove token err = ts.Store.RemoveUserRefreshToken(ctx, user.ID, tokenID) require.NoError(t, err) // Verify removal tokens, err := ts.Store.GetUserRefreshTokens(ctx, user.ID) require.NoError(t, err) assert.Len(t, tokens, 0) }) t.Run("handles multiple refresh tokens", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Add multiple tokens tokenID1 := util.GenUUID() tokenID2 := util.GenUUID() token1 := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: tokenID1, ExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)), CreatedAt: timestamppb.Now(), } token2 := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: tokenID2, ExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)), CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserRefreshToken(ctx, user.ID, token1) require.NoError(t, err) err = ts.Store.AddUserRefreshToken(ctx, user.ID, token2) require.NoError(t, err) // Retrieve all tokens tokens, err := ts.Store.GetUserRefreshTokens(ctx, user.ID) require.NoError(t, err) assert.Len(t, tokens, 2) // Remove one token err = ts.Store.RemoveUserRefreshToken(ctx, user.ID, tokenID1) require.NoError(t, err) // Verify only one token remains tokens, err = ts.Store.GetUserRefreshTokens(ctx, user.ID) require.NoError(t, err) assert.Len(t, tokens, 1) assert.Equal(t, tokenID2, tokens[0].TokenId) }) } func TestStorePersonalAccessTokenMethods(t *testing.T) { ctx := context.Background() t.Run("adds and retrieves PAT", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) token := auth.GeneratePersonalAccessToken() tokenHash := auth.HashPersonalAccessToken(token) tokenID := util.GenUUID() pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID, TokenHash: tokenHash, Description: "Test PAT", CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat) require.NoError(t, err) // Retrieve PATs pats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) assert.Len(t, pats, 1) assert.Equal(t, tokenID, pats[0].TokenId) assert.Equal(t, tokenHash, pats[0].TokenHash) }) t.Run("removes PAT", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) token := auth.GeneratePersonalAccessToken() tokenHash := auth.HashPersonalAccessToken(token) tokenID := util.GenUUID() pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID, TokenHash: tokenHash, Description: "Test PAT", CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat) require.NoError(t, err) // Remove PAT err = ts.Store.RemoveUserPersonalAccessToken(ctx, user.ID, tokenID) require.NoError(t, err) // Verify removal pats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) assert.Len(t, pats, 0) }) t.Run("updates PAT last used time", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) token := auth.GeneratePersonalAccessToken() tokenHash := auth.HashPersonalAccessToken(token) tokenID := util.GenUUID() pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID, TokenHash: tokenHash, Description: "Test PAT", CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat) require.NoError(t, err) // Update last used time lastUsed := timestamppb.Now() err = ts.Store.UpdatePATLastUsed(ctx, user.ID, tokenID, lastUsed) require.NoError(t, err) // Verify update pats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) assert.Len(t, pats, 1) assert.NotNil(t, pats[0].LastUsedAt) }) t.Run("handles multiple PATs", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) // Add multiple PATs token1 := auth.GeneratePersonalAccessToken() tokenHash1 := auth.HashPersonalAccessToken(token1) tokenID1 := util.GenUUID() token2 := auth.GeneratePersonalAccessToken() tokenHash2 := auth.HashPersonalAccessToken(token2) tokenID2 := util.GenUUID() pat1 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID1, TokenHash: tokenHash1, Description: "PAT 1", CreatedAt: timestamppb.Now(), } pat2 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID2, TokenHash: tokenHash2, Description: "PAT 2", CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat1) require.NoError(t, err) err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat2) require.NoError(t, err) // Retrieve all PATs pats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) assert.Len(t, pats, 2) // Remove one PAT err = ts.Store.RemoveUserPersonalAccessToken(ctx, user.ID, tokenID1) require.NoError(t, err) // Verify only one PAT remains pats, err = ts.Store.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) assert.Len(t, pats, 1) assert.Equal(t, tokenID2, pats[0].TokenId) }) t.Run("finds user by PAT hash", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "testuser") require.NoError(t, err) token := auth.GeneratePersonalAccessToken() tokenHash := auth.HashPersonalAccessToken(token) tokenID := util.GenUUID() pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID, TokenHash: tokenHash, Description: "Test PAT", CreatedAt: timestamppb.Now(), } err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat) require.NoError(t, err) // Find user by PAT hash result, err := ts.Store.GetUserByPATHash(ctx, tokenHash) require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, user.ID, result.UserID) assert.NotNil(t, result.User) assert.Equal(t, user.Username, result.User.Username) assert.NotNil(t, result.PAT) assert.Equal(t, tokenID, result.PAT.TokenId) }) } ================================================ FILE: server/router/api/v1/test/idp_service_test.go ================================================ package test 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, "identity-providers/") 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(), "user not authenticated") }) } 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, } // Test unauthenticated, should not contain client secret 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, "", resp.Config.GetOauth2Config().ClientSecret) // Test as host user, should contain client secret respHostUser, err := ts.Service.GetIdentityProvider(userCtx, getReq) require.NoError(t, err) require.NotNil(t, respHostUser) require.Equal(t, created.Name, respHostUser.Name) require.Equal(t, "Test Provider", respHostUser.Title) require.Equal(t, v1pb.IdentityProvider_OAUTH2, respHostUser.Type) require.NotNil(t, respHostUser.Config.GetOauth2Config()) require.Equal(t, "test-client", respHostUser.Config.GetOauth2Config().ClientId) require.Equal(t, "test-secret", respHostUser.Config.GetOauth2Config().ClientSecret) }) t.Run("GetIdentityProvider not found", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() req := &v1pb.GetIdentityProviderRequest{ Name: "identity-providers/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() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, hostUser.ID) req := &v1pb.UpdateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Name: "identity-providers/1", Title: "Updated Provider", }, } _, err = ts.Service.UpdateIdentityProvider(userCtx, 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() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, hostUser.ID) req := &v1pb.UpdateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Name: "invalid-name", Title: "Updated Provider", }, UpdateMask: &fieldmaskpb.FieldMask{ Paths: []string{"title"}, }, } _, err = ts.Service.UpdateIdentityProvider(userCtx, 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() // 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: "invalid-name", } _, err = ts.Service.DeleteIdentityProvider(userCtx, 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: "identity-providers/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(), "user not authenticated") }) } ================================================ FILE: server/router/api/v1/test/instance_admin_cache_test.go ================================================ package test import ( "context" "testing" "github.com/stretchr/testify/require" v1pb "github.com/usememos/memos/proto/gen/api/v1" ) func TestInstanceAdminRetrieval(t *testing.T) { ctx := context.Background() t.Run("Instance becomes initialized after first admin user is created", func(t *testing.T) { // Create test service ts := NewTestService(t) defer ts.Cleanup() // Verify instance is not initialized initially profile1, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) require.NoError(t, err) require.Nil(t, profile1.Admin, "Instance should not be initialized before first admin user") // Create the first admin user user, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) require.NotNil(t, user) // Verify instance is now initialized profile2, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) require.NoError(t, err) require.NotNil(t, profile2.Admin, "Instance should be initialized after first admin user is created") require.Equal(t, user.Username, profile2.Admin.Username) }) t.Run("Admin retrieval is cached by Store layer", func(t *testing.T) { // Create test service ts := NewTestService(t) defer ts.Cleanup() // Create admin user user, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Multiple calls should return consistent admin user (from cache) for i := 0; i < 5; i++ { profile, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{}) require.NoError(t, err) require.NotNil(t, profile.Admin) require.Equal(t, user.Username, profile.Admin.Username) } }) } ================================================ FILE: server/router/api/v1/test/instance_service_test.go ================================================ package test import ( "context" "testing" "github.com/stretchr/testify/require" colorpb "google.golang.org/genproto/googleapis/type/color" "google.golang.org/protobuf/types/known/fieldmaskpb" v1pb "github.com/usememos/memos/proto/gen/api/v1" ) func TestGetInstanceProfile(t *testing.T) { ctx := context.Background() t.Run("GetInstanceProfile returns instance profile", func(t *testing.T) { // Create test service for this specific test ts := NewTestService(t) defer ts.Cleanup() // Call GetInstanceProfile directly req := &v1pb.GetInstanceProfileRequest{} resp, err := ts.Service.GetInstanceProfile(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.True(t, resp.Demo) require.Equal(t, "http://localhost:8080", resp.InstanceUrl) // Instance should not be initialized since no admin users are created require.Nil(t, resp.Admin) }) t.Run("GetInstanceProfile with initialized instance", 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 GetInstanceProfile directly req := &v1pb.GetInstanceProfileRequest{} resp, err := ts.Service.GetInstanceProfile(ctx, req) // Verify response require.NoError(t, err) require.NotNil(t, resp) // Verify the response contains expected data with initialized flag require.Equal(t, "test-1.0.0", resp.Version) require.True(t, resp.Demo) require.Equal(t, "http://localhost:8080", resp.InstanceUrl) // Instance should be initialized since an admin user exists require.NotNil(t, resp.Admin) require.Equal(t, hostUser.Username, resp.Admin.Username) }) } func TestGetInstanceProfile_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 _, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Make concurrent requests numGoroutines := 10 results := make(chan *v1pb.InstanceProfile, numGoroutines) errors := make(chan error, numGoroutines) for i := 0; i < numGoroutines; i++ { go func() { req := &v1pb.GetInstanceProfileRequest{} resp, err := ts.Service.GetInstanceProfile(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.True(t, resp.Demo) require.Equal(t, "http://localhost:8080", resp.InstanceUrl) require.NotNil(t, resp.Admin) } } }) } func TestGetInstanceSetting(t *testing.T) { ctx := context.Background() t.Run("GetInstanceSetting - general setting", func(t *testing.T) { // Create test service for this specific test ts := NewTestService(t) defer ts.Cleanup() // Call GetInstanceSetting for general setting req := &v1pb.GetInstanceSettingRequest{ Name: "instance/settings/GENERAL", } resp, err := ts.Service.GetInstanceSetting(ctx, req) // Verify response require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, "instance/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("GetInstanceSetting - 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 GetInstanceSetting for storage setting req := &v1pb.GetInstanceSettingRequest{ Name: "instance/settings/STORAGE", } resp, err := ts.Service.GetInstanceSetting(userCtx, req) // Verify response require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, "instance/settings/STORAGE", resp.Name) // The storage setting should have a storage_setting field storageSetting := resp.GetStorageSetting() require.NotNil(t, storageSetting) }) t.Run("GetInstanceSetting - memo related setting", func(t *testing.T) { // Create test service for this specific test ts := NewTestService(t) defer ts.Cleanup() // Call GetInstanceSetting for memo related setting req := &v1pb.GetInstanceSettingRequest{ Name: "instance/settings/MEMO_RELATED", } resp, err := ts.Service.GetInstanceSetting(ctx, req) // Verify response require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, "instance/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("GetInstanceSetting - tags setting", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() req := &v1pb.GetInstanceSettingRequest{ Name: "instance/settings/TAGS", } resp, err := ts.Service.GetInstanceSetting(ctx, req) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, "instance/settings/TAGS", resp.Name) require.NotNil(t, resp.GetTagsSetting()) require.Empty(t, resp.GetTagsSetting().GetTags()) }) t.Run("GetInstanceSetting - notification setting", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() req := &v1pb.GetInstanceSettingRequest{ Name: "instance/settings/NOTIFICATION", } resp, err := ts.Service.GetInstanceSetting(ctx, req) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, "instance/settings/NOTIFICATION", resp.Name) require.NotNil(t, resp.GetNotificationSetting()) require.NotNil(t, resp.GetNotificationSetting().GetEmail()) require.False(t, resp.GetNotificationSetting().GetEmail().GetEnabled()) }) t.Run("GetInstanceSetting - invalid setting name", func(t *testing.T) { // Create test service for this specific test ts := NewTestService(t) defer ts.Cleanup() // Call GetInstanceSetting with invalid name req := &v1pb.GetInstanceSettingRequest{ Name: "invalid/setting/name", } _, err := ts.Service.GetInstanceSetting(ctx, req) // Should return an error require.Error(t, err) require.Contains(t, err.Error(), "invalid instance setting name") }) } func TestUpdateInstanceSetting(t *testing.T) { ctx := context.Background() t.Run("UpdateInstanceSetting - tags setting", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) resp, err := ts.Service.UpdateInstanceSetting(ts.CreateUserContext(ctx, hostUser.ID), &v1pb.UpdateInstanceSettingRequest{ Setting: &v1pb.InstanceSetting{ Name: "instance/settings/TAGS", Value: &v1pb.InstanceSetting_TagsSetting_{ TagsSetting: &v1pb.InstanceSetting_TagsSetting{ Tags: map[string]*v1pb.InstanceSetting_TagMetadata{ "bug": { BackgroundColor: &colorpb.Color{ Red: 0.9, Green: 0.1, Blue: 0.1, }, }, }, }, }, }, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"tags"}}, }) require.NoError(t, err) require.NotNil(t, resp.GetTagsSetting()) require.Contains(t, resp.GetTagsSetting().GetTags(), "bug") }) t.Run("UpdateInstanceSetting - invalid tags color", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) _, err = ts.Service.UpdateInstanceSetting(ts.CreateUserContext(ctx, hostUser.ID), &v1pb.UpdateInstanceSettingRequest{ Setting: &v1pb.InstanceSetting{ Name: "instance/settings/TAGS", Value: &v1pb.InstanceSetting_TagsSetting_{ TagsSetting: &v1pb.InstanceSetting_TagsSetting{ Tags: map[string]*v1pb.InstanceSetting_TagMetadata{ "bug": { BackgroundColor: &colorpb.Color{ Red: 1.2, Green: 0.1, Blue: 0.1, }, }, }, }, }, }, }) require.Error(t, err) require.Contains(t, err.Error(), "invalid instance setting") }) t.Run("UpdateInstanceSetting - notification setting", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) resp, err := ts.Service.UpdateInstanceSetting(ts.CreateUserContext(ctx, hostUser.ID), &v1pb.UpdateInstanceSettingRequest{ Setting: &v1pb.InstanceSetting{ Name: "instance/settings/NOTIFICATION", Value: &v1pb.InstanceSetting_NotificationSetting_{ NotificationSetting: &v1pb.InstanceSetting_NotificationSetting{ Email: &v1pb.InstanceSetting_NotificationSetting_EmailSetting{ Enabled: true, SmtpHost: "smtp.example.com", SmtpPort: 587, SmtpUsername: "bot@example.com", SmtpPassword: "secret", FromEmail: "bot@example.com", FromName: "Memos Bot", ReplyTo: "support@example.com", UseTls: true, }, }, }, }, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"notification_setting"}}, }) require.NoError(t, err) require.NotNil(t, resp.GetNotificationSetting()) require.NotNil(t, resp.GetNotificationSetting().GetEmail()) require.True(t, resp.GetNotificationSetting().GetEmail().GetEnabled()) require.Equal(t, "smtp.example.com", resp.GetNotificationSetting().GetEmail().GetSmtpHost()) }) } ================================================ FILE: server/router/api/v1/test/memo_attachment_service_test.go ================================================ package test import ( "context" "testing" "github.com/stretchr/testify/require" apiv1 "github.com/usememos/memos/proto/gen/api/v1" ) func TestSetMemoAttachments(t *testing.T) { ctx := context.Background() t.Run("SetMemoAttachments success by memo owner", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user user, err := ts.CreateRegularUser(ctx, "user") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) // Create memo memo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, memo) // Create attachment attachment, err := ts.Service.CreateAttachment(userCtx, &apiv1.CreateAttachmentRequest{ Attachment: &apiv1.Attachment{ Filename: "test.txt", Size: 5, Type: "text/plain", Content: []byte("hello"), }, }) require.NoError(t, err) require.NotNil(t, attachment) // Set memo attachments - should succeed _, err = ts.Service.SetMemoAttachments(userCtx, &apiv1.SetMemoAttachmentsRequest{ Name: memo.Name, Attachments: []*apiv1.Attachment{ {Name: attachment.Name}, }, }) require.NoError(t, err) }) t.Run("SetMemoAttachments success by 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) regularUserCtx := ts.CreateUserContext(ctx, regularUser.ID) // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) hostCtx := ts.CreateUserContext(ctx, hostUser.ID) // Create memo by regular user memo, err := ts.Service.CreateMemo(regularUserCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, memo) // Host user can modify attachments - should succeed _, err = ts.Service.SetMemoAttachments(hostCtx, &apiv1.SetMemoAttachmentsRequest{ Name: memo.Name, Attachments: []*apiv1.Attachment{}, }) require.NoError(t, err) }) t.Run("SetMemoAttachments permission denied for non-owner", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user1 user1, err := ts.CreateRegularUser(ctx, "user1") require.NoError(t, err) user1Ctx := ts.CreateUserContext(ctx, user1.ID) // Create user2 user2, err := ts.CreateRegularUser(ctx, "user2") require.NoError(t, err) user2Ctx := ts.CreateUserContext(ctx, user2.ID) // Create memo by user1 memo, err := ts.Service.CreateMemo(user1Ctx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, memo) // User2 tries to modify attachments - should fail _, err = ts.Service.SetMemoAttachments(user2Ctx, &apiv1.SetMemoAttachmentsRequest{ Name: memo.Name, Attachments: []*apiv1.Attachment{}, }) require.Error(t, err) require.Contains(t, err.Error(), "permission denied") }) t.Run("SetMemoAttachments unauthenticated", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user user, err := ts.CreateRegularUser(ctx, "user") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) // Create memo memo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, memo) // Unauthenticated user tries to modify attachments - should fail _, err = ts.Service.SetMemoAttachments(ctx, &apiv1.SetMemoAttachmentsRequest{ Name: memo.Name, Attachments: []*apiv1.Attachment{}, }) require.Error(t, err) require.Contains(t, err.Error(), "not authenticated") }) t.Run("SetMemoAttachments memo not found", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user user, err := ts.CreateRegularUser(ctx, "user") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) // Try to set attachments on non-existent memo - should fail _, err = ts.Service.SetMemoAttachments(userCtx, &apiv1.SetMemoAttachmentsRequest{ Name: "memos/nonexistent-uid-12345", Attachments: []*apiv1.Attachment{}, }) require.Error(t, err) require.Contains(t, err.Error(), "not found") }) } ================================================ FILE: server/router/api/v1/test/memo_relation_service_test.go ================================================ package test import ( "context" "testing" "github.com/stretchr/testify/require" apiv1 "github.com/usememos/memos/proto/gen/api/v1" ) func TestSetMemoRelations(t *testing.T) { ctx := context.Background() t.Run("SetMemoRelations success by memo owner", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user user, err := ts.CreateRegularUser(ctx, "user") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) // Create memo1 memo1, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo 1", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, memo1) // Create memo2 memo2, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo 2", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, memo2) // Set memo relations - should succeed _, err = ts.Service.SetMemoRelations(userCtx, &apiv1.SetMemoRelationsRequest{ Name: memo1.Name, Relations: []*apiv1.MemoRelation{ { RelatedMemo: &apiv1.MemoRelation_Memo{ Name: memo2.Name, }, Type: apiv1.MemoRelation_REFERENCE, }, }, }) require.NoError(t, err) }) t.Run("SetMemoRelations success by 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) regularUserCtx := ts.CreateUserContext(ctx, regularUser.ID) // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) hostCtx := ts.CreateUserContext(ctx, hostUser.ID) // Create memo by regular user memo, err := ts.Service.CreateMemo(regularUserCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, memo) // Host user can modify relations - should succeed _, err = ts.Service.SetMemoRelations(hostCtx, &apiv1.SetMemoRelationsRequest{ Name: memo.Name, Relations: []*apiv1.MemoRelation{}, }) require.NoError(t, err) }) t.Run("SetMemoRelations permission denied for non-owner", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user1 user1, err := ts.CreateRegularUser(ctx, "user1") require.NoError(t, err) user1Ctx := ts.CreateUserContext(ctx, user1.ID) // Create user2 user2, err := ts.CreateRegularUser(ctx, "user2") require.NoError(t, err) user2Ctx := ts.CreateUserContext(ctx, user2.ID) // Create memo by user1 memo, err := ts.Service.CreateMemo(user1Ctx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, memo) // User2 tries to modify relations - should fail _, err = ts.Service.SetMemoRelations(user2Ctx, &apiv1.SetMemoRelationsRequest{ Name: memo.Name, Relations: []*apiv1.MemoRelation{}, }) require.Error(t, err) require.Contains(t, err.Error(), "permission denied") }) t.Run("SetMemoRelations unauthenticated", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user user, err := ts.CreateRegularUser(ctx, "user") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) // Create memo memo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, memo) // Unauthenticated user tries to modify relations - should fail _, err = ts.Service.SetMemoRelations(ctx, &apiv1.SetMemoRelationsRequest{ Name: memo.Name, Relations: []*apiv1.MemoRelation{}, }) require.Error(t, err) require.Contains(t, err.Error(), "not authenticated") }) t.Run("SetMemoRelations memo not found", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user user, err := ts.CreateRegularUser(ctx, "user") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) // Try to set relations on non-existent memo - should fail _, err = ts.Service.SetMemoRelations(userCtx, &apiv1.SetMemoRelationsRequest{ Name: "memos/nonexistent-uid-12345", Relations: []*apiv1.MemoRelation{}, }) require.Error(t, err) require.Contains(t, err.Error(), "not found") }) } ================================================ FILE: server/router/api/v1/test/memo_service_test.go ================================================ package test import ( "context" "fmt" "slices" "testing" "time" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" apiv1 "github.com/usememos/memos/proto/gen/api/v1" ) func TestListMemos(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() // Create userOne userOne, err := ts.CreateRegularUser(ctx, "test-user-1") require.NoError(t, err) require.NotNil(t, userOne) // Create userOne context userOneCtx := ts.CreateUserContext(ctx, userOne.ID) // Create userTwo userTwo, err := ts.CreateRegularUser(ctx, "test-user-2") require.NoError(t, err) require.NotNil(t, userTwo) // Create userTwo context userTwoCtx := ts.CreateUserContext(ctx, userTwo.ID) // Create attachmentOne by userOne attachmentOne, err := ts.Service.CreateAttachment(userOneCtx, &apiv1.CreateAttachmentRequest{ Attachment: &apiv1.Attachment{ Name: "", Filename: "hello.txt", Size: 5, Type: "text/plain", Content: []byte{ 104, 101, 108, 108, 111, }, }, }) require.NoError(t, err) require.NotNil(t, attachmentOne) // Create attachmentTwo by userOne attachmentTwo, err := ts.Service.CreateAttachment(userOneCtx, &apiv1.CreateAttachmentRequest{ Attachment: &apiv1.Attachment{ Name: "", Filename: "world.txt", Size: 5, Type: "text/plain", Content: []byte{ 119, 111, 114, 108, 100, }, }, }) require.NoError(t, err) require.NotNil(t, attachmentTwo) // Create memoOne with two attachments by userOne memoOne, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Hellooo, any words after this sentence won't be in the snippet. This is the next sentence. And I also have two attachments.", Visibility: apiv1.Visibility_PROTECTED, Attachments: []*apiv1.Attachment{ &apiv1.Attachment{ Name: attachmentOne.Name, }, &apiv1.Attachment{ Name: attachmentTwo.Name, }, }, }, }) require.NoError(t, err) require.NotNil(t, memoOne) // Create memoTwo by userTwo referencing memoOne memoTwo, err := ts.Service.CreateMemo(userTwoCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "This is a memo reminding you to check the attachment attached to memoOne. I have referenced the memo below.⬇️", Visibility: apiv1.Visibility_PROTECTED, Relations: []*apiv1.MemoRelation{ &apiv1.MemoRelation{ RelatedMemo: &apiv1.MemoRelation_Memo{ Name: memoOne.Name, }, }, }, }, }) require.NoError(t, err) require.NotNil(t, memoTwo) // Create memoThree by userOne memoThree, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "This is a very popular memo. I have 2 reactions!", Visibility: apiv1.Visibility_PROTECTED, }, }) require.NoError(t, err) require.NotNil(t, memoThree) // Create reaction from userOne on memoThree reactionOne, err := ts.Service.UpsertMemoReaction(userOneCtx, &apiv1.UpsertMemoReactionRequest{ Name: memoThree.Name, Reaction: &apiv1.Reaction{ ContentId: memoThree.Name, ReactionType: "❤️", }, }) require.NoError(t, err) require.NotNil(t, reactionOne) // Create reaction from userTwo on memoThree reactionTwo, err := ts.Service.UpsertMemoReaction(userTwoCtx, &apiv1.UpsertMemoReactionRequest{ Name: memoThree.Name, Reaction: &apiv1.Reaction{ ContentId: memoThree.Name, ReactionType: "👍", }, }) require.NoError(t, err) require.NotNil(t, reactionTwo) memos, err := ts.Service.ListMemos(userOneCtx, &apiv1.ListMemosRequest{PageSize: 10}) require.NoError(t, err) require.NotNil(t, memos) require.Equal(t, 3, len(memos.Memos)) // /////////////// // VERIFY MEMO ONE // /////////////// memoOneResIdx := slices.IndexFunc(memos.Memos, func(m *apiv1.Memo) bool { return m.GetName() == memoOne.GetName() }) require.NotEqual(t, memoOneResIdx, -1) memoOneRes := memos.Memos[memoOneResIdx] require.NotNil(t, memoOneRes) require.Equal(t, fmt.Sprintf("users/%d", userOne.ID), memoOneRes.GetCreator()) require.Equal(t, apiv1.Visibility_PROTECTED, memoOneRes.GetVisibility()) require.Equal(t, memoOne.Content, memoOneRes.GetContent()) require.Equal(t, memoOne.Content[:64]+"...", memoOneRes.GetSnippet(), "memoOne's content is snipped past the 64 char limit") require.Len(t, memoOneRes.Attachments, 2) require.Len(t, memoOneRes.Relations, 1) require.Empty(t, memoOneRes.Reactions) // verify memoOne's attachments // attachment one attachmentOneResIdx := slices.IndexFunc(memoOneRes.Attachments, func(a *apiv1.Attachment) bool { return a.GetName() == attachmentOne.GetName() }) require.NotEqual(t, attachmentOneResIdx, -1) attachmentOneRes := memoOneRes.Attachments[attachmentOneResIdx] require.NotNil(t, attachmentOneRes) require.Equal(t, attachmentOne.GetName(), attachmentOneRes.GetName()) require.Equal(t, attachmentOne.GetContent(), attachmentOneRes.GetContent()) // attachment two attachmentTwoResIdx := slices.IndexFunc(memoOneRes.Attachments, func(a *apiv1.Attachment) bool { return a.GetName() == attachmentTwo.GetName() }) require.NotEqual(t, attachmentTwoResIdx, -1) attachmentTwoRes := memoOneRes.Attachments[attachmentTwoResIdx] require.NotNil(t, attachmentTwoRes) require.Equal(t, attachmentTwo.GetName(), attachmentTwoRes.GetName()) require.Equal(t, attachmentTwo.GetName(), attachmentTwoRes.GetName()) require.Equal(t, attachmentTwo.GetContent(), attachmentTwoRes.GetContent()) // verify memoOne's relations require.Len(t, memoOneRes.Relations, 1) memoOneExpectedRelation := &apiv1.MemoRelation{ Memo: &apiv1.MemoRelation_Memo{Name: memoTwo.GetName()}, RelatedMemo: &apiv1.MemoRelation_Memo{Name: memoOne.GetName()}, } require.Equal(t, memoOneExpectedRelation.Memo.GetName(), memoOneRes.Relations[0].Memo.GetName()) require.Equal(t, memoOneExpectedRelation.RelatedMemo.GetName(), memoOneRes.Relations[0].RelatedMemo.GetName()) // /////////////// // VERIFY MEMO TWO // /////////////// memoTwoResIdx := slices.IndexFunc(memos.Memos, func(m *apiv1.Memo) bool { return m.GetName() == memoTwo.GetName() }) require.NotEqual(t, memoTwoResIdx, -1) memoTwoRes := memos.Memos[memoTwoResIdx] require.NotNil(t, memoTwoRes) require.Equal(t, fmt.Sprintf("users/%d", userTwo.ID), memoTwoRes.GetCreator()) require.Equal(t, apiv1.Visibility_PROTECTED, memoTwoRes.GetVisibility()) require.Equal(t, memoTwo.Content, memoTwoRes.GetContent()) require.Empty(t, memoTwoRes.Attachments) require.Len(t, memoTwoRes.Relations, 1) require.Empty(t, memoTwoRes.Reactions) // verify memoTwo's relations require.Len(t, memoTwoRes.Relations, 1) memoTwoExpectedRelation := &apiv1.MemoRelation{ Memo: &apiv1.MemoRelation_Memo{Name: memoTwo.GetName()}, RelatedMemo: &apiv1.MemoRelation_Memo{Name: memoOne.GetName()}, } require.Equal(t, memoTwoExpectedRelation.Memo.GetName(), memoTwoRes.Relations[0].Memo.GetName()) require.Equal(t, memoTwoExpectedRelation.RelatedMemo.GetName(), memoTwoRes.Relations[0].RelatedMemo.GetName()) // /////////////// // VERIFY MEMO THREE // /////////////// memoThreeResIdx := slices.IndexFunc(memos.Memos, func(m *apiv1.Memo) bool { return m.GetName() == memoThree.GetName() }) require.NotEqual(t, memoThreeResIdx, -1) memoThreeRes := memos.Memos[memoThreeResIdx] require.NotNil(t, memoThreeRes) require.Equal(t, fmt.Sprintf("users/%d", userOne.ID), memoThreeRes.GetCreator()) require.Equal(t, apiv1.Visibility_PROTECTED, memoThreeRes.GetVisibility()) require.Equal(t, memoThree.Content, memoThreeRes.GetContent()) require.Empty(t, memoThreeRes.Attachments) require.Empty(t, memoThreeRes.Relations) require.Len(t, memoThreeRes.Reactions, 2) // verify memoThree's reactions require.Len(t, memoThreeRes.Reactions, 2) // userOne's reaction userOneReactionIdx := slices.IndexFunc(memoThreeRes.Reactions, func(r *apiv1.Reaction) bool { return r.GetCreator() == fmt.Sprintf("users/%d", userOne.ID) }) require.NotEqual(t, userOneReactionIdx, -1) userOneReaction := memoThreeRes.Reactions[userOneReactionIdx] require.NotNil(t, userOneReaction) require.Equal(t, "❤️", userOneReaction.ReactionType) // userTwo's reaction userTwoReactionIdx := slices.IndexFunc(memoThreeRes.Reactions, func(r *apiv1.Reaction) bool { return r.GetCreator() == fmt.Sprintf("users/%d", userTwo.ID) }) require.NotEqual(t, userTwoReactionIdx, -1) userTwoReaction := memoThreeRes.Reactions[userTwoReactionIdx] require.NotNil(t, userTwoReaction) require.Equal(t, "👍", userTwoReaction.ReactionType) } // TestCreateMemoWithCustomTimestamps tests that custom timestamps can be set when creating memos and comments. // This addresses issue #5483: https://github.com/usememos/memos/issues/5483 func TestCreateMemoWithCustomTimestamps(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() // Create a test user user, err := ts.CreateRegularUser(ctx, "test-user-timestamps") require.NoError(t, err) require.NotNil(t, user) userCtx := ts.CreateUserContext(ctx, user.ID) // Define custom timestamps (January 1, 2020) customCreateTime := time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC) customUpdateTime := time.Date(2020, 1, 2, 12, 0, 0, 0, time.UTC) customDisplayTime := time.Date(2020, 1, 3, 12, 0, 0, 0, time.UTC) // Test 1: Create a memo with custom create_time memoWithCreateTime, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "This memo has a custom creation time", Visibility: apiv1.Visibility_PRIVATE, CreateTime: timestamppb.New(customCreateTime), }, }) require.NoError(t, err) require.NotNil(t, memoWithCreateTime) require.Equal(t, customCreateTime.Unix(), memoWithCreateTime.CreateTime.AsTime().Unix(), "create_time should match the custom timestamp") // Test 2: Create a memo with custom update_time memoWithUpdateTime, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "This memo has a custom update time", Visibility: apiv1.Visibility_PRIVATE, UpdateTime: timestamppb.New(customUpdateTime), }, }) require.NoError(t, err) require.NotNil(t, memoWithUpdateTime) require.Equal(t, customUpdateTime.Unix(), memoWithUpdateTime.UpdateTime.AsTime().Unix(), "update_time should match the custom timestamp") // Test 3: Create a memo with custom display_time // Note: display_time is computed from either created_ts or updated_ts based on instance setting // Since DisplayWithUpdateTime defaults to false, display_time maps to created_ts memoWithDisplayTime, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "This memo has a custom display time", Visibility: apiv1.Visibility_PRIVATE, DisplayTime: timestamppb.New(customDisplayTime), }, }) require.NoError(t, err) require.NotNil(t, memoWithDisplayTime) // Since DisplayWithUpdateTime is false by default, display_time sets created_ts require.Equal(t, customDisplayTime.Unix(), memoWithDisplayTime.DisplayTime.AsTime().Unix(), "display_time should match the custom timestamp") require.Equal(t, customDisplayTime.Unix(), memoWithDisplayTime.CreateTime.AsTime().Unix(), "create_time should also match since display_time maps to created_ts") // Test 4: Create a memo with all custom timestamps // When both display_time and create_time are provided, create_time takes precedence memoWithAllTimestamps, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "This memo has all custom timestamps", Visibility: apiv1.Visibility_PRIVATE, CreateTime: timestamppb.New(customCreateTime), UpdateTime: timestamppb.New(customUpdateTime), DisplayTime: timestamppb.New(customDisplayTime), }, }) require.NoError(t, err) require.NotNil(t, memoWithAllTimestamps) require.Equal(t, customCreateTime.Unix(), memoWithAllTimestamps.CreateTime.AsTime().Unix(), "create_time should match the custom timestamp") require.Equal(t, customUpdateTime.Unix(), memoWithAllTimestamps.UpdateTime.AsTime().Unix(), "update_time should match the custom timestamp") // display_time is computed from created_ts when DisplayWithUpdateTime is false require.Equal(t, customCreateTime.Unix(), memoWithAllTimestamps.DisplayTime.AsTime().Unix(), "display_time should be derived from create_time") // Test 5: Create a comment (memo relation) with custom timestamps parentMemo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "This is the parent memo", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, parentMemo) customCommentCreateTime := time.Date(2021, 6, 15, 10, 30, 0, 0, time.UTC) comment, err := ts.Service.CreateMemoComment(userCtx, &apiv1.CreateMemoCommentRequest{ Name: parentMemo.Name, Comment: &apiv1.Memo{ Content: "This is a comment with custom create time", Visibility: apiv1.Visibility_PRIVATE, CreateTime: timestamppb.New(customCommentCreateTime), }, }) require.NoError(t, err) require.NotNil(t, comment) require.Equal(t, customCommentCreateTime.Unix(), comment.CreateTime.AsTime().Unix(), "comment create_time should match the custom timestamp") // Test 6: Verify that memos without custom timestamps still get auto-generated ones memoWithoutTimestamps, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "This memo has auto-generated timestamps", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) require.NotNil(t, memoWithoutTimestamps) require.NotNil(t, memoWithoutTimestamps.CreateTime, "create_time should be auto-generated") require.NotNil(t, memoWithoutTimestamps.UpdateTime, "update_time should be auto-generated") require.True(t, time.Now().Unix()-memoWithoutTimestamps.CreateTime.AsTime().Unix() < 5, "create_time should be recent (within 5 seconds)") } ================================================ FILE: server/router/api/v1/test/memo_share_service_test.go ================================================ package test import ( "context" "strings" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" apiv1 "github.com/usememos/memos/proto/gen/api/v1" ) func TestDeleteMemoShare_VerifiesShareBelongsToMemo(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() userOne, err := ts.CreateRegularUser(ctx, "share-owner-one") require.NoError(t, err) userTwo, err := ts.CreateRegularUser(ctx, "share-owner-two") require.NoError(t, err) userOneCtx := ts.CreateUserContext(ctx, userOne.ID) userTwoCtx := ts.CreateUserContext(ctx, userTwo.ID) memoOne, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "memo one", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) memoTwo, err := ts.Service.CreateMemo(userTwoCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "memo two", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) share, err := ts.Service.CreateMemoShare(userTwoCtx, &apiv1.CreateMemoShareRequest{ Parent: memoTwo.Name, MemoShare: &apiv1.MemoShare{}, }) require.NoError(t, err) shareToken := share.Name[strings.LastIndex(share.Name, "/")+1:] forgedName := memoOne.Name + "/shares/" + shareToken _, err = ts.Service.DeleteMemoShare(userOneCtx, &apiv1.DeleteMemoShareRequest{ Name: forgedName, }) require.Error(t, err) require.Equal(t, codes.NotFound, status.Code(err)) sharedMemo, err := ts.Service.GetMemoByShare(ctx, &apiv1.GetMemoByShareRequest{ ShareId: shareToken, }) require.NoError(t, err) require.Equal(t, memoTwo.Name, sharedMemo.Name) } func TestGetMemoByShare_IncludesReactions(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "share-reactions") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) memo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "memo with reactions", Visibility: apiv1.Visibility_PRIVATE, }, }) require.NoError(t, err) reaction, err := ts.Service.UpsertMemoReaction(userCtx, &apiv1.UpsertMemoReactionRequest{ Name: memo.Name, Reaction: &apiv1.Reaction{ ContentId: memo.Name, ReactionType: "👍", }, }) require.NoError(t, err) require.NotNil(t, reaction) share, err := ts.Service.CreateMemoShare(userCtx, &apiv1.CreateMemoShareRequest{ Parent: memo.Name, MemoShare: &apiv1.MemoShare{}, }) require.NoError(t, err) shareToken := share.Name[strings.LastIndex(share.Name, "/")+1:] sharedMemo, err := ts.Service.GetMemoByShare(ctx, &apiv1.GetMemoByShareRequest{ ShareId: shareToken, }) require.NoError(t, err) require.Len(t, sharedMemo.Reactions, 1) require.Equal(t, "👍", sharedMemo.Reactions[0].ReactionType) require.Equal(t, memo.Name, sharedMemo.Reactions[0].ContentId) } ================================================ FILE: server/router/api/v1/test/reaction_service_test.go ================================================ package test import ( "context" "testing" "github.com/stretchr/testify/require" apiv1 "github.com/usememos/memos/proto/gen/api/v1" ) func TestDeleteMemoReaction(t *testing.T) { ctx := context.Background() t.Run("DeleteMemoReaction success by reaction owner", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user user, err := ts.CreateRegularUser(ctx, "user") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) // Create memo memo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PUBLIC, }, }) require.NoError(t, err) require.NotNil(t, memo) // Create reaction reaction, err := ts.Service.UpsertMemoReaction(userCtx, &apiv1.UpsertMemoReactionRequest{ Name: memo.Name, Reaction: &apiv1.Reaction{ ContentId: memo.Name, ReactionType: "👍", }, }) require.NoError(t, err) require.NotNil(t, reaction) // Delete reaction - should succeed _, err = ts.Service.DeleteMemoReaction(userCtx, &apiv1.DeleteMemoReactionRequest{ Name: reaction.Name, }) require.NoError(t, err) }) t.Run("DeleteMemoReaction success by 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) regularUserCtx := ts.CreateUserContext(ctx, regularUser.ID) // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) hostCtx := ts.CreateUserContext(ctx, hostUser.ID) // Create memo by regular user memo, err := ts.Service.CreateMemo(regularUserCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PUBLIC, }, }) require.NoError(t, err) require.NotNil(t, memo) // Create reaction by regular user reaction, err := ts.Service.UpsertMemoReaction(regularUserCtx, &apiv1.UpsertMemoReactionRequest{ Name: memo.Name, Reaction: &apiv1.Reaction{ ContentId: memo.Name, ReactionType: "👍", }, }) require.NoError(t, err) require.NotNil(t, reaction) // Host user can delete reaction - should succeed _, err = ts.Service.DeleteMemoReaction(hostCtx, &apiv1.DeleteMemoReactionRequest{ Name: reaction.Name, }) require.NoError(t, err) }) t.Run("DeleteMemoReaction permission denied for non-owner", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user1 user1, err := ts.CreateRegularUser(ctx, "user1") require.NoError(t, err) user1Ctx := ts.CreateUserContext(ctx, user1.ID) // Create user2 user2, err := ts.CreateRegularUser(ctx, "user2") require.NoError(t, err) user2Ctx := ts.CreateUserContext(ctx, user2.ID) // Create memo by user1 memo, err := ts.Service.CreateMemo(user1Ctx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PUBLIC, }, }) require.NoError(t, err) require.NotNil(t, memo) // Create reaction by user1 reaction, err := ts.Service.UpsertMemoReaction(user1Ctx, &apiv1.UpsertMemoReactionRequest{ Name: memo.Name, Reaction: &apiv1.Reaction{ ContentId: memo.Name, ReactionType: "👍", }, }) require.NoError(t, err) require.NotNil(t, reaction) // User2 tries to delete reaction - should fail with permission denied _, err = ts.Service.DeleteMemoReaction(user2Ctx, &apiv1.DeleteMemoReactionRequest{ Name: reaction.Name, }) require.Error(t, err) require.Contains(t, err.Error(), "permission denied") }) t.Run("DeleteMemoReaction unauthenticated", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user user, err := ts.CreateRegularUser(ctx, "user") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) // Create memo memo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Test memo", Visibility: apiv1.Visibility_PUBLIC, }, }) require.NoError(t, err) require.NotNil(t, memo) // Create reaction reaction, err := ts.Service.UpsertMemoReaction(userCtx, &apiv1.UpsertMemoReactionRequest{ Name: memo.Name, Reaction: &apiv1.Reaction{ ContentId: memo.Name, ReactionType: "👍", }, }) require.NoError(t, err) require.NotNil(t, reaction) // Unauthenticated user tries to delete reaction - should fail _, err = ts.Service.DeleteMemoReaction(ctx, &apiv1.DeleteMemoReactionRequest{ Name: reaction.Name, }) require.Error(t, err) require.Contains(t, err.Error(), "not authenticated") }) t.Run("DeleteMemoReaction not found returns permission denied", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create user user, err := ts.CreateRegularUser(ctx, "user") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) // Try to delete non-existent reaction - should fail with permission denied // (not "not found" to avoid information disclosure) // Use new nested resource format: memos/{memo}/reactions/{reaction} _, err = ts.Service.DeleteMemoReaction(userCtx, &apiv1.DeleteMemoReactionRequest{ Name: "memos/nonexistent/reactions/99999", }) require.Error(t, err) require.Contains(t, err.Error(), "permission denied") require.NotContains(t, err.Error(), "not found") }) } ================================================ FILE: server/router/api/v1/test/shortcut_service_test.go ================================================ package test 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") }) } ================================================ FILE: server/router/api/v1/test/sse_handler_test.go ================================================ package test import ( "context" "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v5" "github.com/stretchr/testify/require" "github.com/usememos/memos/server/auth" apiv1 "github.com/usememos/memos/server/router/api/v1" ) func TestSSEHandler_Authentication(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() user, err := ts.CreateRegularUser(ctx, "sse-user") require.NoError(t, err) token, _, err := auth.GenerateAccessTokenV2( user.ID, user.Username, string(user.Role), string(user.RowStatus), []byte(ts.Secret), ) require.NoError(t, err) e := echo.New() apiv1.RegisterSSERoutes(e, ts.Service.SSEHub, ts.Store, ts.Secret) t.Run("no token returns 401", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/sse", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) require.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("invalid token returns 401", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/sse", nil) req.Header.Set("Authorization", "Bearer invalid-token") rec := httptest.NewRecorder() e.ServeHTTP(rec, req) require.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("valid token returns 200 and stream", func(t *testing.T) { // Use a cancellable context so we can close the SSE connection after // confirming the headers, preventing the handler's event loop from // blocking the test indefinitely. reqCtx, cancel := context.WithCancel(context.Background()) defer cancel() req := httptest.NewRequest(http.MethodGet, "/api/v1/sse", nil).WithContext(reqCtx) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() done := make(chan struct{}) go func() { defer close(done) e.ServeHTTP(rec, req) }() // Cancel the context to signal client disconnect, which exits the SSE loop. cancel() <-done require.Equal(t, http.StatusOK, rec.Code) require.Equal(t, "text/event-stream", rec.Header().Get("Content-Type")) }) t.Run("token in query param returns 401", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/sse?token="+token, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) require.Equal(t, http.StatusUnauthorized, rec.Code) }) } ================================================ FILE: server/router/api/v1/test/test_helper.go ================================================ package test import ( "context" "testing" "github.com/usememos/memos/internal/profile" "github.com/usememos/memos/plugin/markdown" "github.com/usememos/memos/server/auth" 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 with a temp directory for file storage, // so tests that create attachments don't leave artifacts in the source tree. testProfile := &profile.Profile{ Demo: true, Version: "test-1.0.0", InstanceURL: "http://localhost:8080", Driver: "sqlite", DSN: ":memory:", Data: t.TempDir(), } // Create APIV1Service with nil grpcServer since we're testing direct calls secret := "test-secret" markdownService := markdown.NewService( markdown.WithTagExtension(), ) service := &apiv1.APIV1Service{ Secret: secret, Profile: testProfile, Store: testStore, MarkdownService: markdownService, SSEHub: apiv1.NewSSEHub(), } return &TestService{ Service: service, Store: testStore, Profile: testProfile, Secret: secret, } } // Cleanup closes resources after test. func (ts *TestService) Cleanup() { ts.Store.Close() } // CreateHostUser creates an admin 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.RoleAdmin, 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 context key from the auth package return context.WithValue(ctx, auth.UserIDContextKey, userID) } ================================================ FILE: server/router/api/v1/test/user_notification_test.go ================================================ package test import ( "context" "fmt" "testing" "github.com/stretchr/testify/require" apiv1 "github.com/usememos/memos/proto/gen/api/v1" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) func TestListUserNotificationsIncludesMemoCommentPayload(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() owner, err := ts.CreateRegularUser(ctx, "notification-owner") require.NoError(t, err) ownerCtx := ts.CreateUserContext(ctx, owner.ID) commenter, err := ts.CreateRegularUser(ctx, "notification-commenter") require.NoError(t, err) commenterCtx := ts.CreateUserContext(ctx, commenter.ID) memo, err := ts.Service.CreateMemo(ownerCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Base memo", Visibility: apiv1.Visibility_PUBLIC, }, }) require.NoError(t, err) comment, err := ts.Service.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{ Name: memo.Name, Comment: &apiv1.Memo{ Content: "Comment content", Visibility: apiv1.Visibility_PUBLIC, }, }) require.NoError(t, err) resp, err := ts.Service.ListUserNotifications(ownerCtx, &apiv1.ListUserNotificationsRequest{ Parent: fmt.Sprintf("users/%d", owner.ID), }) require.NoError(t, err) require.Len(t, resp.Notifications, 1) notification := resp.Notifications[0] require.Equal(t, apiv1.UserNotification_MEMO_COMMENT, notification.Type) require.NotNil(t, notification.GetMemoComment()) require.Equal(t, comment.Name, notification.GetMemoComment().Memo) require.Equal(t, memo.Name, notification.GetMemoComment().RelatedMemo) } func TestListUserNotificationsStoresMemoCommentPayloadInInbox(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() owner, err := ts.CreateRegularUser(ctx, "notification-owner") require.NoError(t, err) ownerCtx := ts.CreateUserContext(ctx, owner.ID) commenter, err := ts.CreateRegularUser(ctx, "notification-commenter") require.NoError(t, err) commenterCtx := ts.CreateUserContext(ctx, commenter.ID) memo, err := ts.Service.CreateMemo(ownerCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Base memo", Visibility: apiv1.Visibility_PUBLIC, }, }) require.NoError(t, err) _, err = ts.Service.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{ Name: memo.Name, Comment: &apiv1.Memo{ Content: "Comment content", Visibility: apiv1.Visibility_PUBLIC, }, }) require.NoError(t, err) messageType := storepb.InboxMessage_MEMO_COMMENT inboxes, err := ts.Store.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &owner.ID, MessageType: &messageType, }) require.NoError(t, err) require.Len(t, inboxes, 1) require.NotNil(t, inboxes[0].Message) require.NotNil(t, inboxes[0].Message.GetMemoComment()) require.NotZero(t, inboxes[0].Message.GetMemoComment().MemoId) require.NotZero(t, inboxes[0].Message.GetMemoComment().RelatedMemoId) } func TestListUserNotificationsOmitsPayloadWhenMemosDeleted(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() owner, err := ts.CreateRegularUser(ctx, "notification-owner") require.NoError(t, err) ownerCtx := ts.CreateUserContext(ctx, owner.ID) commenter, err := ts.CreateRegularUser(ctx, "notification-commenter") require.NoError(t, err) commenterCtx := ts.CreateUserContext(ctx, commenter.ID) memo, err := ts.Service.CreateMemo(ownerCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "Base memo", Visibility: apiv1.Visibility_PUBLIC, }, }) require.NoError(t, err) _, err = ts.Service.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{ Name: memo.Name, Comment: &apiv1.Memo{ Content: "Comment content", Visibility: apiv1.Visibility_PUBLIC, }, }) require.NoError(t, err) _, err = ts.Service.DeleteMemo(ownerCtx, &apiv1.DeleteMemoRequest{ Name: memo.Name, }) require.NoError(t, err) resp, err := ts.Service.ListUserNotifications(ownerCtx, &apiv1.ListUserNotificationsRequest{ Parent: fmt.Sprintf("users/%d", owner.ID), }) require.NoError(t, err) require.Len(t, resp.Notifications, 1) require.Equal(t, apiv1.UserNotification_MEMO_COMMENT, resp.Notifications[0].Type) require.Nil(t, resp.Notifications[0].GetMemoComment()) } ================================================ FILE: server/router/api/v1/test/user_service_registration_test.go ================================================ package test import ( "context" "testing" "github.com/stretchr/testify/require" apiv1 "github.com/usememos/memos/proto/gen/api/v1" storepb "github.com/usememos/memos/proto/gen/store" ) func TestCreateUserRegistration(t *testing.T) { ctx := context.Background() t.Run("CreateUser success when registration enabled", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // User registration is enabled by default, no need to set it explicitly // Create user without authentication - should succeed _, err := ts.Service.CreateUser(ctx, &apiv1.CreateUserRequest{ User: &apiv1.User{ Username: "newuser", Email: "newuser@example.com", Password: "password123", }, }) require.NoError(t, err) }) t.Run("CreateUser blocked when registration disabled", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create a host user first so we're not in first-user setup mode _, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Disable user registration _, err = ts.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_GENERAL, Value: &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: &storepb.InstanceGeneralSetting{ DisallowUserRegistration: true, }, }, }) require.NoError(t, err) // Try to create user without authentication - should fail _, err = ts.Service.CreateUser(ctx, &apiv1.CreateUserRequest{ User: &apiv1.User{ Username: "newuser", Email: "newuser@example.com", Password: "password123", }, }) require.Error(t, err) require.Contains(t, err.Error(), "not allowed") }) t.Run("CreateUser succeeds for superuser even when registration disabled", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) hostCtx := ts.CreateUserContext(ctx, hostUser.ID) // Disable user registration _, err = ts.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_GENERAL, Value: &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: &storepb.InstanceGeneralSetting{ DisallowUserRegistration: true, }, }, }) require.NoError(t, err) // Host user can create users even when registration is disabled - should succeed _, err = ts.Service.CreateUser(hostCtx, &apiv1.CreateUserRequest{ User: &apiv1.User{ Username: "newuser", Email: "newuser@example.com", Password: "password123", }, }) require.NoError(t, err) }) t.Run("CreateUser regular user cannot create users when registration disabled", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create regular user regularUser, err := ts.CreateRegularUser(ctx, "regularuser") require.NoError(t, err) regularUserCtx := ts.CreateUserContext(ctx, regularUser.ID) // Disable user registration _, err = ts.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_GENERAL, Value: &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: &storepb.InstanceGeneralSetting{ DisallowUserRegistration: true, }, }, }) require.NoError(t, err) // Regular user tries to create user when registration is disabled - should fail _, err = ts.Service.CreateUser(regularUserCtx, &apiv1.CreateUserRequest{ User: &apiv1.User{ Username: "newuser", Email: "newuser@example.com", Password: "password123", }, }) require.Error(t, err) require.Contains(t, err.Error(), "not allowed") }) t.Run("CreateUser host can assign roles", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) hostCtx := ts.CreateUserContext(ctx, hostUser.ID) // Host user can create user with specific role - should succeed createdUser, err := ts.Service.CreateUser(hostCtx, &apiv1.CreateUserRequest{ User: &apiv1.User{ Username: "newadmin", Email: "newadmin@example.com", Password: "password123", Role: apiv1.User_ADMIN, }, }) require.NoError(t, err) require.NotNil(t, createdUser) require.Equal(t, apiv1.User_ADMIN, createdUser.Role) }) t.Run("CreateUser unauthenticated user can only create regular user", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create a host user first so we're not in first-user setup mode _, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // User registration is enabled by default // Unauthenticated user tries to create admin user - role should be ignored createdUser, err := ts.Service.CreateUser(ctx, &apiv1.CreateUserRequest{ User: &apiv1.User{ Username: "wannabeadmin", Email: "wannabeadmin@example.com", Password: "password123", Role: apiv1.User_ADMIN, // This should be ignored }, }) require.NoError(t, err) require.NotNil(t, createdUser) require.Equal(t, apiv1.User_USER, createdUser.Role, "Unauthenticated users can only create USER role") }) } ================================================ FILE: server/router/api/v1/test/user_service_stats_test.go ================================================ package test 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") } ================================================ FILE: server/router/api/v1/user_service.go ================================================ package v1 import ( "context" "crypto/rand" "encoding/hex" "fmt" "regexp" "strconv" "strings" "time" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/ast" "github.com/pkg/errors" "golang.org/x/crypto/bcrypt" "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" "github.com/usememos/memos/internal/util" "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/auth" "github.com/usememos/memos/store" ) func (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) { currentUser, err := s.fetchCurrentUser(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") } if currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } userFind := &store.FindUser{} if request.Filter != "" { username, err := extractUsernameFromFilter(request.Filter) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) } if username != "" { userFind.Username = &username } } users, err := s.Store.ListUsers(ctx, userFind) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list users: %v", err) } // TODO: Implement proper 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) { // Extract identifier from "users/{id_or_username}" identifier := extractUserIdentifierFromName(request.Name) if identifier == "" { return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %s", request.Name) } var user *store.User var err error // Try to parse as numeric ID first if userID, parseErr := strconv.ParseInt(identifier, 10, 32); parseErr == nil { // It's a numeric ID userID32 := int32(userID) user, err = s.Store.GetUser(ctx, &store.FindUser{ ID: &userID32, }) } else { // It's a username user, err = s.Store.GetUser(ctx, &store.FindUser{ Username: &identifier, }) } 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") } return convertUserFromStore(user), nil } func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserRequest) (*v1pb.User, error) { // Get current user (might be nil for unauthenticated requests) currentUser, _ := s.fetchCurrentUser(ctx) // Check if there are any existing users (for first-time setup detection) limitOne := 1 allUsers, err := s.Store.ListUsers(ctx, &store.FindUser{Limit: &limitOne}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list users: %v", err) } isFirstUser := len(allUsers) == 0 // Check registration settings FIRST (unless it's the very first user) if !isFirstUser { // Only allow user registration if it is enabled in the settings, or if the user is a superuser if currentUser == nil || !isSuperUser(currentUser) { instanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance general setting, error: %v", err) } if instanceGeneralSetting.DisallowUserRegistration { return nil, status.Errorf(codes.PermissionDenied, "user registration is not allowed") } } } // Determine the role to assign var roleToAssign store.Role if isFirstUser { // First-time setup: create the first user as ADMIN (no authentication required) roleToAssign = store.RoleAdmin } else if currentUser != nil && currentUser.Role == store.RoleAdmin { // Authenticated ADMIN 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-ADMIN 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, status.Errorf(codes.Internal, "failed to generate password hash: %v", 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.fetchCurrentUser(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") } // Check permission. // Only allow admin or self to update user. if currentUser.ID != userID && currentUser.Role != store.RoleAdmin { 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, } instanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance general setting: %v", err) } for _, field := range request.UpdateMask.Paths { switch field { case "username": if instanceGeneralSetting.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 instanceGeneralSetting.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": // Validate avatar MIME type to prevent XSS during upload if request.User.AvatarUrl != "" { imageType, _, err := extractImageInfo(request.User.AvatarUrl) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid avatar format: %v", err) } // Only allow safe image formats for avatars allowedAvatarTypes := map[string]bool{ "image/png": true, "image/jpeg": true, "image/jpg": true, "image/gif": true, "image/webp": true, } if !allowedAvatarTypes[imageType] { return nil, status.Errorf(codes.InvalidArgument, "invalid avatar image type: %s. Only PNG, JPEG, GIF, and WebP are allowed", imageType) } } 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 { 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, status.Errorf(codes.Internal, "failed to generate password hash: %v", 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.fetchCurrentUser(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") } if currentUser.ID != userID && currentUser.Role != store.RoleAdmin { 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 getDefaultUserGeneralSetting() *v1pb.UserSetting_GeneralSetting { return &v1pb.UserSetting_GeneralSetting{ Locale: "en", MemoVisibility: "PRIVATE", Theme: "", } } func (s *APIV1Service) GetUserSetting(ctx context.Context, request *v1pb.GetUserSettingRequest) (*v1pb.UserSetting, error) { // Parse resource name: users/{user}/settings/{setting} userID, settingKey, err := ExtractUserIDAndSettingKeyFromName(request.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid resource name: %v", err) } currentUser, err := s.fetchCurrentUser(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.Unauthenticated, "user not authenticated") } // Only allow user to get their own settings if currentUser.ID != userID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } // Convert setting key string to store enum storeKey, err := convertSettingKeyToStore(settingKey) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid setting key: %v", err) } userSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &userID, Key: storeKey, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user setting: %v", err) } return convertUserSettingFromStore(userSetting, userID, storeKey), nil } func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.UpdateUserSettingRequest) (*v1pb.UserSetting, error) { // Parse resource name: users/{user}/settings/{setting} userID, settingKey, err := ExtractUserIDAndSettingKeyFromName(request.Setting.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid resource name: %v", err) } currentUser, err := s.fetchCurrentUser(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.Unauthenticated, "user not authenticated") } // 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") } // Convert setting key string to store enum storeKey, err := convertSettingKeyToStore(settingKey) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid setting key: %v", err) } var updatedSetting *v1pb.UserSetting switch storeKey { case storepb.UserSetting_GENERAL: existingUserSetting, _ := s.Store.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &userID, Key: storeKey, }) generalSetting := &storepb.GeneralUserSetting{} if existingUserSetting != nil { // Start with existing general setting values. generalSetting = existingUserSetting.GetGeneral() } updatedGeneral := &v1pb.UserSetting_GeneralSetting{ MemoVisibility: generalSetting.GetMemoVisibility(), Locale: generalSetting.GetLocale(), Theme: generalSetting.GetTheme(), } incomingGeneral := request.Setting.GetGeneralSetting() if incomingGeneral == nil { return nil, status.Errorf(codes.InvalidArgument, "general setting is required") } for _, field := range request.UpdateMask.Paths { switch field { case "memo_visibility": updatedGeneral.MemoVisibility = incomingGeneral.MemoVisibility case "theme": updatedGeneral.Theme = incomingGeneral.Theme case "locale": updatedGeneral.Locale = incomingGeneral.Locale default: // Ignore unsupported fields. } } updatedSetting = &v1pb.UserSetting{ Name: request.Setting.Name, Value: &v1pb.UserSetting_GeneralSetting_{ GeneralSetting: updatedGeneral, }, } default: return nil, status.Errorf(codes.InvalidArgument, "setting type %s should not be updated via UpdateUserSetting", storeKey.String()) } // Convert API setting to store setting storeSetting, err := convertUserSettingToStore(updatedSetting, userID, storeKey) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "failed to convert setting: %v", err) } // Upsert the setting if _, err := s.Store.UpsertUserSetting(ctx, storeSetting); 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) ListUserSettings(ctx context.Context, request *v1pb.ListUserSettingsRequest) (*v1pb.ListUserSettingsResponse, error) { userID, err := ExtractUserIDFromName(request.Parent) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid parent name: %v", err) } currentUser, err := s.fetchCurrentUser(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.Unauthenticated, "user not authenticated") } // Only allow user to list 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) } settings := make([]*v1pb.UserSetting, 0, len(userSettings)) for _, storeSetting := range userSettings { apiSetting := convertUserSettingFromStore(storeSetting, userID, storeSetting.Key) if apiSetting != nil { settings = append(settings, apiSetting) } } hasGeneral := false for _, setting := range settings { if setting.GetGeneralSetting() != nil { hasGeneral = true } } if !hasGeneral { defaultGeneral := &v1pb.UserSetting{ Name: fmt.Sprintf("users/%d/settings/%s", userID, convertSettingKeyFromStore(storepb.UserSetting_GENERAL)), Value: &v1pb.UserSetting_GeneralSetting_{ GeneralSetting: getDefaultUserGeneralSetting(), }, } settings = append([]*v1pb.UserSetting{defaultGeneral}, settings...) } response := &v1pb.ListUserSettingsResponse{ Settings: settings, TotalSize: int32(len(settings)), } return response, nil } // ListPersonalAccessTokens retrieves all Personal Access Tokens (PATs) for a user. // // Personal Access Tokens are used for: // - Mobile app authentication // - CLI tool authentication // - API client authentication // - Any programmatic access requiring Bearer token auth // // Security: // - Only the token owner can list their tokens // - Returns token metadata only (not the actual token value) // - Invalid or expired tokens are filtered out // // Authentication: Required (session cookie or access token) // Authorization: User can only list their own tokens. func (s *APIV1Service) ListPersonalAccessTokens(ctx context.Context, request *v1pb.ListPersonalAccessTokensRequest) (*v1pb.ListPersonalAccessTokensResponse, error) { userID, err := ExtractUserIDFromName(request.Parent) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) } // Verify permission claims := auth.GetUserClaims(ctx) if claims == nil || claims.UserID != userID { currentUser, _ := s.fetchCurrentUser(ctx) if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleAdmin) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } } tokens, err := s.Store.GetUserPersonalAccessTokens(ctx, userID) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get access tokens: %v", err) } personalAccessTokens := make([]*v1pb.PersonalAccessToken, len(tokens)) for i, token := range tokens { personalAccessTokens[i] = &v1pb.PersonalAccessToken{ Name: fmt.Sprintf("%s/personalAccessTokens/%s", request.Parent, token.TokenId), Description: token.Description, ExpiresAt: token.ExpiresAt, CreatedAt: token.CreatedAt, LastUsedAt: token.LastUsedAt, } } return &v1pb.ListPersonalAccessTokensResponse{PersonalAccessTokens: personalAccessTokens}, nil } // CreatePersonalAccessToken creates a new Personal Access Token (PAT) for a user. // // Use cases: // - User manually creates token in settings for mobile app // - User creates token for CLI tool // - User creates token for third-party integration // // Token properties: // - Random string with memos_pat_ prefix // - SHA-256 hash stored in database // - Optional expiration time (can be never-expiring) // - User-provided description for identification // // Security considerations: // - Full token is only shown ONCE (in this response) // - User should copy and store it securely // - Token can be revoked by deleting it from settings // // Authentication: Required (session cookie or access token) // Authorization: User can only create tokens for themselves. func (s *APIV1Service) CreatePersonalAccessToken(ctx context.Context, request *v1pb.CreatePersonalAccessTokenRequest) (*v1pb.CreatePersonalAccessTokenResponse, error) { userID, err := ExtractUserIDFromName(request.Parent) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) } // Verify permission claims := auth.GetUserClaims(ctx) if claims == nil || claims.UserID != userID { currentUser, _ := s.fetchCurrentUser(ctx) if currentUser == nil || currentUser.ID != userID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } } // Generate PAT tokenID := util.GenUUID() token := auth.GeneratePersonalAccessToken() tokenHash := auth.HashPersonalAccessToken(token) var expiresAt *timestamppb.Timestamp if request.ExpiresInDays > 0 { expiresAt = timestamppb.New(time.Now().AddDate(0, 0, int(request.ExpiresInDays))) } patRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tokenID, TokenHash: tokenHash, Description: request.Description, ExpiresAt: expiresAt, CreatedAt: timestamppb.Now(), } if err := s.Store.AddUserPersonalAccessToken(ctx, userID, patRecord); err != nil { return nil, status.Errorf(codes.Internal, "failed to create access token: %v", err) } return &v1pb.CreatePersonalAccessTokenResponse{ PersonalAccessToken: &v1pb.PersonalAccessToken{ Name: fmt.Sprintf("%s/personalAccessTokens/%s", request.Parent, tokenID), Description: request.Description, ExpiresAt: expiresAt, CreatedAt: patRecord.CreatedAt, }, Token: token, // Only returned on creation }, nil } // DeletePersonalAccessToken revokes a Personal Access Token. // // This endpoint: // 1. Removes the token from the user's access tokens list // 2. Immediately invalidates the token (subsequent API calls with it will fail) // // Use cases: // - User revokes a compromised token // - User removes token for unused app/device // - User cleans up old tokens // // Authentication: Required (session cookie or access token) // Authorization: User can only delete their own tokens. func (s *APIV1Service) DeletePersonalAccessToken(ctx context.Context, request *v1pb.DeletePersonalAccessTokenRequest) (*emptypb.Empty, error) { // Parse name: users/{user_id}/personalAccessTokens/{token_id} parts := strings.Split(request.Name, "/") if len(parts) != 4 || parts[0] != "users" || parts[2] != "personalAccessTokens" { return nil, status.Errorf(codes.InvalidArgument, "invalid personal access token name") } userID, err := util.ConvertStringToInt32(parts[1]) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid user ID: %v", err) } tokenID := parts[3] // Verify permission claims := auth.GetUserClaims(ctx) if claims == nil || claims.UserID != userID { currentUser, _ := s.fetchCurrentUser(ctx) if currentUser == nil || currentUser.ID != userID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } } if err := s.Store.RemoveUserPersonalAccessToken(ctx, userID, tokenID); err != nil { return nil, status.Errorf(codes.Internal, "failed to delete access token: %v", err) } return &emptypb.Empty{}, nil } func (s *APIV1Service) ListUserWebhooks(ctx context.Context, request *v1pb.ListUserWebhooksRequest) (*v1pb.ListUserWebhooksResponse, error) { userID, err := ExtractUserIDFromName(request.Parent) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err) } currentUser, err := s.fetchCurrentUser(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.Unauthenticated, "user not authenticated") } if currentUser.ID != userID && currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } webhooks, err := s.Store.GetUserWebhooks(ctx, userID) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user webhooks: %v", err) } userWebhooks := make([]*v1pb.UserWebhook, 0, len(webhooks)) for _, webhook := range webhooks { userWebhooks = append(userWebhooks, convertUserWebhookFromUserSetting(webhook, userID)) } return &v1pb.ListUserWebhooksResponse{ Webhooks: userWebhooks, }, nil } func (s *APIV1Service) CreateUserWebhook(ctx context.Context, request *v1pb.CreateUserWebhookRequest) (*v1pb.UserWebhook, error) { userID, err := ExtractUserIDFromName(request.Parent) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err) } currentUser, err := s.fetchCurrentUser(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.Unauthenticated, "user not authenticated") } if currentUser.ID != userID && currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if request.Webhook.Url == "" { return nil, status.Errorf(codes.InvalidArgument, "webhook URL is required") } if err := webhook.ValidateURL(strings.TrimSpace(request.Webhook.Url)); err != nil { return nil, err } webhookID := generateUserWebhookID() webhook := &storepb.WebhooksUserSetting_Webhook{ Id: webhookID, Title: request.Webhook.DisplayName, Url: strings.TrimSpace(request.Webhook.Url), } err = s.Store.AddUserWebhook(ctx, userID, webhook) if err != nil { return nil, status.Errorf(codes.Internal, "failed to create webhook: %v", err) } return convertUserWebhookFromUserSetting(webhook, userID), nil } func (s *APIV1Service) UpdateUserWebhook(ctx context.Context, request *v1pb.UpdateUserWebhookRequest) (*v1pb.UserWebhook, error) { if request.Webhook == nil { return nil, status.Errorf(codes.InvalidArgument, "webhook is required") } webhookID, userID, err := parseUserWebhookName(request.Webhook.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) } currentUser, err := s.fetchCurrentUser(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.Unauthenticated, "user not authenticated") } if currentUser.ID != userID && currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } // Get existing webhooks webhooks, err := s.Store.GetUserWebhooks(ctx, userID) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user webhooks: %v", err) } // Find the webhook to update var targetWebhook *storepb.WebhooksUserSetting_Webhook for _, webhook := range webhooks { if webhook.Id == webhookID { targetWebhook = webhook break } } if targetWebhook == nil { return nil, status.Errorf(codes.NotFound, "webhook not found") } // Update the webhook updatedWebhook := &storepb.WebhooksUserSetting_Webhook{ Id: webhookID, Title: targetWebhook.Title, Url: targetWebhook.Url, } if request.UpdateMask != nil { for _, path := range request.UpdateMask.Paths { switch path { case "url": if request.Webhook.Url != "" { trimmed := strings.TrimSpace(request.Webhook.Url) if err := webhook.ValidateURL(trimmed); err != nil { return nil, err } updatedWebhook.Url = trimmed } case "display_name": updatedWebhook.Title = request.Webhook.DisplayName default: // Ignore unsupported fields } } } else { // If no update mask is provided, update all fields if request.Webhook.Url != "" { trimmed := strings.TrimSpace(request.Webhook.Url) if err := webhook.ValidateURL(trimmed); err != nil { return nil, err } updatedWebhook.Url = trimmed } updatedWebhook.Title = request.Webhook.DisplayName } err = s.Store.UpdateUserWebhook(ctx, userID, updatedWebhook) if err != nil { return nil, status.Errorf(codes.Internal, "failed to update webhook: %v", err) } return convertUserWebhookFromUserSetting(updatedWebhook, userID), nil } func (s *APIV1Service) DeleteUserWebhook(ctx context.Context, request *v1pb.DeleteUserWebhookRequest) (*emptypb.Empty, error) { webhookID, userID, err := parseUserWebhookName(request.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) } currentUser, err := s.fetchCurrentUser(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.Unauthenticated, "user not authenticated") } if currentUser.ID != userID && currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } // Get existing webhooks to verify the webhook exists webhooks, err := s.Store.GetUserWebhooks(ctx, userID) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user webhooks: %v", err) } // Check if webhook exists found := false for _, webhook := range webhooks { if webhook.Id == webhookID { found = true break } } if !found { return nil, status.Errorf(codes.NotFound, "webhook not found") } err = s.Store.RemoveUserWebhook(ctx, userID, webhookID) if err != nil { return nil, status.Errorf(codes.Internal, "failed to delete webhook: %v", err) } return &emptypb.Empty{}, nil } // Helper functions for webhook operations // generateUserWebhookID generates a unique ID for user webhooks. func generateUserWebhookID() string { b := make([]byte, 8) rand.Read(b) return hex.EncodeToString(b) } // parseUserWebhookName parses a webhook name and returns the webhook ID and user ID. // Format: users/{user}/webhooks/{webhook}. func parseUserWebhookName(name string) (string, int32, error) { parts := strings.Split(name, "/") if len(parts) != 4 || parts[0] != "users" || parts[2] != "webhooks" { return "", 0, errors.New("invalid webhook name format") } userID, err := strconv.ParseInt(parts[1], 10, 32) if err != nil { return "", 0, errors.New("invalid user ID in webhook name") } return parts[3], int32(userID), nil } // convertUserWebhookFromUserSetting converts a storepb webhook to a v1pb UserWebhook. func convertUserWebhookFromUserSetting(webhook *storepb.WebhooksUserSetting_Webhook, userID int32) *v1pb.UserWebhook { return &v1pb.UserWebhook{ Name: fmt.Sprintf("users/%d/webhooks/%s", userID, webhook.Id), Url: webhook.Url, DisplayName: webhook.Title, // Note: create_time and update_time are not available in the user setting webhook structure // This is a limitation of storing webhooks in user settings vs the dedicated webhook table } } 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("/file/%s/avatar", userpb.Name) } else { userpb.AvatarUrl = user.AvatarURL } } return userpb } func convertUserRoleFromStore(role store.Role) v1pb.User_Role { switch role { 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_ADMIN: return store.RoleAdmin default: return store.RoleUser } } // extractImageInfo extracts image type and base64 data from a data URI. // Data URI format: data:image/png;base64,iVBORw0KGgo... 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 } // Helper functions for user settings // ExtractUserIDAndSettingKeyFromName extracts user ID and setting key from resource name. // e.g., "users/123/settings/general" -> 123, "general". func ExtractUserIDAndSettingKeyFromName(name string) (int32, string, error) { // Expected format: users/{user}/settings/{setting} parts := strings.Split(name, "/") if len(parts) != 4 || parts[0] != "users" || parts[2] != "settings" { return 0, "", errors.Errorf("invalid resource name format: %s", name) } userID, err := util.ConvertStringToInt32(parts[1]) if err != nil { return 0, "", errors.Errorf("invalid user ID: %s", parts[1]) } settingKey := parts[3] return userID, settingKey, nil } // convertSettingKeyToStore converts API setting key to store enum. func convertSettingKeyToStore(key string) (storepb.UserSetting_Key, error) { switch key { case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_GENERAL)]: return storepb.UserSetting_GENERAL, nil case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)]: return storepb.UserSetting_WEBHOOKS, nil default: return storepb.UserSetting_KEY_UNSPECIFIED, errors.Errorf("unknown setting key: %s", key) } } // convertSettingKeyFromStore converts store enum to API setting key. func convertSettingKeyFromStore(key storepb.UserSetting_Key) string { switch key { case storepb.UserSetting_GENERAL: return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_GENERAL)] case storepb.UserSetting_SHORTCUTS: return "SHORTCUTS" // Not defined in API proto case storepb.UserSetting_WEBHOOKS: return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)] default: return "unknown" } } // convertUserSettingFromStore converts store UserSetting to API UserSetting. func convertUserSettingFromStore(storeSetting *storepb.UserSetting, userID int32, key storepb.UserSetting_Key) *v1pb.UserSetting { if storeSetting == nil { // Return default setting if none exists settingKey := convertSettingKeyFromStore(key) setting := &v1pb.UserSetting{ Name: fmt.Sprintf("users/%d/settings/%s", userID, settingKey), } switch key { case storepb.UserSetting_WEBHOOKS: setting.Value = &v1pb.UserSetting_WebhooksSetting_{ WebhooksSetting: &v1pb.UserSetting_WebhooksSetting{ Webhooks: []*v1pb.UserWebhook{}, }, } default: // Default to general setting setting.Value = &v1pb.UserSetting_GeneralSetting_{ GeneralSetting: getDefaultUserGeneralSetting(), } } return setting } settingKey := convertSettingKeyFromStore(storeSetting.Key) setting := &v1pb.UserSetting{ Name: fmt.Sprintf("users/%d/settings/%s", userID, settingKey), } switch storeSetting.Key { case storepb.UserSetting_GENERAL: if general := storeSetting.GetGeneral(); general != nil { setting.Value = &v1pb.UserSetting_GeneralSetting_{ GeneralSetting: &v1pb.UserSetting_GeneralSetting{ Locale: general.Locale, MemoVisibility: general.MemoVisibility, Theme: general.Theme, }, } } else { setting.Value = &v1pb.UserSetting_GeneralSetting_{ GeneralSetting: getDefaultUserGeneralSetting(), } } case storepb.UserSetting_WEBHOOKS: webhooks := storeSetting.GetWebhooks() apiWebhooks := make([]*v1pb.UserWebhook, 0, len(webhooks.Webhooks)) for _, webhook := range webhooks.Webhooks { apiWebhook := &v1pb.UserWebhook{ Name: fmt.Sprintf("users/%d/webhooks/%s", userID, webhook.Id), Url: webhook.Url, DisplayName: webhook.Title, } apiWebhooks = append(apiWebhooks, apiWebhook) } setting.Value = &v1pb.UserSetting_WebhooksSetting_{ WebhooksSetting: &v1pb.UserSetting_WebhooksSetting{ Webhooks: apiWebhooks, }, } default: // Default to general setting if unknown key setting.Value = &v1pb.UserSetting_GeneralSetting_{ GeneralSetting: getDefaultUserGeneralSetting(), } } return setting } // convertUserSettingToStore converts API UserSetting to store UserSetting. func convertUserSettingToStore(apiSetting *v1pb.UserSetting, userID int32, key storepb.UserSetting_Key) (*storepb.UserSetting, error) { storeSetting := &storepb.UserSetting{ UserId: userID, Key: key, } switch key { case storepb.UserSetting_GENERAL: if general := apiSetting.GetGeneralSetting(); general != nil { storeSetting.Value = &storepb.UserSetting_General{ General: &storepb.GeneralUserSetting{ Locale: general.Locale, MemoVisibility: general.MemoVisibility, Theme: general.Theme, }, } } else { return nil, errors.Errorf("general setting is required") } case storepb.UserSetting_WEBHOOKS: if webhooks := apiSetting.GetWebhooksSetting(); webhooks != nil { storeWebhooks := make([]*storepb.WebhooksUserSetting_Webhook, 0, len(webhooks.Webhooks)) for _, webhook := range webhooks.Webhooks { storeWebhook := &storepb.WebhooksUserSetting_Webhook{ Id: extractWebhookIDFromName(webhook.Name), Title: webhook.DisplayName, Url: webhook.Url, } storeWebhooks = append(storeWebhooks, storeWebhook) } storeSetting.Value = &storepb.UserSetting_Webhooks{ Webhooks: &storepb.WebhooksUserSetting{ Webhooks: storeWebhooks, }, } } else { return nil, errors.Errorf("webhooks setting is required") } default: return nil, errors.Errorf("unsupported setting key: %v", key) } return storeSetting, nil } // extractWebhookIDFromName extracts webhook ID from resource name. // e.g., "users/123/webhooks/webhook-id" -> "webhook-id". func extractWebhookIDFromName(name string) string { parts := strings.Split(name, "/") if len(parts) >= 4 && parts[0] == "users" && parts[2] == "webhooks" { return parts[3] } return "" } // extractUsernameFromFilter extracts username from the filter string using CEL. // Supported filter format: "username == 'steven'" // Returns the username value and an error if the filter format is invalid. func extractUsernameFromFilter(filterStr string) (string, error) { filterStr = strings.TrimSpace(filterStr) if filterStr == "" { return "", nil } // Create CEL environment with username variable env, err := cel.NewEnv( cel.Variable("username", cel.StringType), ) if err != nil { return "", errors.Wrap(err, "failed to create CEL environment") } // Parse and check the filter expression celAST, issues := env.Compile(filterStr) if issues != nil && issues.Err() != nil { return "", errors.Wrapf(issues.Err(), "invalid filter expression: %s", filterStr) } // Extract username from the AST username, err := extractUsernameFromAST(celAST.NativeRep().Expr()) if err != nil { return "", err } return username, nil } // extractUsernameFromAST extracts the username value from a CEL AST expression. func extractUsernameFromAST(expr ast.Expr) (string, error) { if expr == nil { return "", errors.New("empty expression") } // Check if this is a call expression (for ==, !=, etc.) if expr.Kind() != ast.CallKind { return "", errors.New("filter must be a comparison expression (e.g., username == 'value')") } call := expr.AsCall() // We only support == operator if call.FunctionName() != "_==_" { return "", errors.Errorf("unsupported operator: %s (only '==' is supported)", call.FunctionName()) } // The call should have exactly 2 arguments args := call.Args() if len(args) != 2 { return "", errors.New("invalid comparison expression") } // Try to extract username from either left or right side if username, ok := extractUsernameFromComparison(args[0], args[1]); ok { return username, nil } if username, ok := extractUsernameFromComparison(args[1], args[0]); ok { return username, nil } return "", errors.New("filter must compare 'username' field with a string constant") } // extractUsernameFromComparison tries to extract username value if left is 'username' ident and right is a string constant. func extractUsernameFromComparison(left, right ast.Expr) (string, bool) { // Check if left side is 'username' identifier if left.Kind() != ast.IdentKind { return "", false } ident := left.AsIdent() if ident != "username" { return "", false } // Right side should be a constant string if right.Kind() != ast.LiteralKind { return "", false } literal := right.AsLiteral() // literal is a ref.Val, we need to get the Go value str, ok := literal.Value().(string) if !ok || str == "" { return "", false } return str, true } // ListUserNotifications lists all notifications for a user. // Notifications are backed by the inbox storage layer and represent activities // that require user attention (e.g., memo comments). func (s *APIV1Service) ListUserNotifications(ctx context.Context, request *v1pb.ListUserNotificationsRequest) (*v1pb.ListUserNotificationsResponse, error) { userID, err := ExtractUserIDFromName(request.Parent) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) } // Verify the requesting user has permission to view these notifications currentUser, err := s.fetchCurrentUser(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.Unauthenticated, "user not authenticated") } if currentUser.ID != userID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } // Fetch inbox items from storage // Filter at database level to only include MEMO_COMMENT notifications (ignore legacy VERSION_UPDATE entries) memoCommentType := storepb.InboxMessage_MEMO_COMMENT inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &userID, MessageType: &memoCommentType, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list inboxes: %v", err) } // Convert storage layer inboxes to API notifications notifications := []*v1pb.UserNotification{} for _, inbox := range inboxes { notification, err := s.convertInboxToUserNotification(ctx, inbox) if err != nil { return nil, status.Errorf(codes.Internal, "failed to convert inbox: %v", err) } notifications = append(notifications, notification) } return &v1pb.ListUserNotificationsResponse{ Notifications: notifications, }, nil } // UpdateUserNotification updates a notification's status (e.g., marking as read/archived). // Only the notification owner can update their notifications. func (s *APIV1Service) UpdateUserNotification(ctx context.Context, request *v1pb.UpdateUserNotificationRequest) (*v1pb.UserNotification, error) { if request.Notification == nil { return nil, status.Errorf(codes.InvalidArgument, "notification is required") } notificationID, err := ExtractNotificationIDFromName(request.Notification.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid notification name: %v", err) } currentUser, err := s.fetchCurrentUser(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.Unauthenticated, "user not authenticated") } // Verify ownership before updating inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{ ID: ¬ificationID, }) 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, "notification not found") } inbox := inboxes[0] if inbox.ReceiverID != currentUser.ID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } // Build update request based on field mask update := &store.UpdateInbox{ ID: notificationID, } for _, path := range request.UpdateMask.Paths { switch path { case "status": // Convert API status enum to storage enum var inboxStatus store.InboxStatus switch request.Notification.Status { case v1pb.UserNotification_UNREAD: inboxStatus = store.UNREAD case v1pb.UserNotification_ARCHIVED: inboxStatus = store.ARCHIVED default: return nil, status.Errorf(codes.InvalidArgument, "invalid status") } update.Status = inboxStatus default: return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", path) } } updatedInbox, err := s.Store.UpdateInbox(ctx, update) if err != nil { return nil, status.Errorf(codes.Internal, "failed to update inbox: %v", err) } notification, err := s.convertInboxToUserNotification(ctx, updatedInbox) if err != nil { return nil, status.Errorf(codes.Internal, "failed to convert inbox: %v", err) } return notification, nil } // DeleteUserNotification permanently deletes a notification. // Only the notification owner can delete their notifications. func (s *APIV1Service) DeleteUserNotification(ctx context.Context, request *v1pb.DeleteUserNotificationRequest) (*emptypb.Empty, error) { notificationID, err := ExtractNotificationIDFromName(request.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid notification name: %v", err) } currentUser, err := s.fetchCurrentUser(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.Unauthenticated, "user not authenticated") } // Verify ownership before deletion inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{ ID: ¬ificationID, }) 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, "notification not found") } inbox := inboxes[0] if inbox.ReceiverID != currentUser.ID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if err := s.Store.DeleteInbox(ctx, &store.DeleteInbox{ ID: notificationID, }); err != nil { return nil, status.Errorf(codes.Internal, "failed to delete inbox: %v", err) } return &emptypb.Empty{}, nil } // convertInboxToUserNotification converts a storage-layer inbox to an API notification. // This handles the mapping between the internal inbox representation and the public API. func (s *APIV1Service) convertInboxToUserNotification(ctx context.Context, inbox *store.Inbox) (*v1pb.UserNotification, error) { notification := &v1pb.UserNotification{ Name: fmt.Sprintf("users/%d/notifications/%d", inbox.ReceiverID, inbox.ID), Sender: fmt.Sprintf("%s%d", UserNamePrefix, inbox.SenderID), CreateTime: timestamppb.New(time.Unix(inbox.CreatedTs, 0)), } // Convert status from storage enum to API enum switch inbox.Status { case store.UNREAD: notification.Status = v1pb.UserNotification_UNREAD case store.ARCHIVED: notification.Status = v1pb.UserNotification_ARCHIVED default: notification.Status = v1pb.UserNotification_STATUS_UNSPECIFIED } // Extract notification type and payload from the inbox message. if inbox.Message != nil { switch inbox.Message.Type { case storepb.InboxMessage_MEMO_COMMENT: notification.Type = v1pb.UserNotification_MEMO_COMMENT default: notification.Type = v1pb.UserNotification_TYPE_UNSPECIFIED } payload, err := s.convertUserNotificationPayload(ctx, inbox.Message) if err != nil { return nil, err } if payload != nil { notification.Payload = &v1pb.UserNotification_MemoComment{ MemoComment: payload, } } } return notification, nil } func (s *APIV1Service) convertUserNotificationPayload(ctx context.Context, message *storepb.InboxMessage) (*v1pb.UserNotification_MemoCommentPayload, error) { memoComment := message.GetMemoComment() if message == nil || message.Type != storepb.InboxMessage_MEMO_COMMENT || memoComment == nil { return nil, nil } commentMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ ID: &memoComment.MemoId, ExcludeContent: true, }) if err != nil { return nil, errors.Wrap(err, "failed to get comment memo") } if commentMemo == nil { return nil, nil } relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ ID: &memoComment.RelatedMemoId, ExcludeContent: true, }) if err != nil { return nil, errors.Wrap(err, "failed to get related memo") } if relatedMemo == nil { return nil, nil } return &v1pb.UserNotification_MemoCommentPayload{ Memo: fmt.Sprintf("%s%s", MemoNamePrefix, commentMemo.UID), RelatedMemo: fmt.Sprintf("%s%s", MemoNamePrefix, relatedMemo.UID), }, nil } // ExtractNotificationIDFromName extracts the notification ID from a resource name. // Expected format: users/{user_id}/notifications/{notification_id}. func ExtractNotificationIDFromName(name string) (int32, error) { pattern := regexp.MustCompile(`^users/(\d+)/notifications/(\d+)$`) matches := pattern.FindStringSubmatch(name) if len(matches) != 3 { return 0, errors.Errorf("invalid notification name: %s", name) } id, err := strconv.Atoi(matches[2]) if err != nil { return 0, errors.Errorf("invalid notification id: %s", matches[2]) } return int32(id), nil } ================================================ FILE: server/router/api/v1/user_service_stats.go ================================================ 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) { instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx) if err != nil { return nil, errors.Wrap(err, "failed to get instance memo related setting") } normalStatus := store.Normal memoFind := &store.FindMemo{ // Exclude comments by default. ExcludeComments: true, ExcludeContent: true, RowStatus: &normalStatus, } currentUser, err := s.fetchCurrentUser(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 { filter := fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID) memoFind.Filters = append(memoFind.Filters, filter) } else if *memoFind.CreatorID != currentUser.ID { memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} } } userMemoStatMap := make(map[int32]*v1pb.UserStats) limit := 1000 offset := 0 memoFind.Limit = &limit memoFind.Offset = &offset for { memos, err := s.Store.ListMemos(ctx, memoFind) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) } if len(memos) == 0 { break } for _, memo := range memos { // Initialize user stats if not exists if _, exists := userMemoStatMap[memo.CreatorID]; !exists { userMemoStatMap[memo.CreatorID] = &v1pb.UserStats{ Name: fmt.Sprintf("users/%d/stats", memo.CreatorID), TagCount: make(map[string]int32), MemoDisplayTimestamps: []*timestamppb.Timestamp{}, PinnedMemos: []string{}, MemoTypeStats: &v1pb.UserStats_MemoTypeStats{ LinkCount: 0, CodeCount: 0, TodoCount: 0, UndoCount: 0, }, } } stats := userMemoStatMap[memo.CreatorID] // Add display timestamp displayTs := memo.CreatedTs if instanceMemoRelatedSetting.DisplayWithUpdateTime { displayTs = memo.UpdatedTs } stats.MemoDisplayTimestamps = append(stats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0))) // Count memo stats stats.TotalMemoCount++ // Count tags and other properties if memo.Payload != nil { for _, tag := range memo.Payload.Tags { stats.TagCount[tag]++ } if memo.Payload.Property != nil { if memo.Payload.Property.HasLink { stats.MemoTypeStats.LinkCount++ } if memo.Payload.Property.HasCode { stats.MemoTypeStats.CodeCount++ } if memo.Payload.Property.HasTaskList { stats.MemoTypeStats.TodoCount++ } if memo.Payload.Property.HasIncompleteTasks { stats.MemoTypeStats.UndoCount++ } } } // Track pinned memos if memo.Pinned { stats.PinnedMemos = append(stats.PinnedMemos, fmt.Sprintf("users/%d/memos/%d", memo.CreatorID, memo.ID)) } } offset += limit } userMemoStats := []*v1pb.UserStats{} for _, userMemoStat := range userMemoStatMap { userMemoStats = append(userMemoStats, userMemoStat) } response := &v1pb.ListAllUserStatsResponse{ Stats: 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.fetchCurrentUser(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} } instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx) if err != nil { return nil, errors.Wrap(err, "failed to get instance 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{} totalMemoCount := int32(0) limit := 1000 offset := 0 memoFind.Limit = &limit memoFind.Offset = &offset for { memos, err := s.Store.ListMemos(ctx, memoFind) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) } if len(memos) == 0 { break } totalMemoCount += int32(len(memos)) for _, memo := range memos { displayTs := memo.CreatedTs if instanceMemoRelatedSetting.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)) } } offset += limit } userStats := &v1pb.UserStats{ Name: fmt.Sprintf("users/%d/stats", userID), MemoDisplayTimestamps: displayTimestamps, TagCount: tagCount, PinnedMemos: pinnedMemos, TotalMemoCount: totalMemoCount, MemoTypeStats: &v1pb.UserStats_MemoTypeStats{ LinkCount: linkCount, CodeCount: codeCount, TodoCount: todoCount, UndoCount: undoCount, }, } return userStats, nil } ================================================ FILE: server/router/api/v1/v1.go ================================================ package v1 import ( "context" "net/http" "connectrpc.com/connect" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "golang.org/x/sync/semaphore" "github.com/usememos/memos/internal/profile" "github.com/usememos/memos/plugin/markdown" v1pb "github.com/usememos/memos/proto/gen/api/v1" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) type APIV1Service struct { v1pb.UnimplementedInstanceServiceServer v1pb.UnimplementedAuthServiceServer v1pb.UnimplementedUserServiceServer v1pb.UnimplementedMemoServiceServer v1pb.UnimplementedAttachmentServiceServer v1pb.UnimplementedShortcutServiceServer v1pb.UnimplementedIdentityProviderServiceServer Secret string Profile *profile.Profile Store *store.Store MarkdownService markdown.Service SSEHub *SSEHub // thumbnailSemaphore limits concurrent thumbnail generation to prevent memory exhaustion thumbnailSemaphore *semaphore.Weighted } func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store) *APIV1Service { markdownService := markdown.NewService( markdown.WithTagExtension(), ) return &APIV1Service{ Secret: secret, Profile: profile, Store: store, MarkdownService: markdownService, SSEHub: NewSSEHub(), thumbnailSemaphore: semaphore.NewWeighted(3), // Limit to 3 concurrent thumbnail generations } } // RegisterGateway registers the gRPC-Gateway and Connect handlers with the given Echo instance. func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Echo) error { // Auth middleware for gRPC-Gateway - runs after routing, has access to method name. // Uses the same PublicMethods config as the Connect AuthInterceptor. authenticator := auth.NewAuthenticator(s.Store, s.Secret) gatewayAuthMiddleware := func(next runtime.HandlerFunc) runtime.HandlerFunc { return func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { ctx := r.Context() // Get the RPC method name from context (set by grpc-gateway after routing) rpcMethod, ok := runtime.RPCMethod(ctx) // Extract credentials from HTTP headers authHeader := r.Header.Get("Authorization") result := authenticator.Authenticate(ctx, authHeader) // Enforce authentication for non-public methods // If rpcMethod cannot be determined, allow through, service layer will handle visibility checks if result == nil && ok && !IsPublicMethod(rpcMethod) { http.Error(w, `{"code": 16, "message": "authentication required"}`, http.StatusUnauthorized) return } // Apply auth result to context (no-op when result is nil for public endpoints) if result != nil { ctx = auth.ApplyToContext(ctx, result) r = r.WithContext(ctx) } next(w, r, pathParams) } } // Create gRPC-Gateway mux with auth middleware. gwMux := runtime.NewServeMux( runtime.WithMiddlewares(gatewayAuthMiddleware), ) if err := v1pb.RegisterInstanceServiceHandlerServer(ctx, gwMux, s); err != nil { return err } if err := v1pb.RegisterAuthServiceHandlerServer(ctx, gwMux, s); err != nil { return err } if err := v1pb.RegisterUserServiceHandlerServer(ctx, gwMux, s); err != nil { return err } if err := v1pb.RegisterMemoServiceHandlerServer(ctx, gwMux, s); err != nil { return err } if err := v1pb.RegisterAttachmentServiceHandlerServer(ctx, gwMux, s); err != nil { return err } if err := v1pb.RegisterShortcutServiceHandlerServer(ctx, gwMux, s); err != nil { return err } if err := v1pb.RegisterIdentityProviderServiceHandlerServer(ctx, gwMux, s); err != nil { return err } gwGroup := echoServer.Group("") gwGroup.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"*"}, })) // Register SSE endpoint with same CORS as rest of /api/v1. gwGroup.GET("/api/v1/sse", func(c *echo.Context) error { return handleSSE(c, s.SSEHub, auth.NewAuthenticator(s.Store, s.Secret)) }) handler := echo.WrapHandler(gwMux) gwGroup.Any("/api/v1/*", handler) gwGroup.Any("/file/*", handler) // Connect handlers for browser clients (replaces grpc-web). logStacktraces := s.Profile.Demo connectInterceptors := connect.WithInterceptors( NewMetadataInterceptor(), // Convert HTTP headers to gRPC metadata first NewLoggingInterceptor(logStacktraces), NewRecoveryInterceptor(logStacktraces), NewAuthInterceptor(s.Store, s.Secret), ) connectMux := http.NewServeMux() connectHandler := NewConnectServiceHandler(s) connectHandler.RegisterConnectHandlers(connectMux, connectInterceptors) // Wrap with CORS for browser access corsHandler := middleware.CORSWithConfig(middleware.CORSConfig{ UnsafeAllowOriginFunc: func(_ *echo.Context, origin string) (string, bool, error) { return origin, true, nil }, AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodOptions}, AllowHeaders: []string{"*"}, AllowCredentials: true, }) connectGroup := echoServer.Group("", corsHandler) connectGroup.Any("/memos.api.v1.*", echo.WrapHandler(connectMux)) return nil } ================================================ FILE: server/router/fileserver/README.md ================================================ # Fileserver Package ## Overview The `fileserver` package handles all binary file serving for Memos using native HTTP handlers. It was created to replace gRPC-based binary serving, which had limitations with HTTP range requests (required for Safari video/audio playback). ## Responsibilities - Serve attachment binary files (images, videos, audio, documents) - Serve user avatar images - Handle HTTP range requests for video/audio streaming - Authenticate requests using JWT tokens or Personal Access Tokens - Check permissions for private content - Generate and serve image thumbnails - Prevent XSS attacks on uploaded content - Support S3 external storage ## Architecture ### Design Principles 1. **Separation of Concerns**: Binary files via HTTP, metadata via gRPC 2. **DRY**: Imports auth constants from `api/v1` package (single source of truth) 3. **Security First**: Authentication, authorization, and XSS prevention 4. **Performance**: Native HTTP streaming with proper caching headers ### Package Structure ``` fileserver/ ├── fileserver.go # Main service and HTTP handlers ├── README.md # This file └── fileserver_test.go # Tests (to be added) ``` ## API Endpoints ### 1. Attachment Binary ``` GET /file/attachments/:uid/:filename[?thumbnail=true] ``` **Parameters:** - `uid` - Attachment unique identifier - `filename` - Original filename - `thumbnail` (optional) - Return thumbnail for images **Authentication:** Required for non-public memos **Response:** - `200 OK` - File content with proper Content-Type - `206 Partial Content` - For range requests (video/audio) - `401 Unauthorized` - Authentication required - `403 Forbidden` - User not authorized - `404 Not Found` - Attachment not found **Headers:** - `Content-Type` - MIME type of the file - `Cache-Control: public, max-age=3600` - `Accept-Ranges: bytes` - For video/audio - `Content-Range` - For partial responses (206) ### 2. User Avatar ``` GET /file/users/:identifier/avatar ``` **Parameters:** - `identifier` - User ID (e.g., `1`) or username (e.g., `steven`) **Authentication:** Not required (avatars are public) **Response:** - `200 OK` - Avatar image (PNG/JPEG) - `404 Not Found` - User not found or no avatar set **Headers:** - `Content-Type` - image/png or image/jpeg - `Cache-Control: public, max-age=3600` ## Authentication ### Supported Methods The fileserver supports the following authentication methods: 1. **JWT Access Token** (`Authorization: Bearer {token}`) - Short-lived tokens (15 minutes) for API access - Stateless validation using JWT signature - Extracts user ID from token claims 2. **Personal Access Token (PAT)** (`Authorization: Bearer {pat}`) - Long-lived tokens for programmatic access - Validates against database for revocation - Prefixed with specific identifier ### Authentication Flow ``` Request → getCurrentUser() ├─→ Try Session Cookie │ ├─→ Parse cookie value │ ├─→ Get user from DB │ ├─→ Validate session │ └─→ Return user (if valid) │ └─→ Try JWT Token ├─→ Parse Authorization header ├─→ Verify JWT signature ├─→ Get user from DB ├─→ Validate token in access tokens list └─→ Return user (if valid) ``` ### Permission Model **Attachments:** - Unlinked: Public (no auth required) - Public memo: Public (no auth required) - Protected memo: Requires authentication - Private memo: Creator only **Avatars:** - Always public (no auth required) ## Key Functions ### HTTP Handlers #### `serveAttachmentFile(c echo.Context) error` Main handler for attachment binary serving. **Flow:** 1. Extract UID from URL parameter 2. Fetch attachment from database 3. Check permissions (memo visibility) 4. Get binary blob (local file, S3, or database) 5. Handle thumbnail request (if applicable) 6. Set security headers (XSS prevention) 7. Serve with range request support (video/audio) #### `serveUserAvatar(c echo.Context) error` Main handler for user avatar serving. **Flow:** 1. Extract identifier (ID or username) from URL 2. Lookup user in database 3. Check if avatar exists 4. Decode base64 data URI 5. Serve with proper content type and caching ### Authentication #### `getCurrentUser(ctx, c) (*store.User, error)` Authenticates request using session cookie or JWT token. #### `authenticateBySession(ctx, cookie) (*store.User, error)` Validates session cookie and returns authenticated user. #### `authenticateByJWT(ctx, token) (*store.User, error)` Validates JWT access token and returns authenticated user. ### Permission Checks #### `checkAttachmentPermission(ctx, c, attachment) error` Validates user has permission to access attachment based on memo visibility. ### File Operations #### `getAttachmentBlob(attachment) ([]byte, error)` Retrieves binary content from local storage, S3, or database. #### `getOrGenerateThumbnail(ctx, attachment) ([]byte, error)` Returns cached thumbnail or generates new one (with semaphore limiting). ### Utilities #### `getUserByIdentifier(ctx, identifier) (*store.User, error)` Finds user by ID (int) or username (string). #### `extractImageInfo(dataURI) (type, base64, error)` Parses data URI to extract MIME type and base64 data. ## Dependencies ### External Packages - `github.com/labstack/echo/v5` - HTTP router and middleware - `github.com/golang-jwt/jwt/v5` - JWT parsing and validation - `github.com/disintegration/imaging` - Image thumbnail generation - `golang.org/x/sync/semaphore` - Concurrency control for thumbnails ### Internal Packages - `server/auth` - Authentication utilities - `store` - Database operations - `internal/profile` - Server configuration - `plugin/storage/s3` - S3 storage client ## Configuration ### Constants Auth-related constants are imported from `server/auth`: - `auth.RefreshTokenCookieName` - "memos_refresh" - `auth.PersonalAccessTokenPrefix` - PAT identifier prefix Package-specific constants: - `ThumbnailCacheFolder` - ".thumbnail_cache" - `thumbnailMaxSize` - 600px - `SupportedThumbnailMimeTypes` - ["image/png", "image/jpeg"] ## Error Handling All handlers return Echo HTTP errors with appropriate status codes: ```go // Bad request echo.NewHTTPError(http.StatusBadRequest, "message") // Unauthorized (no auth) echo.NewHTTPError(http.StatusUnauthorized, "message") // Forbidden (auth but no permission) echo.NewHTTPError(http.StatusForbidden, "message") // Not found echo.NewHTTPError(http.StatusNotFound, "message") // Internal error echo.NewHTTPError(http.StatusInternalServerError, "message").SetInternal(err) ``` ## Security Considerations ### 1. XSS Prevention SVG and HTML files are served as `application/octet-stream` to prevent script execution: ```go if contentType == "image/svg+xml" || contentType == "text/html" || contentType == "application/xhtml+xml" { contentType = "application/octet-stream" } ``` ### 2. Authentication Private content requires valid JWT access token or Personal Access Token. ### 3. Authorization Memo visibility rules enforced before serving attachments. ### 4. Input Validation - Attachment UID validated from database - User identifier validated (ID or username) - Range requests validated before processing ## Performance Optimizations ### 1. Thumbnail Caching Thumbnails cached on disk to avoid regeneration: - Cache location: `{data_dir}/.thumbnail_cache/` - Filename: `{attachment_id}{extension}` - Semaphore limits concurrent generation (max 3) ### 2. HTTP Range Requests Video/audio files use `http.ServeContent()` for efficient streaming: - Automatic range parsing - Efficient memory usage (streaming, not loading full file) - Safari-compatible partial content responses ### 3. Caching Headers All responses include cache headers: ``` Cache-Control: public, max-age=3600 ``` ### 4. S3 External Links S3 files served via presigned URLs (no server download). ## Testing ### Unit Tests (To Add) See SAFARI_FIX.md for recommended test coverage. ### Manual Testing ```bash # Test attachment curl "http://localhost:8081/file/attachments/{uid}/file.jpg" # Test avatar by ID curl "http://localhost:8081/file/users/1/avatar" # Test avatar by username curl "http://localhost:8081/file/users/steven/avatar" # Test range request curl -H "Range: bytes=0-999" "http://localhost:8081/file/attachments/{uid}/video.mp4" ``` ## Future Improvements See SAFARI_FIX.md section "Future Improvements" for planned enhancements. ## Related Documentation - [SAFARI_FIX.md](../../../SAFARI_FIX.md) - Full migration guide - [server/router/api/v1/auth.go](../api/v1/auth.go) - Auth constants source of truth - [RFC 7233](https://tools.ietf.org/html/rfc7233) - HTTP Range Requests spec ================================================ FILE: server/router/fileserver/fileserver.go ================================================ package fileserver import ( "bytes" "context" "encoding/base64" "fmt" "io" "log/slog" "net/http" "os" "path/filepath" "regexp" "strings" "time" "github.com/disintegration/imaging" "github.com/labstack/echo/v5" "github.com/pkg/errors" "golang.org/x/sync/semaphore" "github.com/usememos/memos/internal/profile" "github.com/usememos/memos/internal/util" "github.com/usememos/memos/plugin/storage/s3" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) // Constants for file serving configuration. const ( // ThumbnailCacheFolder is the folder name where thumbnail images are stored. ThumbnailCacheFolder = ".thumbnail_cache" // thumbnailMaxSize is the maximum dimension (width or height) for thumbnails. thumbnailMaxSize = 600 // maxConcurrentThumbnails limits concurrent thumbnail generation to prevent memory exhaustion. maxConcurrentThumbnails = 3 // cacheMaxAge is the max-age value for Cache-Control headers (1 hour). cacheMaxAge = "public, max-age=3600" ) // xssUnsafeTypes contains MIME types that could execute scripts if served directly. // These are served as application/octet-stream to prevent XSS attacks. var xssUnsafeTypes = map[string]bool{ "text/html": true, "text/javascript": true, "application/javascript": true, "application/x-javascript": true, "text/xml": true, "application/xml": true, "application/xhtml+xml": true, "image/svg+xml": true, } // thumbnailSupportedTypes contains image MIME types that support thumbnail generation. var thumbnailSupportedTypes = map[string]bool{ "image/png": true, "image/jpeg": true, "image/heic": true, "image/heif": true, "image/webp": true, } // avatarAllowedTypes contains MIME types allowed for user avatars. var avatarAllowedTypes = map[string]bool{ "image/png": true, "image/jpeg": true, "image/jpg": true, "image/gif": true, "image/webp": true, "image/heic": true, "image/heif": true, } // SupportedThumbnailMimeTypes is the exported list of thumbnail-supported MIME types. var SupportedThumbnailMimeTypes = []string{ "image/png", "image/jpeg", "image/heic", "image/heif", "image/webp", } // dataURIRegex parses data URI format: data:image/png;base64,iVBORw0KGgo... var dataURIRegex = regexp.MustCompile(`^data:(?P[^;]+);base64,(?P.+)`) // FileServerService handles HTTP file serving with proper range request support. type FileServerService struct { Profile *profile.Profile Store *store.Store authenticator *auth.Authenticator // thumbnailSemaphore limits concurrent thumbnail generation. thumbnailSemaphore *semaphore.Weighted } // NewFileServerService creates a new file server service. func NewFileServerService(profile *profile.Profile, store *store.Store, secret string) *FileServerService { return &FileServerService{ Profile: profile, Store: store, authenticator: auth.NewAuthenticator(store, secret), thumbnailSemaphore: semaphore.NewWeighted(maxConcurrentThumbnails), } } // RegisterRoutes registers HTTP file serving routes. func (s *FileServerService) RegisterRoutes(echoServer *echo.Echo) { fileGroup := echoServer.Group("/file") fileGroup.GET("/attachments/:uid/:filename", s.serveAttachmentFile) fileGroup.GET("/users/:identifier/avatar", s.serveUserAvatar) } // ============================================================================= // HTTP Handlers // ============================================================================= // serveAttachmentFile serves attachment binary content using native HTTP. func (s *FileServerService) serveAttachmentFile(c *echo.Context) error { ctx := c.Request().Context() uid := c.Param("uid") wantThumbnail := c.QueryParam("thumbnail") == "true" attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{ UID: &uid, GetBlob: true, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to get attachment").Wrap(err) } if attachment == nil { return echo.NewHTTPError(http.StatusNotFound, "attachment not found") } if err := s.checkAttachmentPermission(ctx, c, attachment); err != nil { return err } contentType := s.sanitizeContentType(attachment.Type) // Stream video/audio to avoid loading entire file into memory. if isMediaType(attachment.Type) { return s.serveMediaStream(c, attachment, contentType) } return s.serveStaticFile(c, attachment, contentType, wantThumbnail) } // serveUserAvatar serves user avatar images. func (s *FileServerService) serveUserAvatar(c *echo.Context) error { ctx := c.Request().Context() identifier := c.Param("identifier") user, err := s.getUserByIdentifier(ctx, identifier) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to get user").Wrap(err) } if user == nil { return echo.NewHTTPError(http.StatusNotFound, "user not found") } if user.AvatarURL == "" { return echo.NewHTTPError(http.StatusNotFound, "avatar not found") } imageType, imageData, err := s.parseDataURI(user.AvatarURL) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to parse avatar data").Wrap(err) } if !avatarAllowedTypes[imageType] { return echo.NewHTTPError(http.StatusBadRequest, "invalid avatar image type") } setSecurityHeaders(c) c.Response().Header().Set(echo.HeaderContentType, imageType) c.Response().Header().Set(echo.HeaderCacheControl, cacheMaxAge) return c.Blob(http.StatusOK, imageType, imageData) } // ============================================================================= // File Serving Methods // ============================================================================= // serveMediaStream serves video/audio files using streaming to avoid memory exhaustion. func (s *FileServerService) serveMediaStream(c *echo.Context, attachment *store.Attachment, contentType string) error { setSecurityHeaders(c) setMediaHeaders(c, contentType, attachment.Type) switch attachment.StorageType { case storepb.AttachmentStorageType_LOCAL: filePath, err := s.resolveLocalPath(attachment.Reference) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to resolve file path").Wrap(err) } http.ServeFile(c.Response(), c.Request(), filePath) return nil case storepb.AttachmentStorageType_S3: presignURL, err := s.getS3PresignedURL(c.Request().Context(), attachment) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate presigned URL").Wrap(err) } return c.Redirect(http.StatusTemporaryRedirect, presignURL) default: // Database storage fallback. modTime := time.Unix(attachment.UpdatedTs, 0) http.ServeContent(c.Response(), c.Request(), attachment.Filename, modTime, bytes.NewReader(attachment.Blob)) return nil } } // serveStaticFile serves non-streaming files (images, documents, etc.). func (s *FileServerService) serveStaticFile(c *echo.Context, attachment *store.Attachment, contentType string, wantThumbnail bool) error { blob, err := s.getAttachmentBlob(attachment) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to get attachment blob").Wrap(err) } // Generate thumbnail for supported image types. if wantThumbnail && thumbnailSupportedTypes[attachment.Type] { if thumbnailBlob, err := s.getOrGenerateThumbnail(c.Request().Context(), attachment); err != nil { slog.Warn("failed to get thumbnail", "error", err) } else { blob = thumbnailBlob } } setSecurityHeaders(c) setMediaHeaders(c, contentType, attachment.Type) // Force download for non-media files to prevent XSS execution. if !strings.HasPrefix(contentType, "image/") && contentType != "application/pdf" { c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%q", attachment.Filename)) } return c.Blob(http.StatusOK, contentType, blob) } // ============================================================================= // Storage Operations // ============================================================================= // getAttachmentBlob retrieves the binary content of an attachment from storage. func (s *FileServerService) getAttachmentBlob(attachment *store.Attachment) ([]byte, error) { switch attachment.StorageType { case storepb.AttachmentStorageType_LOCAL: return s.readLocalFile(attachment.Reference) case storepb.AttachmentStorageType_S3: return s.downloadFromS3(attachment) default: return attachment.Blob, nil } } // getAttachmentReader returns a reader for streaming attachment content. func (s *FileServerService) getAttachmentReader(attachment *store.Attachment) (io.ReadCloser, error) { switch attachment.StorageType { case storepb.AttachmentStorageType_LOCAL: filePath, err := s.resolveLocalPath(attachment.Reference) if err != nil { return nil, err } file, err := os.Open(filePath) if err != nil { if os.IsNotExist(err) { return nil, errors.Wrap(err, "file not found") } return nil, errors.Wrap(err, "failed to open file") } return file, nil case storepb.AttachmentStorageType_S3: s3Client, s3Object, err := s.createS3Client(attachment) if err != nil { return nil, err } reader, err := s3Client.GetObjectStream(context.Background(), s3Object.Key) if err != nil { return nil, errors.Wrap(err, "failed to stream from S3") } return reader, nil default: return io.NopCloser(bytes.NewReader(attachment.Blob)), nil } } // resolveLocalPath converts a storage reference to an absolute file path. func (s *FileServerService) resolveLocalPath(reference string) (string, error) { filePath := filepath.FromSlash(reference) if !filepath.IsAbs(filePath) { filePath = filepath.Join(s.Profile.Data, filePath) } return filePath, nil } // readLocalFile reads the entire contents of a local file. func (s *FileServerService) readLocalFile(reference string) ([]byte, error) { filePath, err := s.resolveLocalPath(reference) if err != nil { return nil, err } file, err := os.Open(filePath) if err != nil { if os.IsNotExist(err) { return nil, errors.Wrap(err, "file not found") } return nil, errors.Wrap(err, "failed to open file") } defer file.Close() blob, err := io.ReadAll(file) if err != nil { return nil, errors.Wrap(err, "failed to read file") } return blob, nil } // createS3Client creates an S3 client from attachment payload. func (*FileServerService) createS3Client(attachment *store.Attachment) (*s3.Client, *storepb.AttachmentPayload_S3Object, error) { if attachment.Payload == nil { return nil, nil, errors.New("attachment payload is missing") } s3Object := attachment.Payload.GetS3Object() if s3Object == nil { return nil, nil, errors.New("S3 object payload is missing") } if s3Object.S3Config == nil { return nil, nil, errors.New("S3 config is missing") } if s3Object.Key == "" { return nil, nil, errors.New("S3 object key is missing") } client, err := s3.NewClient(context.Background(), s3Object.S3Config) if err != nil { return nil, nil, errors.Wrap(err, "failed to create S3 client") } return client, s3Object, nil } // downloadFromS3 downloads the entire object from S3. func (s *FileServerService) downloadFromS3(attachment *store.Attachment) ([]byte, error) { client, s3Object, err := s.createS3Client(attachment) if err != nil { return nil, err } blob, err := client.GetObject(context.Background(), s3Object.Key) if err != nil { return nil, errors.Wrap(err, "failed to download from S3") } return blob, nil } // getS3PresignedURL generates a presigned URL for direct S3 access. func (s *FileServerService) getS3PresignedURL(ctx context.Context, attachment *store.Attachment) (string, error) { client, s3Object, err := s.createS3Client(attachment) if err != nil { return "", err } url, err := client.PresignGetObject(ctx, s3Object.Key) if err != nil { return "", errors.Wrap(err, "failed to presign URL") } return url, nil } // ============================================================================= // Thumbnail Generation // ============================================================================= // getOrGenerateThumbnail returns the thumbnail image of the attachment. // Uses semaphore to limit concurrent thumbnail generation and prevent memory exhaustion. func (s *FileServerService) getOrGenerateThumbnail(ctx context.Context, attachment *store.Attachment) ([]byte, error) { thumbnailPath, err := s.getThumbnailPath(attachment) if err != nil { return nil, err } // Fast path: return cached thumbnail if exists. if blob, err := s.readCachedThumbnail(thumbnailPath); err == nil { return blob, nil } // Acquire semaphore to limit concurrent generation. if err := s.thumbnailSemaphore.Acquire(ctx, 1); err != nil { return nil, errors.Wrap(err, "failed to acquire semaphore") } defer s.thumbnailSemaphore.Release(1) // Double-check after acquiring semaphore (another goroutine may have generated it). if blob, err := s.readCachedThumbnail(thumbnailPath); err == nil { return blob, nil } return s.generateThumbnail(attachment, thumbnailPath) } // getThumbnailPath returns the file path for a cached thumbnail. func (s *FileServerService) getThumbnailPath(attachment *store.Attachment) (string, error) { cacheFolder := filepath.Join(s.Profile.Data, ThumbnailCacheFolder) if err := os.MkdirAll(cacheFolder, os.ModePerm); err != nil { return "", errors.Wrap(err, "failed to create thumbnail cache folder") } filename := fmt.Sprintf("%d%s", attachment.ID, filepath.Ext(attachment.Filename)) return filepath.Join(cacheFolder, filename), nil } // readCachedThumbnail reads a thumbnail from the cache directory. func (*FileServerService) readCachedThumbnail(path string) ([]byte, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() return io.ReadAll(file) } // generateThumbnail creates a new thumbnail and saves it to disk. func (s *FileServerService) generateThumbnail(attachment *store.Attachment, thumbnailPath string) ([]byte, error) { reader, err := s.getAttachmentReader(attachment) if err != nil { return nil, errors.Wrap(err, "failed to get attachment reader") } defer reader.Close() img, err := imaging.Decode(reader, imaging.AutoOrientation(true)) if err != nil { return nil, errors.Wrap(err, "failed to decode image") } width, height := img.Bounds().Dx(), img.Bounds().Dy() thumbnailWidth, thumbnailHeight := calculateThumbnailDimensions(width, height) thumbnailImage := imaging.Resize(img, thumbnailWidth, thumbnailHeight, imaging.Lanczos) if err := imaging.Save(thumbnailImage, thumbnailPath); err != nil { return nil, errors.Wrap(err, "failed to save thumbnail") } return s.readCachedThumbnail(thumbnailPath) } // calculateThumbnailDimensions calculates the target dimensions for a thumbnail. // The largest dimension is constrained to thumbnailMaxSize while maintaining aspect ratio. // Small images are not enlarged. func calculateThumbnailDimensions(width, height int) (int, int) { if max(width, height) <= thumbnailMaxSize { return width, height } if width >= height { return thumbnailMaxSize, 0 // Landscape: constrain width. } return 0, thumbnailMaxSize // Portrait: constrain height. } // ============================================================================= // Authentication & Authorization // ============================================================================= // checkAttachmentPermission verifies the user has permission to access the attachment. func (s *FileServerService) checkAttachmentPermission(ctx context.Context, c *echo.Context, attachment *store.Attachment) error { // For unlinked attachments, only the creator can access. if attachment.MemoID == nil { user, err := s.getCurrentUser(ctx, c) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to get current user").Wrap(err) } if user == nil { return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized access") } if user.ID != attachment.CreatorID && user.Role != store.RoleAdmin { return echo.NewHTTPError(http.StatusForbidden, "forbidden access") } return nil } memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: attachment.MemoID}) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to find memo").Wrap(err) } if memo == nil { return echo.NewHTTPError(http.StatusNotFound, "memo not found") } if memo.Visibility == store.Public { return nil } // Check share token fallback: allow access if request carries a valid, non-expired share token // that was issued for this specific memo. This covers attachment requests made from the shared // memo page for private or protected memos. if shareToken := (*c).QueryParam("share_token"); shareToken != "" { ms, err := s.Store.GetMemoShare(ctx, &store.FindMemoShare{UID: &shareToken}) if err == nil && ms != nil && !isMemoShareExpired(ms) && ms.MemoID == memo.ID { return nil } } user, err := s.getCurrentUser(ctx, c) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to get current user").Wrap(err) } if user == nil { return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized access") } if memo.Visibility == store.Private && user.ID != memo.CreatorID && user.Role != store.RoleAdmin { return echo.NewHTTPError(http.StatusForbidden, "forbidden access") } return nil } // getCurrentUser retrieves the current authenticated user from the request. // Authentication priority: Bearer token (Access Token V2 or PAT) > Refresh token cookie. func (s *FileServerService) getCurrentUser(ctx context.Context, c *echo.Context) (*store.User, error) { authHeader := c.Request().Header.Get(echo.HeaderAuthorization) cookieHeader := c.Request().Header.Get("Cookie") return s.authenticator.AuthenticateToUser(ctx, authHeader, cookieHeader) } // getUserByIdentifier finds a user by either ID or username. func (s *FileServerService) getUserByIdentifier(ctx context.Context, identifier string) (*store.User, error) { if userID, err := util.ConvertStringToInt32(identifier); err == nil { return s.Store.GetUser(ctx, &store.FindUser{ID: &userID}) } return s.Store.GetUser(ctx, &store.FindUser{Username: &identifier}) } // ============================================================================= // Helper Functions // ============================================================================= // sanitizeContentType converts potentially dangerous MIME types to safe alternatives. func (*FileServerService) sanitizeContentType(mimeType string) string { contentType := mimeType if strings.HasPrefix(contentType, "text/") { contentType += "; charset=utf-8" } // Normalize for case-insensitive lookup. if xssUnsafeTypes[strings.ToLower(mimeType)] { return "application/octet-stream" } return contentType } // parseDataURI extracts MIME type and decoded data from a data URI. func (*FileServerService) parseDataURI(dataURI string) (string, []byte, error) { matches := dataURIRegex.FindStringSubmatch(dataURI) if len(matches) != 3 { return "", nil, errors.New("invalid data URI format") } imageType := matches[1] imageData, err := base64.StdEncoding.DecodeString(matches[2]) if err != nil { return "", nil, errors.Wrap(err, "failed to decode base64 data") } return imageType, imageData, nil } // isMediaType checks if the MIME type is video or audio. func isMediaType(mimeType string) bool { return strings.HasPrefix(mimeType, "video/") || strings.HasPrefix(mimeType, "audio/") } // setSecurityHeaders sets common security headers for all responses. func setSecurityHeaders(c *echo.Context) { h := c.Response().Header() h.Set("X-Content-Type-Options", "nosniff") h.Set("X-Frame-Options", "DENY") h.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline';") } // setMediaHeaders sets headers for media file responses. func setMediaHeaders(c *echo.Context, contentType, originalType string) { h := c.Response().Header() h.Set(echo.HeaderContentType, contentType) h.Set(echo.HeaderCacheControl, cacheMaxAge) // Support HDR/wide color gamut for images and videos. if strings.HasPrefix(originalType, "image/") || strings.HasPrefix(originalType, "video/") { h.Set("Color-Gamut", "srgb, p3, rec2020") } } // isMemoShareExpired returns true if the share has a defined expiry that has already passed. func isMemoShareExpired(ms *store.MemoShare) bool { return ms.ExpiresTs != nil && time.Now().Unix() > *ms.ExpiresTs } ================================================ FILE: server/router/fileserver/fileserver_test.go ================================================ package fileserver import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/labstack/echo/v5" "github.com/stretchr/testify/require" "github.com/usememos/memos/internal/profile" "github.com/usememos/memos/plugin/markdown" apiv1 "github.com/usememos/memos/proto/gen/api/v1" "github.com/usememos/memos/server/auth" apiv1service "github.com/usememos/memos/server/router/api/v1" "github.com/usememos/memos/store" teststore "github.com/usememos/memos/store/test" ) func TestServeAttachmentFile_ShareTokenAllowsDirectMemoAttachment(t *testing.T) { ctx := context.Background() svc, fs, _, cleanup := newShareAttachmentTestServices(ctx, t) defer cleanup() creator, err := svc.Store.CreateUser(ctx, &store.User{ Username: "share-parent-owner", Role: store.RoleUser, Email: "share-parent-owner@example.com", }) require.NoError(t, err) creatorCtx := context.WithValue(ctx, auth.UserIDContextKey, creator.ID) attachment, err := svc.CreateAttachment(creatorCtx, &apiv1.CreateAttachmentRequest{ Attachment: &apiv1.Attachment{ Filename: "memo.txt", Type: "text/plain", Content: []byte("memo attachment"), }, }) require.NoError(t, err) parentMemo, err := svc.CreateMemo(creatorCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "shared parent", Visibility: apiv1.Visibility_PROTECTED, Attachments: []*apiv1.Attachment{ {Name: attachment.Name}, }, }, }) require.NoError(t, err) share, err := svc.CreateMemoShare(creatorCtx, &apiv1.CreateMemoShareRequest{ Parent: parentMemo.Name, MemoShare: &apiv1.MemoShare{}, }) require.NoError(t, err) shareToken := share.Name[strings.LastIndex(share.Name, "/")+1:] e := echo.New() fs.RegisterRoutes(e) req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/file/%s/%s?share_token=%s", attachment.Name, attachment.Filename, shareToken), nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) require.Equal(t, http.StatusOK, rec.Code) require.Equal(t, "memo attachment", rec.Body.String()) } func TestServeAttachmentFile_ShareTokenRejectsCommentAttachment(t *testing.T) { ctx := context.Background() svc, fs, _, cleanup := newShareAttachmentTestServices(ctx, t) defer cleanup() creator, err := svc.Store.CreateUser(ctx, &store.User{ Username: "private-parent-owner", Role: store.RoleUser, Email: "private-parent-owner@example.com", }) require.NoError(t, err) creatorCtx := context.WithValue(ctx, auth.UserIDContextKey, creator.ID) commenter, err := svc.Store.CreateUser(ctx, &store.User{ Username: "share-commenter", Role: store.RoleUser, Email: "share-commenter@example.com", }) require.NoError(t, err) commenterCtx := context.WithValue(ctx, auth.UserIDContextKey, commenter.ID) parentMemo, err := svc.CreateMemo(creatorCtx, &apiv1.CreateMemoRequest{ Memo: &apiv1.Memo{ Content: "shared parent", Visibility: apiv1.Visibility_PROTECTED, }, }) require.NoError(t, err) commentAttachment, err := svc.CreateAttachment(commenterCtx, &apiv1.CreateAttachmentRequest{ Attachment: &apiv1.Attachment{ Filename: "comment.txt", Type: "text/plain", Content: []byte("comment attachment"), }, }) require.NoError(t, err) _, err = svc.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{ Name: parentMemo.Name, Comment: &apiv1.Memo{ Content: "comment with attachment", Visibility: apiv1.Visibility_PROTECTED, Attachments: []*apiv1.Attachment{ {Name: commentAttachment.Name}, }, }, }) require.NoError(t, err) share, err := svc.CreateMemoShare(creatorCtx, &apiv1.CreateMemoShareRequest{ Parent: parentMemo.Name, MemoShare: &apiv1.MemoShare{}, }) require.NoError(t, err) shareToken := share.Name[strings.LastIndex(share.Name, "/")+1:] e := echo.New() fs.RegisterRoutes(e) req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/file/%s/%s?share_token=%s", commentAttachment.Name, commentAttachment.Filename, shareToken), nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) require.Equal(t, http.StatusUnauthorized, rec.Code) } func newShareAttachmentTestServices(ctx context.Context, t *testing.T) (*apiv1service.APIV1Service, *FileServerService, *store.Store, func()) { t.Helper() testStore := teststore.NewTestingStore(ctx, t) testProfile := &profile.Profile{ Demo: true, Version: "test-1.0.0", InstanceURL: "http://localhost:8080", Driver: "sqlite", DSN: ":memory:", Data: t.TempDir(), } secret := "test-secret" markdownService := markdown.NewService(markdown.WithTagExtension()) apiService := &apiv1service.APIV1Service{ Secret: secret, Profile: testProfile, Store: testStore, MarkdownService: markdownService, SSEHub: apiv1service.NewSSEHub(), } fileService := NewFileServerService(testProfile, testStore, secret) return apiService, fileService, testStore, func() { testStore.Close() } } ================================================ FILE: server/router/frontend/frontend.go ================================================ package frontend import ( "context" "embed" "io/fs" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/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 } // For index.html and root path, set no-cache headers to prevent browser caching // This prevents sensitive data from being accessible via browser back button after logout if c.Path() == "/" || c.Path() == "/index.html" { c.Response().Header().Set(echo.HeaderCacheControl, "no-cache, no-store, must-revalidate") c.Response().Header().Set("Pragma", "no-cache") c.Response().Header().Set("Expires", "0") return false } // Set Cache-Control header for static assets. // Since Vite generates content-hashed filenames (e.g., index-BtVjejZf.js), // we can cache aggressively but use immutable to prevent revalidation checks. // For frequently redeployed instances, use shorter max-age (1 hour) to avoid // serving stale assets after redeployment. c.Response().Header().Set(echo.HeaderCacheControl, "public, max-age=3600, immutable") // 1 hour 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) fs.FS { sub, err := fs.Sub(embeddedFiles, path) if err != nil { panic(err) } return sub } ================================================ FILE: server/router/mcp/README.md ================================================ # MCP Server This package implements a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server embedded in the Memos HTTP process. It exposes memo operations as MCP tools, making Memos accessible to any MCP-compatible AI client (Claude Desktop, Cursor, Zed, etc.). ## Endpoint ``` POST /mcp (tool calls, initialize) GET /mcp (optional SSE stream for server-to-client messages) ``` Transport: [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports) (single endpoint, MCP spec 2025-03-26). ## Capabilities The server advertises the following MCP capabilities: | Capability | Enabled | Details | |---|---|---| | Tools | Yes | List changed notifications supported | | Resources | Yes | Subscribe + list changed supported | | Prompts | Yes | List changed notifications supported | | Logging | Yes | Structured log events | ## Authentication Every request must include a Personal Access Token (PAT): ``` Authorization: Bearer ``` PATs are long-lived tokens created in Settings → My Account → Access Tokens. Short-lived JWT session tokens are also accepted. Requests without a valid token receive `HTTP 401`. ## Tools ### Memo Tools | Tool | Description | Required params | Optional params | |---|---|---|---| | `list_memos` | List memos | — | `page_size`, `page`, `state`, `order_by_pinned`, `filter` (CEL) | | `get_memo` | Get a single memo | `name` | — | | `search_memos` | Full-text search | `query` | — | | `create_memo` | Create a memo | `content` | `visibility` | | `update_memo` | Update a memo | `name` | `content`, `visibility`, `pinned`, `state` | | `delete_memo` | Delete a memo | `name` | — | | `list_memo_comments` | List comments | `name` | — | | `create_memo_comment` | Add a comment | `name`, `content` | — | ### Tag Tools | Tool | Description | Required params | |---|---|---| | `list_tags` | List all tags with counts | — | ### Attachment Tools | Tool | Description | Required params | Optional params | |---|---|---|---| | `list_attachments` | List user's attachments | — | `page_size`, `page`, `memo` | | `get_attachment` | Get attachment metadata | `name` | — | | `delete_attachment` | Delete an attachment | `name` | — | | `link_attachment_to_memo` | Link attachment to memo | `name`, `memo` | — | ### Relation Tools | Tool | Description | Required params | Optional params | |---|---|---|---| | `list_memo_relations` | List relations (refs + comments) | `name` | `type` | | `create_memo_relation` | Create a reference relation | `name`, `related_memo` | — | | `delete_memo_relation` | Delete a reference relation | `name`, `related_memo` | — | ### Reaction Tools | Tool | Description | Required params | |---|---|---| | `list_reactions` | List reactions on a memo | `name` | | `upsert_reaction` | Add a reaction emoji | `name`, `reaction_type` | | `delete_reaction` | Remove a reaction | `id` | ## Resources | URI Template | Description | MIME Type | |---|---|---| | `memo://memos/{uid}` | Memo content with YAML frontmatter | `text/markdown` | ## Prompts | Prompt | Description | Arguments | |---|---|---| | `capture` | Quick-save a thought as a memo | `content` (required), `tags`, `visibility` | | `review` | Search and summarize memos on a topic | `topic` (required) | | `daily_digest` | Summarize recent memo activity | `days` | | `organize` | Suggest tags/relations for unorganized memos | `scope` | ## Resource Names - Memos: `memos/` (e.g. `memos/abc123`) - Attachments: `attachments/` (e.g. `attachments/def456`) ## Connecting Claude Code ```bash claude mcp add --transport http memos http://localhost:5230/mcp \ --header "Authorization: Bearer " ``` Use `--scope user` to make it available across all projects: ```bash claude mcp add --scope user --transport http memos http://localhost:5230/mcp \ --header "Authorization: Bearer " ``` ## Package Structure | File | Responsibility | |---|---| | `mcp.go` | `MCPService` struct, constructor, route registration, auth middleware | | `tools_memo.go` | Memo CRUD tools + helpers (JSON types, visibility/access checks) | | `tools_tag.go` | Tag listing tool | | `tools_attachment.go` | Attachment listing, metadata, deletion, linking tools | | `tools_relation.go` | Memo relation (reference) tools | | `tools_reaction.go` | Reaction (emoji) tools | | `resources_memo.go` | Memo resource template handler | | `prompts.go` | Prompt handlers (capture, review, daily_digest, organize) | ================================================ FILE: server/router/mcp/mcp.go ================================================ package mcp import ( "net/http" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" mcpserver "github.com/mark3labs/mcp-go/server" "github.com/usememos/memos/internal/profile" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) type MCPService struct { profile *profile.Profile store *store.Store authenticator *auth.Authenticator } func NewMCPService(profile *profile.Profile, store *store.Store, secret string) *MCPService { return &MCPService{ profile: profile, store: store, authenticator: auth.NewAuthenticator(store, secret), } } func (s *MCPService) RegisterRoutes(echoServer *echo.Echo) { mcpSrv := mcpserver.NewMCPServer("Memos", "1.0.0", mcpserver.WithToolCapabilities(true), mcpserver.WithResourceCapabilities(true, true), mcpserver.WithPromptCapabilities(true), mcpserver.WithLogging(), ) s.registerMemoTools(mcpSrv) s.registerTagTools(mcpSrv) s.registerAttachmentTools(mcpSrv) s.registerRelationTools(mcpSrv) s.registerReactionTools(mcpSrv) s.registerMemoResources(mcpSrv) s.registerPrompts(mcpSrv) httpHandler := mcpserver.NewStreamableHTTPServer(mcpSrv) mcpGroup := echoServer.Group("") mcpGroup.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"*"}, })) mcpGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { authHeader := c.Request().Header.Get("Authorization") if authHeader != "" { result := s.authenticator.Authenticate(c.Request().Context(), authHeader) if result == nil { return c.JSON(http.StatusUnauthorized, map[string]string{"message": "invalid or expired token"}) } ctx := auth.ApplyToContext(c.Request().Context(), result) c.SetRequest(c.Request().WithContext(ctx)) } return next(c) } }) mcpGroup.Any("/mcp", echo.WrapHandler(httpHandler)) } ================================================ FILE: server/router/mcp/prompts.go ================================================ package mcp import ( "context" "errors" "fmt" "strings" "github.com/mark3labs/mcp-go/mcp" mcpserver "github.com/mark3labs/mcp-go/server" ) func (s *MCPService) registerPrompts(mcpSrv *mcpserver.MCPServer) { // capture — turns free-form user input into a structured create_memo call. mcpSrv.AddPrompt( mcp.NewPrompt("capture", mcp.WithPromptDescription("Capture a thought, idea, or note as a new memo. "+ "Use this prompt when the user wants to quickly save something. "+ "The assistant will call create_memo with the provided content."), mcp.WithArgument("content", mcp.ArgumentDescription("The text to save as a memo"), mcp.RequiredArgument(), ), mcp.WithArgument("tags", mcp.ArgumentDescription("Comma-separated tags to apply, e.g. \"work,project\""), ), mcp.WithArgument("visibility", mcp.ArgumentDescription("Memo visibility: PRIVATE (default), PROTECTED, or PUBLIC"), ), ), s.handleCapturePrompt, ) // review — surfaces existing memos on a topic for summarisation. mcpSrv.AddPrompt( mcp.NewPrompt("review", mcp.WithPromptDescription("Search and review memos on a given topic. "+ "The assistant will call search_memos and summarise the results, "+ "including memo resource URIs for easy reference."), mcp.WithArgument("topic", mcp.ArgumentDescription("Topic or keyword to search for"), mcp.RequiredArgument(), ), ), s.handleReviewPrompt, ) // daily_digest — summarise recent activity. mcpSrv.AddPrompt( mcp.NewPrompt("daily_digest", mcp.WithPromptDescription("Get a summary of recent memo activity. "+ "The assistant will list recent memos, group them by tags, and highlight "+ "any incomplete tasks or pinned items."), mcp.WithArgument("days", mcp.ArgumentDescription("Number of days to look back (default: 1)"), ), ), s.handleDailyDigestPrompt, ) // organize — suggest tags and relations for untagged memos. mcpSrv.AddPrompt( mcp.NewPrompt("organize", mcp.WithPromptDescription("Analyze untagged or loosely organized memos and suggest "+ "tags, relations, and groupings to improve discoverability."), mcp.WithArgument("scope", mcp.ArgumentDescription("Scope of analysis: \"untagged\" (default) for memos without tags, \"all\" for all recent memos"), ), ), s.handleOrganizePrompt, ) } func (*MCPService) handleCapturePrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { content := req.Params.Arguments["content"] if content == "" { return nil, errors.New("content argument is required") } tags := req.Params.Arguments["tags"] visibility := req.Params.Arguments["visibility"] if visibility == "" { visibility = "PRIVATE" } var sb strings.Builder sb.WriteString("Save the following as a new memo using the create_memo tool.\n\n") fmt.Fprintf(&sb, "Visibility: %s\n\n", visibility) sb.WriteString("Content:\n") sb.WriteString(content) if tags != "" { fmt.Fprintf(&sb, "\n\nAppend these tags inline using #tag syntax: %s", tags) } sb.WriteString("\n\nAfter creating the memo, confirm by showing the memo resource name (e.g. memo://memos/) so it can be referenced later.") return &mcp.GetPromptResult{ Description: "Capture a memo", Messages: []mcp.PromptMessage{ mcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(sb.String())), }, }, nil } func (*MCPService) handleReviewPrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { topic := req.Params.Arguments["topic"] if topic == "" { return nil, errors.New("topic argument is required") } instruction := fmt.Sprintf( `Use the search_memos tool to find memos about %q, then: 1. Group results by theme or tag 2. For each memo, include its resource reference (memo://memos/) so the user can access it directly 3. Provide a concise summary of what has been written on this topic 4. Highlight any memos with incomplete tasks (has_incomplete_tasks) 5. Note the most recent update times to show currency of the information`, topic, ) return &mcp.GetPromptResult{ Description: fmt.Sprintf("Review memos about %q", topic), Messages: []mcp.PromptMessage{ mcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(instruction)), }, }, nil } func (*MCPService) handleDailyDigestPrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { days := req.Params.Arguments["days"] if days == "" { days = "1" } instruction := fmt.Sprintf( `Generate a daily digest of memo activity from the last %s day(s): 1. Use list_memos to fetch recent memos (order by update time, check multiple pages if needed) 2. Use list_tags to get the current tag landscape 3. Group memos by tags and summarize each group 4. Highlight: - Pinned memos (important items) - Memos with incomplete tasks (action items) - New memos created vs. memos updated 5. Include memo resource references (memo://memos/) for each item 6. End with a brief "action items" section listing incomplete tasks across all memos`, days, ) return &mcp.GetPromptResult{ Description: "Daily memo digest", Messages: []mcp.PromptMessage{ mcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(instruction)), }, }, nil } func (*MCPService) handleOrganizePrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { scope := req.Params.Arguments["scope"] if scope == "" { scope = "untagged" } var filter string if scope == "untagged" { filter = `Focus on memos that have no tags. Use list_memos and identify those with empty tag arrays.` } else { filter = `Analyze all recent memos regardless of tagging status.` } instruction := fmt.Sprintf( `Analyze memos and suggest organizational improvements: 1. %s 2. Use list_tags to understand the existing tag taxonomy 3. For each unorganized memo, suggest: - Appropriate tags from the existing taxonomy, or new tags if needed - Potential relations (references) to other memos on similar topics 4. Present suggestions as a structured list: - Memo: memo://memos/ (first line of content as preview) - Suggested tags: #tag1, #tag2 - Related to: memo://memos/ (brief reason) 5. After presenting suggestions, ask the user which changes to apply 6. Apply approved changes using update_memo (for tags in content) and create_memo_relation (for references)`, filter, ) return &mcp.GetPromptResult{ Description: fmt.Sprintf("Organize memos (scope: %s)", scope), Messages: []mcp.PromptMessage{ mcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(instruction)), }, }, nil } ================================================ FILE: server/router/mcp/resources_memo.go ================================================ package mcp import ( "context" "fmt" "strings" "github.com/mark3labs/mcp-go/mcp" mcpserver "github.com/mark3labs/mcp-go/server" "github.com/pkg/errors" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) // Memo resource URI scheme: memo://memos/{uid} // Clients can read any memo they have access to by URI without calling a tool. func (s *MCPService) registerMemoResources(mcpSrv *mcpserver.MCPServer) { mcpSrv.AddResourceTemplate( mcp.NewResourceTemplate( "memo://memos/{uid}", "Memo", mcp.WithTemplateDescription("A single Memos note identified by its UID. Returns the memo content as Markdown with a YAML frontmatter header containing metadata."), mcp.WithTemplateMIMEType("text/markdown"), ), s.handleReadMemoResource, ) } func (s *MCPService) handleReadMemoResource(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { userID := auth.GetUserID(ctx) // URI format: memo://memos/{uid} uid := strings.TrimPrefix(req.Params.URI, "memo://memos/") if uid == req.Params.URI || uid == "" { return nil, errors.Errorf("invalid memo URI %q: expected memo://memos/", req.Params.URI) } memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid}) if err != nil { return nil, errors.Wrap(err, "failed to get memo") } if memo == nil { return nil, errors.Errorf("memo not found: %s", uid) } if err := checkMemoAccess(memo, userID); err != nil { return nil, err } j := storeMemoToJSON(memo) text := formatMemoMarkdown(j) return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: req.Params.URI, MIMEType: "text/markdown", Text: text, }, }, nil } // formatMemoMarkdown renders a memo as Markdown with a YAML frontmatter header. func formatMemoMarkdown(j memoJSON) string { var sb strings.Builder sb.WriteString("---\n") fmt.Fprintf(&sb, "name: %s\n", j.Name) fmt.Fprintf(&sb, "creator: %s\n", j.Creator) fmt.Fprintf(&sb, "visibility: %s\n", j.Visibility) fmt.Fprintf(&sb, "state: %s\n", j.State) fmt.Fprintf(&sb, "pinned: %v\n", j.Pinned) if len(j.Tags) > 0 { fmt.Fprintf(&sb, "tags: [%s]\n", strings.Join(j.Tags, ", ")) } fmt.Fprintf(&sb, "create_time: %d\n", j.CreateTime) fmt.Fprintf(&sb, "update_time: %d\n", j.UpdateTime) if j.Parent != "" { fmt.Fprintf(&sb, "parent: %s\n", j.Parent) } sb.WriteString("---\n\n") sb.WriteString(j.Content) return sb.String() } ================================================ FILE: server/router/mcp/tools_attachment.go ================================================ package mcp import ( "context" "fmt" "strings" "github.com/mark3labs/mcp-go/mcp" mcpserver "github.com/mark3labs/mcp-go/server" "github.com/pkg/errors" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) type attachmentJSON struct { Name string `json:"name"` Creator string `json:"creator"` CreateTime int64 `json:"create_time"` Filename string `json:"filename"` Type string `json:"type"` Size int64 `json:"size"` StorageType string `json:"storage_type"` ExternalLink string `json:"external_link,omitempty"` Memo string `json:"memo,omitempty"` } func storeAttachmentToJSON(a *store.Attachment) attachmentJSON { j := attachmentJSON{ Name: "attachments/" + a.UID, Creator: fmt.Sprintf("users/%d", a.CreatorID), CreateTime: a.CreatedTs, Filename: a.Filename, Type: a.Type, Size: a.Size, } switch a.StorageType { case storepb.AttachmentStorageType_LOCAL: j.StorageType = "LOCAL" case storepb.AttachmentStorageType_S3: j.StorageType = "S3" j.ExternalLink = a.Reference case storepb.AttachmentStorageType_EXTERNAL: j.StorageType = "EXTERNAL" j.ExternalLink = a.Reference default: j.StorageType = "DATABASE" } if a.MemoUID != nil && *a.MemoUID != "" { j.Memo = "memos/" + *a.MemoUID } return j } func parseAttachmentUID(name string) (string, error) { uid, ok := strings.CutPrefix(name, "attachments/") if !ok || uid == "" { return "", errors.Errorf(`attachment name must be "attachments/", got %q`, name) } return uid, nil } func (s *MCPService) registerAttachmentTools(mcpSrv *mcpserver.MCPServer) { mcpSrv.AddTool(mcp.NewTool("list_attachments", mcp.WithDescription("List attachments owned by the authenticated user. Supports pagination and optional filtering by linked memo."), mcp.WithNumber("page_size", mcp.Description("Maximum attachments to return (1–100, default 20)")), mcp.WithNumber("page", mcp.Description("Zero-based page index (default 0)")), mcp.WithString("memo", mcp.Description(`Filter by linked memo resource name, e.g. "memos/abc123"`)), ), s.handleListAttachments) mcpSrv.AddTool(mcp.NewTool("get_attachment", mcp.WithDescription("Get a single attachment's metadata by resource name. Requires authentication."), mcp.WithString("name", mcp.Required(), mcp.Description(`Attachment resource name, e.g. "attachments/abc123"`)), ), s.handleGetAttachment) mcpSrv.AddTool(mcp.NewTool("delete_attachment", mcp.WithDescription("Permanently delete an attachment and its stored file. Requires authentication and ownership."), mcp.WithString("name", mcp.Required(), mcp.Description(`Attachment resource name, e.g. "attachments/abc123"`)), ), s.handleDeleteAttachment) mcpSrv.AddTool(mcp.NewTool("link_attachment_to_memo", mcp.WithDescription("Link an existing attachment to a memo. Requires authentication and ownership of the attachment."), mcp.WithString("name", mcp.Required(), mcp.Description(`Attachment resource name, e.g. "attachments/abc123"`)), mcp.WithString("memo", mcp.Required(), mcp.Description(`Memo resource name, e.g. "memos/abc123"`)), ), s.handleLinkAttachmentToMemo) } func (s *MCPService) handleListAttachments(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } pageSize := req.GetInt("page_size", 20) if pageSize <= 0 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } page := req.GetInt("page", 0) if page < 0 { page = 0 } limit := pageSize + 1 offset := page * pageSize find := &store.FindAttachment{ CreatorID: &userID, Limit: &limit, Offset: &offset, } if memoName := req.GetString("memo", ""); memoName != "" { memoUID, err := parseMemoUID(memoName) if err != nil { return mcp.NewToolResultError(err.Error()), nil } memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to find memo: %v", err)), nil } if memo == nil { return mcp.NewToolResultError("memo not found"), nil } find.MemoID = &memo.ID } attachments, err := s.store.ListAttachments(ctx, find) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to list attachments: %v", err)), nil } hasMore := len(attachments) > pageSize if hasMore { attachments = attachments[:pageSize] } results := make([]attachmentJSON, len(attachments)) for i, a := range attachments { results[i] = storeAttachmentToJSON(a) } type listResponse struct { Attachments []attachmentJSON `json:"attachments"` HasMore bool `json:"has_more"` } out, err := marshalJSON(listResponse{Attachments: results, HasMore: hasMore}) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleGetAttachment(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID := auth.GetUserID(ctx) uid, err := parseAttachmentUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } attachment, err := s.store.GetAttachment(ctx, &store.FindAttachment{UID: &uid}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get attachment: %v", err)), nil } if attachment == nil { return mcp.NewToolResultError("attachment not found"), nil } // Check access: creator can always access; linked memo visibility applies otherwise. if attachment.CreatorID != userID { if attachment.MemoID != nil { memo, err := s.store.GetMemo(ctx, &store.FindMemo{ID: attachment.MemoID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get linked memo: %v", err)), nil } if memo != nil { if err := checkMemoAccess(memo, userID); err != nil { return mcp.NewToolResultError(err.Error()), nil } } } else { return mcp.NewToolResultError("permission denied"), nil } } out, err := marshalJSON(storeAttachmentToJSON(attachment)) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleDeleteAttachment(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } uid, err := parseAttachmentUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } attachment, err := s.store.GetAttachment(ctx, &store.FindAttachment{UID: &uid, CreatorID: &userID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to find attachment: %v", err)), nil } if attachment == nil { return mcp.NewToolResultError("attachment not found"), nil } if err := s.store.DeleteAttachment(ctx, &store.DeleteAttachment{ID: attachment.ID}); err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to delete attachment: %v", err)), nil } return mcp.NewToolResultText(`{"deleted":true}`), nil } func (s *MCPService) handleLinkAttachmentToMemo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } uid, err := parseAttachmentUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } attachment, err := s.store.GetAttachment(ctx, &store.FindAttachment{UID: &uid}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get attachment: %v", err)), nil } if attachment == nil { return mcp.NewToolResultError("attachment not found"), nil } if attachment.CreatorID != userID { return mcp.NewToolResultError("permission denied"), nil } memoUID, err := parseMemoUID(req.GetString("memo", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil } if memo == nil { return mcp.NewToolResultError("memo not found"), nil } if err := s.store.UpdateAttachment(ctx, &store.UpdateAttachment{ ID: attachment.ID, MemoID: &memo.ID, }); err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to link attachment: %v", err)), nil } // Re-fetch to get updated memo UID. updated, err := s.store.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to fetch updated attachment: %v", err)), nil } out, err := marshalJSON(storeAttachmentToJSON(updated)) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } ================================================ FILE: server/router/mcp/tools_memo.go ================================================ package mcp import ( "context" "encoding/json" "fmt" "regexp" "strings" "github.com/lithammer/shortuuid/v4" "github.com/mark3labs/mcp-go/mcp" mcpserver "github.com/mark3labs/mcp-go/server" "github.com/pkg/errors" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) // tagRegexp matches #tag patterns in memo content. // A tag must start with a letter and contain no whitespace or # characters. var tagRegexp = regexp.MustCompile(`(?:^|\s)#([A-Za-z][^\s#]*)`) // extractTags does a best-effort extraction of #tags from raw markdown content. // It is used when creating or updating memos via MCP to pre-populate Payload.Tags. // The full markdown service may later rebuild a more accurate payload. func extractTags(content string) []string { matches := tagRegexp.FindAllStringSubmatch(content, -1) seen := make(map[string]struct{}, len(matches)) tags := make([]string, 0, len(matches)) for _, m := range matches { tag := m[1] if _, ok := seen[tag]; !ok { seen[tag] = struct{}{} tags = append(tags, tag) } } return tags } // buildPayload constructs a MemoPayload with tags extracted from content. // Returns nil when no tags are found so the store omits the payload entirely. func buildPayload(content string) *storepb.MemoPayload { tags := extractTags(content) if len(tags) == 0 { return nil } return &storepb.MemoPayload{Tags: tags} } // propertyJSON is the serialisable form of MemoPayload.Property. type propertyJSON struct { HasLink bool `json:"has_link"` HasTaskList bool `json:"has_task_list"` HasCode bool `json:"has_code"` HasIncompleteTasks bool `json:"has_incomplete_tasks"` } // memoJSON is the canonical response shape for all MCP memo results. // It serialises correctly with standard encoding/json (no proto marshalling needed). type memoJSON struct { Name string `json:"name"` Creator string `json:"creator"` CreateTime int64 `json:"create_time"` UpdateTime int64 `json:"update_time"` Content string `json:"content,omitempty"` Visibility string `json:"visibility"` Tags []string `json:"tags"` Pinned bool `json:"pinned"` State string `json:"state"` Property *propertyJSON `json:"property,omitempty"` Parent string `json:"parent,omitempty"` } func storeMemoToJSON(m *store.Memo) memoJSON { j := memoJSON{ Name: "memos/" + m.UID, Creator: fmt.Sprintf("users/%d", m.CreatorID), CreateTime: m.CreatedTs, UpdateTime: m.UpdatedTs, Content: m.Content, Visibility: string(m.Visibility), Pinned: m.Pinned, State: string(m.RowStatus), Tags: []string{}, } if m.Payload != nil { if len(m.Payload.Tags) > 0 { j.Tags = m.Payload.Tags } if p := m.Payload.Property; p != nil && (p.HasLink || p.HasTaskList || p.HasCode || p.HasIncompleteTasks) { j.Property = &propertyJSON{ HasLink: p.HasLink, HasTaskList: p.HasTaskList, HasCode: p.HasCode, HasIncompleteTasks: p.HasIncompleteTasks, } } } if m.ParentUID != nil { j.Parent = "memos/" + *m.ParentUID } return j } // checkMemoAccess returns an error if the caller cannot read memo. // userID == 0 means anonymous. func checkMemoAccess(memo *store.Memo, userID int32) error { switch memo.Visibility { case store.Protected: if userID == 0 { return errors.New("permission denied") } case store.Private: if memo.CreatorID != userID { return errors.New("permission denied") } default: // store.Public and any unknown visibility: allow } return nil } // applyVisibilityFilter restricts find to memos the caller may see. func applyVisibilityFilter(find *store.FindMemo, userID int32) { if userID == 0 { find.VisibilityList = []store.Visibility{store.Public} } else { find.Filters = append(find.Filters, fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, userID)) } } // parseMemoUID extracts the UID from a "memos/" resource name. func parseMemoUID(name string) (string, error) { uid, ok := strings.CutPrefix(name, "memos/") if !ok || uid == "" { return "", errors.Errorf(`memo name must be in the format "memos/", got %q`, name) } return uid, nil } // parseVisibility validates a visibility string and returns the store constant. func parseVisibility(s string) (store.Visibility, error) { switch v := store.Visibility(s); v { case store.Public, store.Protected, store.Private: return v, nil default: return "", errors.Errorf("visibility must be PRIVATE, PROTECTED, or PUBLIC; got %q", s) } } // parseRowStatus validates a state string and returns the store constant. func parseRowStatus(s string) (store.RowStatus, error) { switch rs := store.RowStatus(s); rs { case store.Normal, store.Archived: return rs, nil default: return "", errors.Errorf("state must be NORMAL or ARCHIVED; got %q", s) } } func extractUserID(ctx context.Context) (int32, error) { id := auth.GetUserID(ctx) if id == 0 { return 0, errors.New("unauthenticated: a personal access token is required") } return id, nil } func marshalJSON(v any) (string, error) { b, err := json.Marshal(v) if err != nil { return "", err } return string(b), nil } func (s *MCPService) registerMemoTools(mcpSrv *mcpserver.MCPServer) { mcpSrv.AddTool(mcp.NewTool("list_memos", mcp.WithDescription("List memos visible to the caller. Authenticated users see their own memos plus public and protected memos; unauthenticated callers see only public memos."), mcp.WithNumber("page_size", mcp.Description("Maximum memos to return (1–100, default 20)")), mcp.WithNumber("page", mcp.Description("Zero-based page index for pagination (default 0)")), mcp.WithString("state", mcp.Enum("NORMAL", "ARCHIVED"), mcp.Description("Filter by state: NORMAL (default) or ARCHIVED"), ), mcp.WithBoolean("order_by_pinned", mcp.Description("When true, pinned memos appear first (default false)")), mcp.WithString("filter", mcp.Description(`Optional CEL filter, e.g. content.contains("keyword") or tags.exists(t, t == "work")`)), ), s.handleListMemos) mcpSrv.AddTool(mcp.NewTool("get_memo", mcp.WithDescription("Get a single memo by resource name. Public memos are accessible without authentication."), mcp.WithString("name", mcp.Required(), mcp.Description(`Memo resource name, e.g. "memos/abc123"`)), ), s.handleGetMemo) mcpSrv.AddTool(mcp.NewTool("create_memo", mcp.WithDescription("Create a new memo. Requires authentication."), mcp.WithString("content", mcp.Required(), mcp.Description("Memo content in Markdown. Use #tag syntax for tagging.")), mcp.WithString("visibility", mcp.Enum("PRIVATE", "PROTECTED", "PUBLIC"), mcp.Description("Visibility (default: PRIVATE)"), ), ), s.handleCreateMemo) mcpSrv.AddTool(mcp.NewTool("update_memo", mcp.WithDescription("Update a memo's content, visibility, pin state, or archive state. Requires authentication and ownership. Omit any field to leave it unchanged."), mcp.WithString("name", mcp.Required(), mcp.Description(`Memo resource name, e.g. "memos/abc123"`)), mcp.WithString("content", mcp.Description("New Markdown content")), mcp.WithString("visibility", mcp.Enum("PRIVATE", "PROTECTED", "PUBLIC"), mcp.Description("New visibility"), ), mcp.WithBoolean("pinned", mcp.Description("Pin or unpin the memo")), mcp.WithString("state", mcp.Enum("NORMAL", "ARCHIVED"), mcp.Description("Set to ARCHIVED to archive, NORMAL to restore"), ), ), s.handleUpdateMemo) mcpSrv.AddTool(mcp.NewTool("delete_memo", mcp.WithDescription("Permanently delete a memo. Requires authentication and ownership."), mcp.WithString("name", mcp.Required(), mcp.Description(`Memo resource name, e.g. "memos/abc123"`)), ), s.handleDeleteMemo) mcpSrv.AddTool(mcp.NewTool("search_memos", mcp.WithDescription("Search memo content. Authenticated users search their own and visible memos; unauthenticated callers search public memos only."), mcp.WithString("query", mcp.Required(), mcp.Description("Text to search for in memo content")), ), s.handleSearchMemos) mcpSrv.AddTool(mcp.NewTool("list_memo_comments", mcp.WithDescription("List comments on a memo. Visibility rules for comments match those of the parent memo."), mcp.WithString("name", mcp.Required(), mcp.Description(`Memo resource name, e.g. "memos/abc123"`)), ), s.handleListMemoComments) mcpSrv.AddTool(mcp.NewTool("create_memo_comment", mcp.WithDescription("Add a comment to a memo. The comment inherits the parent memo's visibility. Requires authentication."), mcp.WithString("name", mcp.Required(), mcp.Description(`Memo resource name to comment on, e.g. "memos/abc123"`)), mcp.WithString("content", mcp.Required(), mcp.Description("Comment content in Markdown")), ), s.handleCreateMemoComment) } func (s *MCPService) handleListMemos(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID := auth.GetUserID(ctx) pageSize := req.GetInt("page_size", 20) if pageSize <= 0 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } page := req.GetInt("page", 0) if page < 0 { page = 0 } var rowStatus *store.RowStatus if state := req.GetString("state", "NORMAL"); state != "" { rs, err := parseRowStatus(state) if err != nil { return mcp.NewToolResultError(err.Error()), nil } rowStatus = &rs } limit := pageSize + 1 offset := page * pageSize find := &store.FindMemo{ ExcludeComments: true, RowStatus: rowStatus, Limit: &limit, Offset: &offset, OrderByPinned: req.GetBool("order_by_pinned", false), } applyVisibilityFilter(find, userID) if filter := req.GetString("filter", ""); filter != "" { find.Filters = append(find.Filters, filter) } memos, err := s.store.ListMemos(ctx, find) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to list memos: %v", err)), nil } hasMore := len(memos) > pageSize if hasMore { memos = memos[:pageSize] } results := make([]memoJSON, len(memos)) for i, m := range memos { results[i] = storeMemoToJSON(m) } type listResponse struct { Memos []memoJSON `json:"memos"` HasMore bool `json:"has_more"` } out, err := marshalJSON(listResponse{Memos: results, HasMore: hasMore}) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleGetMemo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID := auth.GetUserID(ctx) uid, err := parseMemoUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil } if memo == nil { return mcp.NewToolResultError("memo not found"), nil } if err := checkMemoAccess(memo, userID); err != nil { return mcp.NewToolResultError(err.Error()), nil } out, err := marshalJSON(storeMemoToJSON(memo)) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleCreateMemo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } content := req.GetString("content", "") if content == "" { return mcp.NewToolResultError("content is required"), nil } visibility, err := parseVisibility(req.GetString("visibility", "PRIVATE")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } memo, err := s.store.CreateMemo(ctx, &store.Memo{ UID: shortuuid.New(), CreatorID: userID, Content: content, Visibility: visibility, Payload: buildPayload(content), }) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to create memo: %v", err)), nil } out, err := marshalJSON(storeMemoToJSON(memo)) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleUpdateMemo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } uid, err := parseMemoUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil } if memo == nil { return mcp.NewToolResultError("memo not found"), nil } if memo.CreatorID != userID { return mcp.NewToolResultError("permission denied"), nil } update := &store.UpdateMemo{ID: memo.ID} args := req.GetArguments() if v := req.GetString("content", ""); v != "" { update.Content = &v update.Payload = buildPayload(v) } if v := req.GetString("visibility", ""); v != "" { vis, err := parseVisibility(v) if err != nil { return mcp.NewToolResultError(err.Error()), nil } update.Visibility = &vis } if v := req.GetString("state", ""); v != "" { rs, err := parseRowStatus(v) if err != nil { return mcp.NewToolResultError(err.Error()), nil } update.RowStatus = &rs } if _, ok := args["pinned"]; ok { pinned := req.GetBool("pinned", false) update.Pinned = &pinned } if err := s.store.UpdateMemo(ctx, update); err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to update memo: %v", err)), nil } updated, err := s.store.GetMemo(ctx, &store.FindMemo{ID: &memo.ID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to fetch updated memo: %v", err)), nil } out, err := marshalJSON(storeMemoToJSON(updated)) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleDeleteMemo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } uid, err := parseMemoUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil } if memo == nil { return mcp.NewToolResultError("memo not found"), nil } if memo.CreatorID != userID { return mcp.NewToolResultError("permission denied"), nil } if err := s.store.DeleteMemo(ctx, &store.DeleteMemo{ID: memo.ID}); err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to delete memo: %v", err)), nil } return mcp.NewToolResultText(`{"deleted":true}`), nil } func (s *MCPService) handleSearchMemos(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID := auth.GetUserID(ctx) query := req.GetString("query", "") if query == "" { return mcp.NewToolResultError("query is required"), nil } limit := 50 zero := 0 rowStatus := store.Normal find := &store.FindMemo{ ExcludeComments: true, RowStatus: &rowStatus, Limit: &limit, Offset: &zero, Filters: []string{fmt.Sprintf(`content.contains(%q)`, query)}, } applyVisibilityFilter(find, userID) memos, err := s.store.ListMemos(ctx, find) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to search memos: %v", err)), nil } results := make([]memoJSON, len(memos)) for i, m := range memos { results[i] = storeMemoToJSON(m) } out, err := marshalJSON(results) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleListMemoComments(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID := auth.GetUserID(ctx) uid, err := parseMemoUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } parent, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil } if parent == nil { return mcp.NewToolResultError("memo not found"), nil } if err := checkMemoAccess(parent, userID); err != nil { return mcp.NewToolResultError(err.Error()), nil } relationType := store.MemoRelationComment relations, err := s.store.ListMemoRelations(ctx, &store.FindMemoRelation{ RelatedMemoID: &parent.ID, Type: &relationType, }) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to list relations: %v", err)), nil } if len(relations) == 0 { out, _ := marshalJSON([]memoJSON{}) return mcp.NewToolResultText(out), nil } commentIDs := make([]int32, len(relations)) for i, r := range relations { commentIDs[i] = r.MemoID } memos, err := s.store.ListMemos(ctx, &store.FindMemo{IDList: commentIDs}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to list comments: %v", err)), nil } results := make([]memoJSON, 0, len(memos)) for _, m := range memos { if checkMemoAccess(m, userID) == nil { results = append(results, storeMemoToJSON(m)) } } out, err := marshalJSON(results) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleCreateMemoComment(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } uid, err := parseMemoUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } content := req.GetString("content", "") if content == "" { return mcp.NewToolResultError("content is required"), nil } parent, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil } if parent == nil { return mcp.NewToolResultError("memo not found"), nil } if err := checkMemoAccess(parent, userID); err != nil { return mcp.NewToolResultError(err.Error()), nil } comment, err := s.store.CreateMemo(ctx, &store.Memo{ UID: shortuuid.New(), CreatorID: userID, Content: content, Visibility: parent.Visibility, Payload: buildPayload(content), ParentUID: &parent.UID, }) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to create comment: %v", err)), nil } if _, err = s.store.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: comment.ID, RelatedMemoID: parent.ID, Type: store.MemoRelationComment, }); err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to link comment: %v", err)), nil } out, err := marshalJSON(storeMemoToJSON(comment)) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } ================================================ FILE: server/router/mcp/tools_reaction.go ================================================ package mcp import ( "context" "fmt" "github.com/mark3labs/mcp-go/mcp" mcpserver "github.com/mark3labs/mcp-go/server" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) type reactionJSON struct { ID int32 `json:"id"` Creator string `json:"creator"` ReactionType string `json:"reaction_type"` CreateTime int64 `json:"create_time"` } func (s *MCPService) registerReactionTools(mcpSrv *mcpserver.MCPServer) { mcpSrv.AddTool(mcp.NewTool("list_reactions", mcp.WithDescription("List all reactions on a memo. Returns reaction type and creator for each reaction."), mcp.WithString("name", mcp.Required(), mcp.Description(`Memo resource name, e.g. "memos/abc123"`)), ), s.handleListReactions) mcpSrv.AddTool(mcp.NewTool("upsert_reaction", mcp.WithDescription("Add a reaction (emoji) to a memo. If the same reaction already exists from the same user, this is a no-op. Requires authentication."), mcp.WithString("name", mcp.Required(), mcp.Description(`Memo resource name, e.g. "memos/abc123"`)), mcp.WithString("reaction_type", mcp.Required(), mcp.Description(`Reaction emoji, e.g. "👍", "❤️", "🎉"`)), ), s.handleUpsertReaction) mcpSrv.AddTool(mcp.NewTool("delete_reaction", mcp.WithDescription("Remove a reaction by its ID. Requires authentication and ownership of the reaction."), mcp.WithNumber("id", mcp.Required(), mcp.Description("Reaction ID to delete")), ), s.handleDeleteReaction) } func (s *MCPService) handleListReactions(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID := auth.GetUserID(ctx) uid, err := parseMemoUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil } if memo == nil { return mcp.NewToolResultError("memo not found"), nil } if err := checkMemoAccess(memo, userID); err != nil { return mcp.NewToolResultError(err.Error()), nil } contentID := "memos/" + uid reactions, err := s.store.ListReactions(ctx, &store.FindReaction{ContentID: &contentID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to list reactions: %v", err)), nil } results := make([]reactionJSON, len(reactions)) for i, r := range reactions { results[i] = reactionJSON{ ID: r.ID, Creator: fmt.Sprintf("users/%d", r.CreatorID), ReactionType: r.ReactionType, CreateTime: r.CreatedTs, } } out, err := marshalJSON(results) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleUpsertReaction(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } uid, err := parseMemoUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } reactionType := req.GetString("reaction_type", "") if reactionType == "" { return mcp.NewToolResultError("reaction_type is required"), nil } memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil } if memo == nil { return mcp.NewToolResultError("memo not found"), nil } if err := checkMemoAccess(memo, userID); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Validate reaction type against allowed reactions. memoRelatedSetting, err := s.store.GetInstanceMemoRelatedSetting(ctx) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get reaction settings: %v", err)), nil } allowed := false for _, r := range memoRelatedSetting.Reactions { if r == reactionType { allowed = true break } } if !allowed { return mcp.NewToolResultError(fmt.Sprintf("reaction %q is not in the allowed reaction list", reactionType)), nil } contentID := "memos/" + uid reaction, err := s.store.UpsertReaction(ctx, &store.Reaction{ CreatorID: userID, ContentID: contentID, ReactionType: reactionType, }) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to upsert reaction: %v", err)), nil } out, err := marshalJSON(reactionJSON{ ID: reaction.ID, Creator: fmt.Sprintf("users/%d", reaction.CreatorID), ReactionType: reaction.ReactionType, CreateTime: reaction.CreatedTs, }) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleDeleteReaction(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } reactionID := int32(req.GetInt("id", 0)) if reactionID == 0 { return mcp.NewToolResultError("id is required"), nil } reaction, err := s.store.GetReaction(ctx, &store.FindReaction{ID: &reactionID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get reaction: %v", err)), nil } if reaction == nil { return mcp.NewToolResultError("reaction not found"), nil } if reaction.CreatorID != userID { return mcp.NewToolResultError("permission denied: can only delete your own reactions"), nil } if err := s.store.DeleteReaction(ctx, &store.DeleteReaction{ID: reactionID}); err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to delete reaction: %v", err)), nil } return mcp.NewToolResultText(`{"deleted":true}`), nil } ================================================ FILE: server/router/mcp/tools_relation.go ================================================ package mcp import ( "context" "fmt" "github.com/mark3labs/mcp-go/mcp" mcpserver "github.com/mark3labs/mcp-go/server" "github.com/usememos/memos/store" ) type relationJSON struct { Memo string `json:"memo"` RelatedMemo string `json:"related_memo"` Type string `json:"type"` } func (s *MCPService) registerRelationTools(mcpSrv *mcpserver.MCPServer) { mcpSrv.AddTool(mcp.NewTool("list_memo_relations", mcp.WithDescription("List all relations (references and comments) for a memo. Requires read access to the memo."), mcp.WithString("name", mcp.Required(), mcp.Description(`Memo resource name, e.g. "memos/abc123"`)), mcp.WithString("type", mcp.Enum("REFERENCE", "COMMENT"), mcp.Description("Filter by relation type (optional)"), ), ), s.handleListMemoRelations) mcpSrv.AddTool(mcp.NewTool("create_memo_relation", mcp.WithDescription("Create a reference relation between two memos. Requires authentication. For comments, use create_memo_comment instead."), mcp.WithString("name", mcp.Required(), mcp.Description(`Source memo resource name, e.g. "memos/abc123"`)), mcp.WithString("related_memo", mcp.Required(), mcp.Description(`Target memo resource name, e.g. "memos/def456"`)), ), s.handleCreateMemoRelation) mcpSrv.AddTool(mcp.NewTool("delete_memo_relation", mcp.WithDescription("Delete a reference relation between two memos. Requires authentication and ownership of the source memo."), mcp.WithString("name", mcp.Required(), mcp.Description(`Source memo resource name, e.g. "memos/abc123"`)), mcp.WithString("related_memo", mcp.Required(), mcp.Description(`Target memo resource name, e.g. "memos/def456"`)), ), s.handleDeleteMemoRelation) } func (s *MCPService) handleListMemoRelations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { uid, err := parseMemoUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil } if memo == nil { return mcp.NewToolResultError("memo not found"), nil } find := &store.FindMemoRelation{ MemoIDList: []int32{memo.ID}, } if typeStr := req.GetString("type", ""); typeStr != "" { switch store.MemoRelationType(typeStr) { case store.MemoRelationReference, store.MemoRelationComment: t := store.MemoRelationType(typeStr) find.Type = &t default: return mcp.NewToolResultError(fmt.Sprintf("type must be REFERENCE or COMMENT, got %q", typeStr)), nil } } relations, err := s.store.ListMemoRelations(ctx, find) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to list relations: %v", err)), nil } // Resolve memo IDs to UIDs. idSet := make(map[int32]struct{}) for _, r := range relations { idSet[r.MemoID] = struct{}{} idSet[r.RelatedMemoID] = struct{}{} } ids := make([]int32, 0, len(idSet)) for id := range idSet { ids = append(ids, id) } memos, err := s.store.ListMemos(ctx, &store.FindMemo{IDList: ids, ExcludeContent: true}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to resolve memos: %v", err)), nil } uidByID := make(map[int32]string, len(memos)) for _, m := range memos { uidByID[m.ID] = m.UID } results := make([]relationJSON, 0, len(relations)) for _, r := range relations { memoUID, ok1 := uidByID[r.MemoID] relatedUID, ok2 := uidByID[r.RelatedMemoID] if !ok1 || !ok2 { continue } results = append(results, relationJSON{ Memo: "memos/" + memoUID, RelatedMemo: "memos/" + relatedUID, Type: string(r.Type), }) } out, err := marshalJSON(results) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleCreateMemoRelation(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } srcUID, err := parseMemoUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } dstUID, err := parseMemoUID(req.GetString("related_memo", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } srcMemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &srcUID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get source memo: %v", err)), nil } if srcMemo == nil { return mcp.NewToolResultError("source memo not found"), nil } if srcMemo.CreatorID != userID { return mcp.NewToolResultError("permission denied: must own the source memo"), nil } dstMemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &dstUID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get related memo: %v", err)), nil } if dstMemo == nil { return mcp.NewToolResultError("related memo not found"), nil } relation, err := s.store.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: srcMemo.ID, RelatedMemoID: dstMemo.ID, Type: store.MemoRelationReference, }) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to create relation: %v", err)), nil } out, err := marshalJSON(relationJSON{ Memo: "memos/" + srcUID, RelatedMemo: "memos/" + dstUID, Type: string(relation.Type), }) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } func (s *MCPService) handleDeleteMemoRelation(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID, err := extractUserID(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } srcUID, err := parseMemoUID(req.GetString("name", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } dstUID, err := parseMemoUID(req.GetString("related_memo", "")) if err != nil { return mcp.NewToolResultError(err.Error()), nil } srcMemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &srcUID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get source memo: %v", err)), nil } if srcMemo == nil { return mcp.NewToolResultError("source memo not found"), nil } if srcMemo.CreatorID != userID { return mcp.NewToolResultError("permission denied: must own the source memo"), nil } dstMemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &dstUID}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to get related memo: %v", err)), nil } if dstMemo == nil { return mcp.NewToolResultError("related memo not found"), nil } refType := store.MemoRelationReference if err := s.store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{ MemoID: &srcMemo.ID, RelatedMemoID: &dstMemo.ID, Type: &refType, }); err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to delete relation: %v", err)), nil } return mcp.NewToolResultText(`{"deleted":true}`), nil } ================================================ FILE: server/router/mcp/tools_tag.go ================================================ package mcp import ( "context" "fmt" "slices" "github.com/mark3labs/mcp-go/mcp" mcpserver "github.com/mark3labs/mcp-go/server" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) func (s *MCPService) registerTagTools(mcpSrv *mcpserver.MCPServer) { mcpSrv.AddTool(mcp.NewTool("list_tags", mcp.WithDescription("List all tags with their memo counts. Authenticated users see tags from their own and visible memos; unauthenticated callers see tags from public memos only. Results are sorted by count descending, then alphabetically."), ), s.handleListTags) } type tagEntry struct { Tag string `json:"tag"` Count int `json:"count"` } func (s *MCPService) handleListTags(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { userID := auth.GetUserID(ctx) rowStatus := store.Normal find := &store.FindMemo{ ExcludeComments: true, ExcludeContent: true, RowStatus: &rowStatus, } applyVisibilityFilter(find, userID) memos, err := s.store.ListMemos(ctx, find) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to list memos: %v", err)), nil } counts := make(map[string]int) for _, m := range memos { if m.Payload == nil { continue } for _, tag := range m.Payload.Tags { counts[tag]++ } } entries := make([]tagEntry, 0, len(counts)) for tag, count := range counts { entries = append(entries, tagEntry{Tag: tag, Count: count}) } slices.SortFunc(entries, func(a, b tagEntry) int { if a.Count != b.Count { if a.Count > b.Count { return -1 } return 1 } switch { case a.Tag < b.Tag: return -1 case a.Tag > b.Tag: return 1 default: return 0 } }) out, err := marshalJSON(entries) if err != nil { return nil, err } return mcp.NewToolResultText(out), nil } ================================================ FILE: server/router/rss/rss.go ================================================ package rss import ( "context" "crypto/sha256" "fmt" "net/http" "regexp" "strconv" "strings" "sync" "time" "github.com/gorilla/feeds" "github.com/labstack/echo/v5" "github.com/usememos/memos/internal/profile" "github.com/usememos/memos/plugin/markdown" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) const ( maxRSSItemCount = 100 defaultCacheDuration = 1 * time.Hour maxCacheSize = 50 // Maximum number of cached feeds ) var ( // Regex to match markdown headings at the start of a line. markdownHeadingRegex = regexp.MustCompile(`^#{1,6}\s*`) ) // cacheEntry represents a cached RSS feed with expiration. type cacheEntry struct { content string etag string lastModified time.Time createdAt time.Time } type RSSService struct { Profile *profile.Profile Store *store.Store MarkdownService markdown.Service // Cache for RSS feeds cache map[string]*cacheEntry cacheMutex sync.RWMutex } type RSSHeading struct { Title string Description string Language string } func NewRSSService(profile *profile.Profile, store *store.Store, markdownService markdown.Service) *RSSService { return &RSSService{ Profile: profile, Store: store, MarkdownService: markdownService, cache: make(map[string]*cacheEntry), } } 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() cacheKey := "explore" // Check cache first if cached := s.getFromCache(cacheKey); cached != nil { // Check ETag for conditional request if c.Request().Header.Get("If-None-Match") == cached.etag { return c.NoContent(http.StatusNotModified) } s.setRSSHeaders(c, cached.etag, cached.lastModified) return c.String(http.StatusOK, cached.content) } normalStatus := store.Normal limit := maxRSSItemCount memoFind := store.FindMemo{ RowStatus: &normalStatus, VisibilityList: []store.Visibility{store.Public}, Limit: &limit, } memoList, err := s.Store.ListMemos(ctx, &memoFind) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").Wrap(err) } baseURL := c.Scheme() + "://" + c.Request().Host rss, lastModified, err := s.generateRSSFromMemoList(ctx, memoList, baseURL, nil) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").Wrap(err) } // Cache the result etag := s.putInCache(cacheKey, rss, lastModified) s.setRSSHeaders(c, etag, lastModified) return c.String(http.StatusOK, rss) } func (s *RSSService) GetUserRSS(c *echo.Context) error { ctx := c.Request().Context() username := c.Param("username") cacheKey := "user:" + username // Check cache first if cached := s.getFromCache(cacheKey); cached != nil { // Check ETag for conditional request if c.Request().Header.Get("If-None-Match") == cached.etag { return c.NoContent(http.StatusNotModified) } s.setRSSHeaders(c, cached.etag, cached.lastModified) return c.String(http.StatusOK, cached.content) } user, err := s.Store.GetUser(ctx, &store.FindUser{ Username: &username, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").Wrap(err) } if user == nil { return echo.NewHTTPError(http.StatusNotFound, "User not found") } normalStatus := store.Normal limit := maxRSSItemCount memoFind := store.FindMemo{ CreatorID: &user.ID, RowStatus: &normalStatus, VisibilityList: []store.Visibility{store.Public}, Limit: &limit, } memoList, err := s.Store.ListMemos(ctx, &memoFind) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").Wrap(err) } baseURL := c.Scheme() + "://" + c.Request().Host rss, lastModified, err := s.generateRSSFromMemoList(ctx, memoList, baseURL, user) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").Wrap(err) } // Cache the result etag := s.putInCache(cacheKey, rss, lastModified) s.setRSSHeaders(c, etag, lastModified) return c.String(http.StatusOK, rss) } func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*store.Memo, baseURL string, user *store.User) (string, time.Time, error) { rssHeading, err := getRSSHeading(ctx, s.Store) if err != nil { return "", time.Time{}, err } feed := &feeds.Feed{ Title: rssHeading.Title, Link: &feeds.Link{Href: baseURL}, Description: rssHeading.Description, Created: time.Now(), } var itemCountLimit = min(len(memoList), maxRSSItemCount) if itemCountLimit == 0 { // Return empty feed if no memos rss, err := feed.ToRss() return rss, time.Time{}, err } // Track the most recent update time for Last-Modified header var lastModified time.Time if len(memoList) > 0 { lastModified = time.Unix(memoList[0].UpdatedTs, 0) } // Batch load all attachments for all memos to avoid N+1 query problem memoIDs := make([]int32, itemCountLimit) for i := 0; i < itemCountLimit; i++ { memoIDs[i] = memoList[i].ID } allAttachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{ MemoIDList: memoIDs, }) if err != nil { return "", lastModified, err } // Group attachments by memo ID for quick lookup attachmentsByMemoID := make(map[int32][]*store.Attachment) for _, attachment := range allAttachments { if attachment.MemoID != nil { attachmentsByMemoID[*attachment.MemoID] = append(attachmentsByMemoID[*attachment.MemoID], attachment) } } // Batch load all memo creators creatorMap := make(map[int32]*store.User) if user != nil { // Single user feed - reuse the user object creatorMap[user.ID] = user } else { // Multi-user feed - batch load all unique creators creatorIDs := make(map[int32]bool) for _, memo := range memoList[:itemCountLimit] { creatorIDs[memo.CreatorID] = true } // Batch load all users with a single query by getting all users and filtering // Note: This is more efficient than N separate queries for creatorID := range creatorIDs { creator, err := s.Store.GetUser(ctx, &store.FindUser{ID: &creatorID}) if err == nil && creator != nil { creatorMap[creatorID] = creator } } } // Generate feed items feed.Items = make([]*feeds.Item, itemCountLimit) for i := 0; i < itemCountLimit; i++ { memo := memoList[i] // Generate item title from memo content title := s.generateItemTitle(memo.Content) // Render content as HTML htmlContent, err := s.getRSSItemDescription(memo.Content) if err != nil { return "", lastModified, err } link := &feeds.Link{Href: baseURL + "/memos/" + memo.UID} item := &feeds.Item{ Title: title, Link: link, Description: htmlContent, // Summary/excerpt Content: htmlContent, // Full content in content:encoded Created: time.Unix(memo.CreatedTs, 0), Updated: time.Unix(memo.UpdatedTs, 0), Id: link.Href, } // Add author information if creator, ok := creatorMap[memo.CreatorID]; ok { authorName := creator.Nickname if authorName == "" { authorName = creator.Username } item.Author = &feeds.Author{ Name: authorName, Email: creator.Email, } } // Note: gorilla/feeds doesn't support categories in RSS items // Tags could be added to the description or content if needed // Add first attachment as enclosure if attachments, ok := attachmentsByMemoID[memo.ID]; ok && 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 item.Enclosure = &enclosure } feed.Items[i] = item } rss, err := feed.ToRss() if err != nil { return "", lastModified, err } return rss, lastModified, nil } func (*RSSService) generateItemTitle(content string) string { // Extract first line as title lines := strings.Split(content, "\n") title := strings.TrimSpace(lines[0]) // Remove markdown heading syntax using regex (handles # to ###### with optional spaces) title = markdownHeadingRegex.ReplaceAllString(title, "") title = strings.TrimSpace(title) // Limit title length const maxTitleLength = 100 if len(title) > maxTitleLength { // Find last space before limit to avoid cutting words cutoff := maxTitleLength for i := min(maxTitleLength-1, len(title)-1); i > 0; i-- { if title[i] == ' ' { cutoff = i break } } if cutoff < maxTitleLength { title = title[:cutoff] + "..." } else { // No space found, just truncate title = title[:maxTitleLength] + "..." } } // If title is empty, use a default if title == "" { title = "Memo" } return title } func (s *RSSService) getRSSItemDescription(content string) (string, error) { html, err := s.MarkdownService.RenderHTML([]byte(content)) if err != nil { return "", err } return html, nil } // getFromCache retrieves a cached feed entry if it exists and is not expired. func (s *RSSService) getFromCache(key string) *cacheEntry { s.cacheMutex.RLock() entry, exists := s.cache[key] s.cacheMutex.RUnlock() if !exists { return nil } // Check if cache entry is still valid if time.Since(entry.createdAt) > defaultCacheDuration { // Entry is expired, remove it s.cacheMutex.Lock() delete(s.cache, key) s.cacheMutex.Unlock() return nil } return entry } // putInCache stores a feed in the cache and returns its ETag. func (s *RSSService) putInCache(key, content string, lastModified time.Time) string { s.cacheMutex.Lock() defer s.cacheMutex.Unlock() // Generate ETag from content hash hash := sha256.Sum256([]byte(content)) etag := fmt.Sprintf(`"%x"`, hash[:8]) // Implement simple LRU: if cache is too large, remove oldest entries if len(s.cache) >= maxCacheSize { var oldestKey string var oldestTime time.Time for k, v := range s.cache { if oldestKey == "" || v.createdAt.Before(oldestTime) { oldestKey = k oldestTime = v.createdAt } } if oldestKey != "" { delete(s.cache, oldestKey) } } s.cache[key] = &cacheEntry{ content: content, etag: etag, lastModified: lastModified, createdAt: time.Now(), } return etag } // setRSSHeaders sets appropriate HTTP headers for RSS responses. func (*RSSService) setRSSHeaders(c *echo.Context, etag string, lastModified time.Time) { c.Response().Header().Set(echo.HeaderContentType, "application/rss+xml; charset=utf-8") c.Response().Header().Set(echo.HeaderCacheControl, fmt.Sprintf("public, max-age=%d", int(defaultCacheDuration.Seconds()))) c.Response().Header().Set("ETag", etag) if !lastModified.IsZero() { c.Response().Header().Set("Last-Modified", lastModified.UTC().Format(http.TimeFormat)) } } func getRSSHeading(ctx context.Context, stores *store.Store) (RSSHeading, error) { settings, err := stores.GetInstanceGeneralSetting(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.", Language: "en-us", }, nil } customProfile := settings.CustomProfile return RSSHeading{ Title: customProfile.Title, Description: customProfile.Description, Language: "en-us", }, nil } ================================================ FILE: server/runner/memopayload/runner.go ================================================ package memopayload import ( "context" "log/slog" "github.com/pkg/errors" "github.com/usememos/memos/plugin/markdown" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) type Runner struct { Store *store.Store MarkdownService markdown.Service } func NewRunner(store *store.Store, markdownService markdown.Service) *Runner { return &Runner{ Store: store, MarkdownService: markdownService, } } // 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, r.MarkdownService); 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, markdownService markdown.Service) error { if memo.Payload == nil { memo.Payload = &storepb.MemoPayload{} } // Use goldmark service to extract all metadata in a single pass (more efficient) data, err := markdownService.ExtractAll([]byte(memo.Content)) if err != nil { return errors.Wrap(err, "failed to extract markdown metadata") } memo.Payload.Tags = data.Tags memo.Payload.Property = data.Property return nil } ================================================ FILE: server/runner/s3presign/runner.go ================================================ 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) { instanceStorageSetting, err := r.Store.GetInstanceStorageSetting(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 := instanceStorageSetting.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) } } ================================================ FILE: server/server.go ================================================ package server import ( "context" "fmt" "log/slog" "net" "net/http" "runtime" "time" "github.com/google/uuid" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "github.com/pkg/errors" "github.com/usememos/memos/internal/profile" storepb "github.com/usememos/memos/proto/gen/store" apiv1 "github.com/usememos/memos/server/router/api/v1" "github.com/usememos/memos/server/router/fileserver" "github.com/usememos/memos/server/router/frontend" mcprouter "github.com/usememos/memos/server/router/mcp" "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 httpServer *http.Server 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.Use(middleware.Recover()) s.echoServer = echoServer instanceBasicSetting, err := s.getOrUpsertInstanceBasicSetting(ctx) if err != nil { return nil, errors.Wrap(err, "failed to get instance basic setting") } secret := "usememos" if !profile.Demo { secret = instanceBasicSetting.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("") apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store) // Register HTTP file server routes BEFORE gRPC-Gateway to ensure proper range request handling for Safari. // This uses native HTTP serving (http.ServeContent) instead of gRPC for video/audio files. fileServerService := fileserver.NewFileServerService(s.Profile, s.Store, s.Secret) fileServerService.RegisterRoutes(echoServer) // Create and register RSS routes (needs markdown service from apiV1Service). rss.NewRSSService(s.Profile, s.Store, apiV1Service.MarkdownService).RegisterRoutes(rootGroup) // Register gRPC gateway as api v1 (includes SSE endpoint on CORS-enabled group). if err := apiV1Service.RegisterGateway(ctx, echoServer); err != nil { return nil, errors.Wrap(err, "failed to register gRPC gateway") } // Register MCP server. mcpService := mcprouter.NewMCPService(s.Profile, s.Store, s.Secret) mcpService.RegisterRoutes(echoServer) 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") } // Start Echo server directly (no cmux needed - all traffic is HTTP). s.httpServer = &http.Server{Handler: s.echoServer} go func() { if err := s.httpServer.Serve(listener); err != nil && err != http.ErrServerClosed { slog.Error("failed to start echo server", "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 HTTP server. if s.httpServer != nil { if err := s.httpServer.Shutdown(ctx); err != nil { slog.Error("failed to shutdown server", slog.String("error", err.Error())) } } // 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) getOrUpsertInstanceBasicSetting(ctx context.Context) (*storepb.InstanceBasicSetting, error) { instanceBasicSetting, err := s.Store.GetInstanceBasicSetting(ctx) if err != nil { return nil, errors.Wrap(err, "failed to get instance basic setting") } modified := false if instanceBasicSetting.SecretKey == "" { instanceBasicSetting.SecretKey = uuid.NewString() modified = true } if modified { instanceSetting, err := s.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_BASIC, Value: &storepb.InstanceSetting_BasicSetting{BasicSetting: instanceBasicSetting}, }) if err != nil { return nil, errors.Wrap(err, "failed to upsert instance setting") } instanceBasicSetting = instanceSetting.GetBasicSetting() } return instanceBasicSetting, nil } ================================================ FILE: store/attachment.go ================================================ 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 // Composed field MemoUID *string } type FindAttachment struct { GetBlob bool ID *int32 UID *string CreatorID *int32 Filename *string FilenameSearch *string MemoID *int32 MemoIDList []int32 HasRelatedMemo bool StorageType *storepb.AttachmentStorageType Filters []string 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 shouldApplyDefaultLimit := find.Limit == nil && len(find.MemoIDList) == 0 if shouldApplyDefaultLimit && find.GetBlob { // When fetching blobs, we should be especially careful with limits defaultLimit := 10 find.Limit = &defaultLimit } else if shouldApplyDefaultLimit { // 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 && !os.IsNotExist(err) { 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") } instanceStorageSetting, err := s.GetInstanceStorageSetting(ctx) if err != nil { return errors.Wrap(err, "failed to get instance storage setting") } s3Config := s3ObjectPayload.S3Config if s3Config == nil { if instanceStorageSetting.S3Config == nil { return errors.Errorf("S3 config is not found") } s3Config = instanceStorageSetting.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) } ================================================ FILE: store/cache/cache.go ================================================ 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 { itemCount atomic.Int64 // Use atomic operations to track item count data sync.Map config Config 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. (&c.itemCount).Add(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 && (&c.itemCount).Load() > 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) (&c.itemCount).Add(-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 { (&c.itemCount).Add(-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) { count := 0 c.data.Range(func(key, value any) bool { if c.config.OnEviction != nil { itm, ok := value.(item) if ok { if keyStr, ok := key.(string); ok { c.config.OnEviction(keyStr, itm.value) } } } c.data.Delete(key) count++ return true }) (&c.itemCount).Store(0) } // Size returns the number of items in the cache. func (c *Cache) Size() int64 { return (&c.itemCount).Load() } // 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 { (&c.itemCount).Add(-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 := (&c.itemCount).Load() // 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 { (&c.itemCount).Add(-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 } } ================================================ FILE: store/cache/cache_test.go ================================================ 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.Error("Key 'key1' should have been deleted") } // Test automatic expiration time.Sleep(150 * time.Millisecond) if _, ok := cache.Get(ctx, "key1"); ok { t.Error("Key 'key1' should have expired") } // key2 should still be valid (200ms TTL) if _, ok := cache.Get(ctx, "key2"); !ok { t.Error("Key 'key2' should still be valid") } // Wait for key2 to expire time.Sleep(100 * time.Millisecond) if _, ok := cache.Get(ctx, "key2"); ok { t.Error("Key 'key2' should have expired") } // Test Clear cache.Set(ctx, "key3", "value3") cache.Clear(ctx) if _, ok := cache.Get(ctx, "key3"); ok { t.Error("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.Error("No keys were evicted despite exceeding max items") } // The newer keys should still be present if _, ok := cache.Get(ctx, "keyA"); !ok { t.Error("Key 'keyA' should be in the cache") } if _, ok := cache.Get(ctx, "keyB"); !ok { t.Error("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.Error("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.Error("Eviction callback not triggered for TTL expiration") } evictedMu.Unlock() } ================================================ FILE: store/cache.go ================================================ package store import ( "fmt" ) func getUserSettingCacheKey(userID int32, key string) string { return fmt.Sprintf("%d-%s", userID, key) } ================================================ FILE: store/common.go ================================================ 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) } ================================================ FILE: store/db/db.go ================================================ 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 } ================================================ FILE: store/db/mysql/attachment.go ================================================ package mysql import ( "context" "database/sql" "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) 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 `attachment` (" + 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, "`attachment`.`id` = ?"), append(args, *v) } if v := find.UID; v != nil { where, args = append(where, "`attachment`.`uid` = ?"), append(args, *v) } if v := find.CreatorID; v != nil { where, args = append(where, "`attachment`.`creator_id` = ?"), append(args, *v) } if v := find.Filename; v != nil { where, args = append(where, "`attachment`.`filename` = ?"), append(args, *v) } if v := find.FilenameSearch; v != nil { where, args = append(where, "`attachment`.`filename` LIKE ?"), append(args, "%"+*v+"%") } if v := find.MemoID; v != nil { where, args = append(where, "`attachment`.`memo_id` = ?"), append(args, *v) } if len(find.MemoIDList) > 0 { placeholders := make([]string, 0, len(find.MemoIDList)) for range find.MemoIDList { placeholders = append(placeholders, "?") } where = append(where, "`attachment`.`memo_id` IN ("+strings.Join(placeholders, ",")+")") for _, id := range find.MemoIDList { args = append(args, id) } } if find.HasRelatedMemo { where = append(where, "`attachment`.`memo_id` IS NOT NULL") } if find.StorageType != nil { where, args = append(where, "`attachment`.`storage_type` = ?"), append(args, find.StorageType.String()) } if len(find.Filters) > 0 { engine, err := filter.DefaultAttachmentEngine() if err != nil { return nil, errors.Wrap(err, "failed to get filter engine") } if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectMySQL, &where, &args); err != nil { return nil, errors.Wrap(err, "failed to append filter conditions") } } fields := []string{ "`attachment`.`id` AS `id`", "`attachment`.`uid` AS `uid`", "`attachment`.`filename` AS `filename`", "`attachment`.`type` AS `type`", "`attachment`.`size` AS `size`", "`attachment`.`creator_id` AS `creator_id`", "UNIX_TIMESTAMP(`attachment`.`created_ts`) AS `created_ts`", "UNIX_TIMESTAMP(`attachment`.`updated_ts`) AS `updated_ts`", "`attachment`.`memo_id` AS `memo_id`", "`attachment`.`storage_type` AS `storage_type`", "`attachment`.`reference` AS `reference`", "`attachment`.`payload` AS `payload`", "CASE WHEN `memo`.`uid` IS NOT NULL THEN `memo`.`uid` ELSE NULL END AS `memo_uid`", } if find.GetBlob { fields = append(fields, "`attachment`.`blob` AS `blob`") } query := "SELECT " + strings.Join(fields, ", ") + " FROM `attachment`" + " " + "LEFT JOIN `memo` ON `attachment`.`memo_id` = `memo`.`id`" + " " + "WHERE " + strings.Join(where, " AND ") + " " + "ORDER BY `updated_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 := 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, &attachment.MemoUID, } 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 `attachment` 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 `attachment` 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 } ================================================ FILE: store/db/mysql/common.go ================================================ package mysql import "google.golang.org/protobuf/encoding/protojson" var ( protojsonUnmarshaler = protojson.UnmarshalOptions{ AllowPartial: true, DiscardUnknown: true, } ) ================================================ FILE: store/db/mysql/idp.go ================================================ 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{"`uid`", "`name`", "`type`", "`identifier_filter`", "`config`"} args := []any{create.UID, 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) } if v := find.UID; v != nil { where, args = append(where, "`uid` = ?"), append(args, *v) } rows, err := d.db.QueryContext(ctx, "SELECT `id`, `uid`, `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.UID, &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 } ================================================ FILE: store/db/mysql/inbox.go ================================================ 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) } if find.MessageType != nil { // Filter by message type using JSON extraction // Note: The type field in JSON is stored as string representation of the enum name if *find.MessageType == storepb.InboxMessage_TYPE_UNSPECIFIED { where, args = append(where, "(JSON_EXTRACT(`message`, '$.type') IS NULL OR JSON_EXTRACT(`message`, '$.type') = ?)"), append(args, find.MessageType.String()) } else { where, args = append(where, "JSON_EXTRACT(`message`, '$.type') = ?"), append(args, find.MessageType.String()) } } 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 } ================================================ FILE: store/db/mysql/instance_setting.go ================================================ package mysql import ( "context" "strings" "github.com/usememos/memos/store" ) func (d *DB) UpsertInstanceSetting(ctx context.Context, upsert *store.InstanceSetting) (*store.InstanceSetting, 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) ListInstanceSettings(ctx context.Context, find *store.FindInstanceSetting) ([]*store.InstanceSetting, 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.InstanceSetting{} for rows.Next() { systemSettingMessage := &store.InstanceSetting{} 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) DeleteInstanceSetting(ctx context.Context, delete *store.DeleteInstanceSetting) error { stmt := "DELETE FROM `system_setting` WHERE `name` = ?" _, err := d.db.ExecContext(ctx, stmt, delete.Name) return err } ================================================ FILE: store/db/mysql/memo.go ================================================ 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} // Add custom timestamps if provided if create.CreatedTs != 0 { fields = append(fields, "`created_ts`") placeholder = append(placeholder, "FROM_UNIXTIME(?)") args = append(args, create.CreatedTs) } if create.UpdatedTs != 0 { fields = append(fields, "`updated_ts`") placeholder = append(placeholder, "FROM_UNIXTIME(?)") args = append(args, create.UpdatedTs) } 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{} engine, err := filter.DefaultEngine() if err != nil { return nil, err } if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectMySQL, &where, &args); err != nil { return nil, err } if v := find.ID; v != nil { where, args = append(where, "`memo`.`id` = ?"), append(args, *v) } if len(find.IDList) > 0 { placeholders := make([]string, 0, len(find.IDList)) for range find.IDList { placeholders = append(placeholders, "?") } where = append(where, "`memo`.`id` IN ("+strings.Join(placeholders, ",")+")") for _, id := range find.IDList { args = append(args, id) } } if v := find.UID; v != nil { where, args = append(where, "`memo`.`uid` = ?"), append(args, *v) } if len(find.UIDList) > 0 { placeholders := make([]string, 0, len(find.UIDList)) for range find.UIDList { placeholders = append(placeholders, "?") } where = append(where, "`memo`.`uid` IN ("+strings.Join(placeholders, ",")+")") for _, uid := range find.UIDList { args = append(args, uid) } } 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.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 find.ExcludeComments { having = append(having, "`parent_uid` 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) } // Add id as final tie-breaker orderBy = append(orderBy, "`id` DESC") 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`", "CASE WHEN `parent_memo`.`uid` IS NOT NULL THEN `parent_memo`.`uid` ELSE NULL END AS `parent_uid`", } 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'" + " " + "LEFT JOIN `memo` AS `parent_memo` ON `memo_relation`.`related_memo_id` = `parent_memo`.`id`" + " " + "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.ParentUID, } 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 } ================================================ FILE: store/db/mysql/memo_relation.go ================================================ 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 (?, ?, ?) ON DUPLICATE KEY UPDATE `type` = `type`" _, 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 len(find.MemoIDList) > 0 { placeholders := make([]string, len(find.MemoIDList)) for i, id := range find.MemoIDList { placeholders[i] = "?" args = append(args, id) } inClause := strings.Join(placeholders, ", ") for _, id := range find.MemoIDList { args = append(args, id) } where = append(where, fmt.Sprintf("(`memo_id` IN (%s) OR `related_memo_id` IN (%s))", inClause, inClause)) } if find.MemoFilter != nil { engine, err := filter.DefaultEngine() if err != nil { return nil, err } stmt, err := engine.CompileToStatement(ctx, *find.MemoFilter, filter.RenderOptions{ Dialect: filter.DialectMySQL, PlaceholderOffset: 0, }) if err != nil { return nil, err } if stmt.SQL != "" { where = append(where, fmt.Sprintf("memo_id IN (SELECT id FROM memo WHERE %s)", stmt.SQL)) where = append(where, fmt.Sprintf("related_memo_id IN (SELECT id FROM memo WHERE %s)", stmt.SQL)) args = append(args, append(stmt.Args, stmt.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 } ================================================ FILE: store/db/mysql/memo_share.go ================================================ package mysql import ( "context" "strings" "github.com/pkg/errors" "github.com/usememos/memos/store" ) func (d *DB) CreateMemoShare(ctx context.Context, create *store.MemoShare) (*store.MemoShare, error) { fields := []string{"`uid`", "`memo_id`", "`creator_id`"} placeholders := []string{"?", "?", "?"} args := []any{create.UID, create.MemoID, create.CreatorID} if create.ExpiresTs != nil { fields = append(fields, "`expires_ts`") placeholders = append(placeholders, "?") args = append(args, *create.ExpiresTs) } stmt := "INSERT INTO `memo_share` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholders, ", ") + ")" 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) ms, err := d.GetMemoShare(ctx, &store.FindMemoShare{ID: &id}) if err != nil { return nil, err } if ms == nil { return nil, errors.Errorf("failed to create memo share") } return ms, nil } func (d *DB) ListMemoShares(ctx context.Context, find *store.FindMemoShare) ([]*store.MemoShare, error) { where, args := []string{"1 = 1"}, []any{} if find.ID != nil { where, args = append(where, "`id` = ?"), append(args, *find.ID) } if find.UID != nil { where, args = append(where, "`uid` = ?"), append(args, *find.UID) } if find.MemoID != nil { where, args = append(where, "`memo_id` = ?"), append(args, *find.MemoID) } rows, err := d.db.QueryContext(ctx, ` SELECT id, uid, memo_id, creator_id, created_ts, expires_ts FROM memo_share WHERE `+strings.Join(where, " AND ")+` ORDER BY id ASC`, args..., ) if err != nil { return nil, err } defer rows.Close() list := []*store.MemoShare{} for rows.Next() { ms := &store.MemoShare{} if err := rows.Scan( &ms.ID, &ms.UID, &ms.MemoID, &ms.CreatorID, &ms.CreatedTs, &ms.ExpiresTs, ); err != nil { return nil, err } list = append(list, ms) } if err := rows.Err(); err != nil { return nil, err } return list, nil } func (d *DB) GetMemoShare(ctx context.Context, find *store.FindMemoShare) (*store.MemoShare, error) { list, err := d.ListMemoShares(ctx, find) if err != nil { return nil, err } if len(list) == 0 { return nil, nil } return list[0], nil } func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare) error { where, args := []string{"1 = 1"}, []any{} if delete.ID != nil { where, args = append(where, "`id` = ?"), append(args, *delete.ID) } if delete.UID != nil { where, args = append(where, "`uid` = ?"), append(args, *delete.UID) } _, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...) return err } ================================================ FILE: store/db/mysql/mysql.go ================================================ 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 error") } 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_SCHEMA = DATABASE() AND 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 } ================================================ FILE: store/db/mysql/reaction.go ================================================ 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"}, []any{} 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) } if len(find.ContentIDList) > 0 { placeholders := make([]string, 0, len(find.ContentIDList)) for _, id := range find.ContentIDList { placeholders = append(placeholders, "?") args = append(args, id) } where = append(where, "`content_id` IN ("+strings.Join(placeholders, ",")+")") } 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 } ================================================ FILE: store/db/mysql/user.go ================================================ 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 len(find.Filters) > 0 { return nil, errors.Errorf("user filters are not supported") } 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 } ================================================ FILE: store/db/mysql/user_setting.go ================================================ 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) 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 } func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) { query := ` SELECT user_id, value FROM user_setting WHERE ` + "`key`" + ` = 'PERSONAL_ACCESS_TOKENS' AND JSON_SEARCH(value, 'one', ?, NULL, '$.tokens[*].tokenHash') IS NOT NULL ` var userID int32 var tokensJSON string err := d.db.QueryRowContext(ctx, query, tokenHash).Scan(&userID, &tokensJSON) if err != nil { return nil, err } patsUserSetting := &storepb.PersonalAccessTokensUserSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(tokensJSON), patsUserSetting); err != nil { return nil, err } for _, pat := range patsUserSetting.Tokens { if pat.TokenHash == tokenHash { return &store.PATQueryResult{ UserID: userID, PAT: pat, }, nil } } return nil, errors.New("PAT not found") } ================================================ FILE: store/db/postgres/attachment.go ================================================ package postgres import ( "context" "database/sql" "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) 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 attachment (" + 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, "attachment.id = "+placeholder(len(args)+1)), append(args, *v) } if v := find.UID; v != nil { where, args = append(where, "attachment.uid = "+placeholder(len(args)+1)), append(args, *v) } if v := find.CreatorID; v != nil { where, args = append(where, "attachment.creator_id = "+placeholder(len(args)+1)), append(args, *v) } if v := find.Filename; v != nil { where, args = append(where, "attachment.filename = "+placeholder(len(args)+1)), append(args, *v) } if v := find.FilenameSearch; v != nil { where, args = append(where, "attachment.filename LIKE "+placeholder(len(args)+1)), append(args, fmt.Sprintf("%%%s%%", *v)) } if v := find.MemoID; v != nil { where, args = append(where, "attachment.memo_id = "+placeholder(len(args)+1)), append(args, *v) } if len(find.MemoIDList) > 0 { holders := make([]string, 0, len(find.MemoIDList)) for _, id := range find.MemoIDList { holders = append(holders, placeholder(len(args)+1)) args = append(args, id) } where = append(where, "attachment.memo_id IN ("+strings.Join(holders, ", ")+")") } if find.HasRelatedMemo { where = append(where, "attachment.memo_id IS NOT NULL") } if v := find.StorageType; v != nil { where, args = append(where, "attachment.storage_type = "+placeholder(len(args)+1)), append(args, v.String()) } if len(find.Filters) > 0 { engine, err := filter.DefaultAttachmentEngine() if err != nil { return nil, errors.Wrap(err, "failed to get filter engine") } if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectPostgres, &where, &args); err != nil { return nil, errors.Wrap(err, "failed to append filter conditions") } } fields := []string{ "attachment.id AS id", "attachment.uid AS uid", "attachment.filename AS filename", "attachment.type AS type", "attachment.size AS size", "attachment.creator_id AS creator_id", "attachment.created_ts AS created_ts", "attachment.updated_ts AS updated_ts", "attachment.memo_id AS memo_id", "attachment.storage_type AS storage_type", "attachment.reference AS reference", "attachment.payload AS payload", "CASE WHEN memo.uid IS NOT NULL THEN memo.uid ELSE NULL END AS memo_uid", } if find.GetBlob { fields = append(fields, "attachment.blob AS blob") } query := fmt.Sprintf(` SELECT %s FROM attachment LEFT JOIN memo ON attachment.memo_id = memo.id WHERE %s ORDER BY attachment.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, &attachment.MemoUID, } 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 attachment 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 attachment 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 } ================================================ FILE: store/db/postgres/common.go ================================================ 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, ", ") } ================================================ FILE: store/db/postgres/idp.go ================================================ 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{"uid", "name", "type", "identifier_filter", "config"} args := []any{create.UID, 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) } if v := find.UID; v != nil { where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *v) } rows, err := d.db.QueryContext(ctx, ` SELECT id, uid, 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.UID, &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, uid, 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.UID, &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 } ================================================ FILE: store/db/postgres/inbox.go ================================================ 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) } if find.MessageType != nil { // Filter by message type using PostgreSQL JSON extraction // Note: The type field in JSON is stored as string representation of the enum name // Cast to JSONB since the column is TEXT if *find.MessageType == storepb.InboxMessage_TYPE_UNSPECIFIED { where, args = append(where, "(message::JSONB->>'type' IS NULL OR message::JSONB->>'type' = "+placeholder(len(args)+1)+")"), append(args, find.MessageType.String()) } else { where, args = append(where, "message::JSONB->>'type' = "+placeholder(len(args)+1)), append(args, find.MessageType.String()) } } 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 } ================================================ FILE: store/db/postgres/instance_setting.go ================================================ package postgres import ( "context" "strings" "github.com/usememos/memos/store" ) func (d *DB) UpsertInstanceSetting(ctx context.Context, upsert *store.InstanceSetting) (*store.InstanceSetting, 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) ListInstanceSettings(ctx context.Context, find *store.FindInstanceSetting) ([]*store.InstanceSetting, 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.InstanceSetting{} for rows.Next() { systemSettingMessage := &store.InstanceSetting{} 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) DeleteInstanceSetting(ctx context.Context, delete *store.DeleteInstanceSetting) error { stmt := `DELETE FROM system_setting WHERE name = $1` _, err := d.db.ExecContext(ctx, stmt, delete.Name) return err } ================================================ FILE: store/db/postgres/memo.go ================================================ 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} // Add custom timestamps if provided if create.CreatedTs != 0 { fields = append(fields, "created_ts") args = append(args, create.CreatedTs) } if create.UpdatedTs != 0 { fields = append(fields, "updated_ts") args = append(args, create.UpdatedTs) } 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{} engine, err := filter.DefaultEngine() if err != nil { return nil, err } if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectPostgres, &where, &args); err != nil { return nil, err } if v := find.ID; v != nil { where, args = append(where, "memo.id = "+placeholder(len(args)+1)), append(args, *v) } if len(find.IDList) > 0 { holders := make([]string, 0, len(find.IDList)) for _, id := range find.IDList { holders = append(holders, placeholder(len(args)+1)) args = append(args, id) } where = append(where, "memo.id IN ("+strings.Join(holders, ", ")+")") } if v := find.UID; v != nil { where, args = append(where, "memo.uid = "+placeholder(len(args)+1)), append(args, *v) } if len(find.UIDList) > 0 { holders := make([]string, 0, len(find.UIDList)) for _, uid := range find.UIDList { holders = append(holders, placeholder(len(args)+1)) args = append(args, uid) } where = append(where, "memo.uid IN ("+strings.Join(holders, ", ")+")") } 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.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 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) } // Add id as final tie-breaker orderBy = append(orderBy, "id DESC") 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`, `CASE WHEN parent_memo.uid IS NOT NULL THEN parent_memo.uid ELSE NULL END AS parent_uid`, } 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' LEFT JOIN memo AS parent_memo ON memo_relation.related_memo_id = parent_memo.id 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.ParentUID, } 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 } ================================================ FILE: store/db/postgres/memo_relation.go ================================================ 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) + `) ON CONFLICT (memo_id, related_memo_id, type) DO UPDATE SET type = EXCLUDED.type 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 len(find.MemoIDList) > 0 { memoPlaceholders := make([]string, len(find.MemoIDList)) for i, id := range find.MemoIDList { memoPlaceholders[i] = placeholder(len(args) + 1) args = append(args, id) } relatedPlaceholders := make([]string, len(find.MemoIDList)) for i, id := range find.MemoIDList { relatedPlaceholders[i] = placeholder(len(args) + 1) args = append(args, id) } where = append(where, fmt.Sprintf("(memo_id IN (%s) OR related_memo_id IN (%s))", strings.Join(memoPlaceholders, ", "), strings.Join(relatedPlaceholders, ", "))) } if find.MemoFilter != nil { engine, err := filter.DefaultEngine() if err != nil { return nil, err } stmt, err := engine.CompileToStatement(ctx, *find.MemoFilter, filter.RenderOptions{ Dialect: filter.DialectPostgres, PlaceholderOffset: len(args), }) if err != nil { return nil, err } if stmt.SQL != "" { where = append(where, fmt.Sprintf("memo_id IN (SELECT id FROM memo WHERE %s)", stmt.SQL)) args = append(args, stmt.Args...) stmtRelated, err := engine.CompileToStatement(ctx, *find.MemoFilter, filter.RenderOptions{ Dialect: filter.DialectPostgres, PlaceholderOffset: len(args), }) if err != nil { return nil, err } if stmtRelated.SQL != "" { where = append(where, fmt.Sprintf("related_memo_id IN (SELECT id FROM memo WHERE %s)", stmtRelated.SQL)) args = append(args, stmtRelated.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 } ================================================ FILE: store/db/postgres/memo_share.go ================================================ package postgres import ( "context" "database/sql" "errors" "strings" "github.com/usememos/memos/store" ) func (d *DB) CreateMemoShare(ctx context.Context, create *store.MemoShare) (*store.MemoShare, error) { fields := []string{"uid", "memo_id", "creator_id"} args := []any{create.UID, create.MemoID, create.CreatorID} if create.ExpiresTs != nil { fields = append(fields, "expires_ts") args = append(args, *create.ExpiresTs) } stmt := "INSERT INTO memo_share (" + 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) ListMemoShares(ctx context.Context, find *store.FindMemoShare) ([]*store.MemoShare, 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.UID != nil { where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *find.UID) } if find.MemoID != nil { where, args = append(where, "memo_id = "+placeholder(len(args)+1)), append(args, *find.MemoID) } rows, err := d.db.QueryContext(ctx, ` SELECT id, uid, memo_id, creator_id, created_ts, expires_ts FROM memo_share WHERE `+strings.Join(where, " AND ")+` ORDER BY id ASC`, args..., ) if err != nil { return nil, err } defer rows.Close() list := []*store.MemoShare{} for rows.Next() { ms := &store.MemoShare{} if err := rows.Scan( &ms.ID, &ms.UID, &ms.MemoID, &ms.CreatorID, &ms.CreatedTs, &ms.ExpiresTs, ); err != nil { return nil, err } list = append(list, ms) } if err := rows.Err(); err != nil { return nil, err } return list, nil } func (d *DB) GetMemoShare(ctx context.Context, find *store.FindMemoShare) (*store.MemoShare, 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.UID != nil { where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *find.UID) } if find.MemoID != nil { where, args = append(where, "memo_id = "+placeholder(len(args)+1)), append(args, *find.MemoID) } ms := &store.MemoShare{} if err := d.db.QueryRowContext(ctx, ` SELECT id, uid, memo_id, creator_id, created_ts, expires_ts FROM memo_share WHERE `+strings.Join(where, " AND ")+` LIMIT 1`, args..., ).Scan( &ms.ID, &ms.UID, &ms.MemoID, &ms.CreatorID, &ms.CreatedTs, &ms.ExpiresTs, ); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, err } return ms, nil } func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare) error { where, args := []string{"1 = 1"}, []any{} if delete.ID != nil { where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *delete.ID) } if delete.UID != nil { where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *delete.UID) } _, err := d.db.ExecContext(ctx, "DELETE FROM memo_share WHERE "+strings.Join(where, " AND "), args...) return err } ================================================ FILE: store/db/postgres/postgres.go ================================================ 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_catalog = current_database() AND 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 } ================================================ FILE: store/db/postgres/reaction.go ================================================ 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"}, []any{} 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) } if len(find.ContentIDList) > 0 { holders := make([]string, 0, len(find.ContentIDList)) for _, id := range find.ContentIDList { holders = append(holders, placeholder(len(args)+1)) args = append(args, id) } where = append(where, "content_id IN ("+strings.Join(holders, ", ")+")") } 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) 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 = $1", delete.ID) return err } ================================================ FILE: store/db/postgres/user.go ================================================ package postgres 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"} 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 len(find.Filters) > 0 { return nil, errors.Errorf("user filters are not supported") } 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 } ================================================ FILE: store/db/postgres/user_setting.go ================================================ package postgres import ( "context" "strings" "github.com/pkg/errors" 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 } func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) { // Simplified query: fetch all PERSONAL_ACCESS_TOKENS rows and search in Go // This matches SQLite/MySQL behavior and avoids PostgreSQL's strict JSONB errors query := ` SELECT user_id, value FROM user_setting WHERE key = 'PERSONAL_ACCESS_TOKENS' ` rows, err := d.db.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() // Iterate through all users with PAT settings for rows.Next() { var userID int32 var tokensJSON string if err := rows.Scan(&userID, &tokensJSON); err != nil { continue // Skip malformed rows } // Try to unmarshal - skip if invalid JSON patsUserSetting := &storepb.PersonalAccessTokensUserSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(tokensJSON), patsUserSetting); err != nil { continue // Skip invalid JSON } // Search for matching token hash for _, pat := range patsUserSetting.Tokens { if pat.TokenHash == tokenHash { return &store.PATQueryResult{ UserID: userID, PAT: pat, }, nil } } } if err := rows.Err(); err != nil { return nil, err } return nil, errors.New("PAT not found") } ================================================ FILE: store/db/postgres/user_setting_test.go ================================================ package postgres import ( "context" "database/sql" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) // TestGetUserByPATHashWithMissingData tests the fix for #5611 and #5612. // Verifies that GetUserByPATHash handles missing/malformed data gracefully // instead of throwing PostgreSQL JSONB errors. func TestGetUserByPATHashWithMissingData(t *testing.T) { if testing.Short() { t.Skip("Skipping PostgreSQL integration test in short mode") } // This test requires a real PostgreSQL connection // If DSN is not provided, skip the test dsn := getTestDSN() if dsn == "" { t.Skip("PostgreSQL DSN not provided, skipping test") } db, err := sql.Open("postgres", dsn) require.NoError(t, err) defer db.Close() // Create test database ctx := context.Background() driver := &DB{db: db} // Setup: Create user_setting table if needed _, err = db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS user_setting ( user_id INTEGER NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, UNIQUE(user_id, key) ) `) require.NoError(t, err) // Cleanup defer func() { db.ExecContext(ctx, "DELETE FROM user_setting WHERE user_id IN (1001, 1002, 1003)") }() t.Run("NoTokensKeyAtAll", func(t *testing.T) { // Test case: User has no PERSONAL_ACCESS_TOKENS key // This simulates fresh users or users upgraded from v0.25.3 result, err := driver.GetUserByPATHash(ctx, "any-hash") assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "PAT not found") }) t.Run("EmptyTokensArray", func(t *testing.T) { // Insert user with empty tokens array _, err := db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1001, "PERSONAL_ACCESS_TOKENS", `{"tokens":[]}`) require.NoError(t, err) result, err := driver.GetUserByPATHash(ctx, "any-hash") assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "PAT not found") }) t.Run("MalformedJSON", func(t *testing.T) { // Insert user with malformed JSON _, err := db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1002, "PERSONAL_ACCESS_TOKENS", `{invalid json}`) require.NoError(t, err) // Should handle gracefully without crashing result, err := driver.GetUserByPATHash(ctx, "any-hash") assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "PAT not found") }) t.Run("MissingTokensField", func(t *testing.T) { // Insert user with valid JSON but missing 'tokens' field _, err := db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1003, "PERSONAL_ACCESS_TOKENS", `{"someOtherField":"value"}`) require.NoError(t, err) // Should handle gracefully result, err := driver.GetUserByPATHash(ctx, "any-hash") assert.Error(t, err) assert.Nil(t, result) }) t.Run("ValidTokenFound", func(t *testing.T) { // Insert user with valid PAT validJSON := `{ "tokens": [ { "tokenId": "pat-test", "tokenHash": "hash-test-123", "description": "Test PAT" } ] }` _, err := db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1001, "PERSONAL_ACCESS_TOKENS", validJSON) require.NoError(t, err) // Should find the token result, err := driver.GetUserByPATHash(ctx, "hash-test-123") assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, int32(1001), result.UserID) assert.Equal(t, "pat-test", result.PAT.TokenId) assert.Equal(t, "hash-test-123", result.PAT.TokenHash) }) t.Run("MultipleUsersWithMixedData", func(t *testing.T) { // User 1001: Valid PAT validJSON := `{ "tokens": [ { "tokenId": "pat-user1", "tokenHash": "hash-user1", "description": "User 1 PAT" } ] }` _, err := db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1001, "PERSONAL_ACCESS_TOKENS", validJSON) require.NoError(t, err) // User 1002: Malformed JSON (should be skipped) _, err = db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1002, "PERSONAL_ACCESS_TOKENS", `{invalid}`) require.NoError(t, err) // User 1003: Empty array (should be skipped) _, err = db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1003, "PERSONAL_ACCESS_TOKENS", `{"tokens":[]}`) require.NoError(t, err) // Should still find user 1001's token despite other users having bad data result, err := driver.GetUserByPATHash(ctx, "hash-user1") assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, int32(1001), result.UserID) }) } // TestGetUserByPATHashPerformance ensures the simplified query doesn't cause performance issues. func TestGetUserByPATHashPerformance(t *testing.T) { if testing.Short() { t.Skip("Skipping performance test in short mode") } dsn := getTestDSN() if dsn == "" { t.Skip("PostgreSQL DSN not provided, skipping test") } db, err := sql.Open("postgres", dsn) require.NoError(t, err) defer db.Close() ctx := context.Background() driver := &DB{db: db} // Setup table _, err = db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS user_setting ( user_id INTEGER NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, UNIQUE(user_id, key) ) `) require.NoError(t, err) // Cleanup defer func() { db.ExecContext(ctx, "DELETE FROM user_setting WHERE user_id >= 2000 AND user_id < 2100") }() // Insert 100 users with PATs for i := 2000; i < 2100; i++ { json := `{ "tokens": [ { "tokenId": "pat-` + string(rune(i)) + `", "tokenHash": "hash-` + string(rune(i)) + `", "description": "Test PAT" } ] }` _, err = db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, i, "PERSONAL_ACCESS_TOKENS", json) require.NoError(t, err) } // Query should complete quickly even with 100 users result, err := driver.GetUserByPATHash(ctx, "hash-"+string(rune(2050))) assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, int32(2050), result.UserID) } // getTestDSN returns PostgreSQL DSN from environment or returns empty string. func getTestDSN() string { // For unit tests, we expect TEST_POSTGRES_DSN to be set. // Example: TEST_POSTGRES_DSN="postgresql://user:pass@localhost:5432/memos_test?sslmode=disable". return "" } // TestUpsertUserSetting tests basic upsert functionality. func TestUpsertUserSetting(t *testing.T) { dsn := getTestDSN() if dsn == "" { t.Skip("PostgreSQL DSN not provided, skipping test") } db, err := sql.Open("postgres", dsn) require.NoError(t, err) defer db.Close() ctx := context.Background() driver := &DB{db: db} // Setup _, err = db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS user_setting ( user_id INTEGER NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, UNIQUE(user_id, key) ) `) require.NoError(t, err) defer func() { db.ExecContext(ctx, "DELETE FROM user_setting WHERE user_id = 9999") }() // Test insert setting := &store.UserSetting{ UserID: 9999, Key: storepb.UserSetting_GENERAL, Value: `{"locale":"en"}`, } result, err := driver.UpsertUserSetting(ctx, setting) assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, int32(9999), result.UserID) // Test update (upsert on conflict) setting.Value = `{"locale":"zh"}` result, err = driver.UpsertUserSetting(ctx, setting) assert.NoError(t, err) assert.Equal(t, `{"locale":"zh"}`, result.Value) } ================================================ FILE: store/db/sqlite/attachment.go ================================================ package sqlite import ( "context" "database/sql" "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) 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 `attachment` (" + 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, "`attachment`.`id` = ?"), append(args, *v) } if v := find.UID; v != nil { where, args = append(where, "`attachment`.`uid` = ?"), append(args, *v) } if v := find.CreatorID; v != nil { where, args = append(where, "`attachment`.`creator_id` = ?"), append(args, *v) } if v := find.Filename; v != nil { where, args = append(where, "`attachment`.`filename` = ?"), append(args, *v) } if v := find.FilenameSearch; v != nil { where, args = append(where, "`attachment`.`filename` LIKE ?"), append(args, fmt.Sprintf("%%%s%%", *v)) } if v := find.MemoID; v != nil { where, args = append(where, "`attachment`.`memo_id` = ?"), append(args, *v) } if len(find.MemoIDList) > 0 { placeholders := make([]string, 0, len(find.MemoIDList)) for range find.MemoIDList { placeholders = append(placeholders, "?") } where = append(where, "`attachment`.`memo_id` IN ("+strings.Join(placeholders, ",")+")") for _, id := range find.MemoIDList { args = append(args, id) } } if find.HasRelatedMemo { where = append(where, "`attachment`.`memo_id` IS NOT NULL") } if find.StorageType != nil { where, args = append(where, "`attachment`.`storage_type` = ?"), append(args, find.StorageType.String()) } if len(find.Filters) > 0 { engine, err := filter.DefaultAttachmentEngine() if err != nil { return nil, errors.Wrap(err, "failed to get filter engine") } if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectSQLite, &where, &args); err != nil { return nil, errors.Wrap(err, "failed to append filter conditions") } } fields := []string{ "`attachment`.`id` AS `id`", "`attachment`.`uid` AS `uid`", "`attachment`.`filename` AS `filename`", "`attachment`.`type` AS `type`", "`attachment`.`size` AS `size`", "`attachment`.`creator_id` AS `creator_id`", "`attachment`.`created_ts` AS `created_ts`", "`attachment`.`updated_ts` AS `updated_ts`", "`attachment`.`memo_id` AS `memo_id`", "`attachment`.`storage_type` AS `storage_type`", "`attachment`.`reference` AS `reference`", "`attachment`.`payload` AS `payload`", "CASE WHEN `memo`.`uid` IS NOT NULL THEN `memo`.`uid` ELSE NULL END AS `memo_uid`", } if find.GetBlob { fields = append(fields, "`attachment`.`blob` AS `blob`") } query := "SELECT " + strings.Join(fields, ", ") + " FROM `attachment`" + " " + "LEFT JOIN `memo` ON `attachment`.`memo_id` = `memo`.`id`" + " " + "WHERE " + strings.Join(where, " AND ") + " " + "ORDER BY `attachment`.`updated_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 := 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, &attachment.MemoUID, } 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 `attachment` 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 `attachment` 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 } ================================================ FILE: store/db/sqlite/common.go ================================================ package sqlite import "google.golang.org/protobuf/encoding/protojson" var ( protojsonUnmarshaler = protojson.UnmarshalOptions{ DiscardUnknown: true, } ) ================================================ FILE: store/db/sqlite/functions.go ================================================ // Package sqlite provides SQLite driver implementation with custom functions. // Custom functions are registered globally on first use to extend SQLite's // limited ASCII-only text operations with proper Unicode support. package sqlite import ( "database/sql/driver" "sync" "golang.org/x/text/cases" msqlite "modernc.org/sqlite" ) var ( registerUnicodeLowerOnce sync.Once registerUnicodeLowerErr error // unicodeFold provides Unicode case folding for case-insensitive comparisons. // It's safe to use concurrently and reused across all function calls. unicodeFold = cases.Fold() ) // ensureUnicodeLowerRegistered registers the memos_unicode_lower custom function // with SQLite. This function provides proper Unicode case folding for case-insensitive // text comparisons, overcoming modernc.org/sqlite's lack of ICU extension. // // The function is registered once globally and is safe to call multiple times. func ensureUnicodeLowerRegistered() error { registerUnicodeLowerOnce.Do(func() { registerUnicodeLowerErr = msqlite.RegisterScalarFunction("memos_unicode_lower", 1, func(_ *msqlite.FunctionContext, args []driver.Value) (driver.Value, error) { if len(args) == 0 || args[0] == nil { return nil, nil } switch v := args[0].(type) { case string: return unicodeFold.String(v), nil case []byte: return unicodeFold.String(string(v)), nil default: return v, nil } }) }) return registerUnicodeLowerErr } ================================================ FILE: store/db/sqlite/idp.go ================================================ 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{"`uid`", "`name`", "`type`", "`identifier_filter`", "`config`"} args := []any{create.UID, 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) } if v := find.UID; v != nil { where, args = append(where, fmt.Sprintf("uid = $%d", len(args)+1)), append(args, *v) } rows, err := d.db.QueryContext(ctx, ` SELECT id, uid, 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.UID, &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, uid, name, type, identifier_filter, config ` var identityProvider store.IdentityProvider var typeString string if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( &identityProvider.ID, &identityProvider.UID, &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 } ================================================ FILE: store/db/sqlite/inbox.go ================================================ 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) } if find.MessageType != nil { // Filter by message type using JSON extraction // Note: The type field in JSON is stored as string representation of the enum name if *find.MessageType == storepb.InboxMessage_TYPE_UNSPECIFIED { where, args = append(where, "(JSON_EXTRACT(`message`, '$.type') IS NULL OR JSON_EXTRACT(`message`, '$.type') = ?)"), append(args, find.MessageType.String()) } else { where, args = append(where, "JSON_EXTRACT(`message`, '$.type') = ?"), append(args, find.MessageType.String()) } } 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 } ================================================ FILE: store/db/sqlite/instance_setting.go ================================================ package sqlite import ( "context" "strings" "github.com/usememos/memos/store" ) func (d *DB) UpsertInstanceSetting(ctx context.Context, upsert *store.InstanceSetting) (*store.InstanceSetting, 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) ListInstanceSettings(ctx context.Context, find *store.FindInstanceSetting) ([]*store.InstanceSetting, 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.InstanceSetting{} for rows.Next() { systemSettingMessage := &store.InstanceSetting{} 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) DeleteInstanceSetting(ctx context.Context, delete *store.DeleteInstanceSetting) error { stmt := "DELETE FROM system_setting WHERE name = ?" _, err := d.db.ExecContext(ctx, stmt, delete.Name) return err } ================================================ FILE: store/db/sqlite/memo.go ================================================ 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} // Add custom timestamps if provided if create.CreatedTs != 0 { fields = append(fields, "`created_ts`") placeholder = append(placeholder, "?") args = append(args, create.CreatedTs) } if create.UpdatedTs != 0 { fields = append(fields, "`updated_ts`") placeholder = append(placeholder, "?") args = append(args, create.UpdatedTs) } 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{} engine, err := filter.DefaultEngine() if err != nil { return nil, err } if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectSQLite, &where, &args); err != nil { return nil, err } if v := find.ID; v != nil { where, args = append(where, "`memo`.`id` = ?"), append(args, *v) } if len(find.IDList) > 0 { placeholders := make([]string, 0, len(find.IDList)) for range find.IDList { placeholders = append(placeholders, "?") } where = append(where, "`memo`.`id` IN ("+strings.Join(placeholders, ",")+")") for _, id := range find.IDList { args = append(args, id) } } if v := find.UID; v != nil { where, args = append(where, "`memo`.`uid` = ?"), append(args, *v) } if len(find.UIDList) > 0 { placeholders := make([]string, 0, len(find.UIDList)) for range find.UIDList { placeholders = append(placeholders, "?") } where = append(where, "`memo`.`uid` IN ("+strings.Join(placeholders, ",")+")") for _, uid := range find.UIDList { args = append(args, uid) } } 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.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 find.ExcludeComments { where = append(where, "`parent_uid` 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) } // Add id as final tie-breaker orderBy = append(orderBy, "`id` DESC") 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`", "CASE WHEN `parent_memo`.`uid` IS NOT NULL THEN `parent_memo`.`uid` ELSE NULL END AS `parent_uid`", } 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\" " + "LEFT JOIN `memo` AS `parent_memo` ON `memo_relation`.`related_memo_id` = `parent_memo`.`id` " + "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.ParentUID, } 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 } ================================================ FILE: store/db/sqlite/memo_relation.go ================================================ 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 (?, ?, ?) ON CONFLICT(memo_id, related_memo_id, type) DO UPDATE SET type = excluded.type 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 len(find.MemoIDList) > 0 { placeholders := make([]string, len(find.MemoIDList)) for i, id := range find.MemoIDList { placeholders[i] = "?" args = append(args, id) } inClause := strings.Join(placeholders, ", ") // Duplicate args for the second IN clause. for _, id := range find.MemoIDList { args = append(args, id) } where = append(where, fmt.Sprintf("(memo_id IN (%s) OR related_memo_id IN (%s))", inClause, inClause)) } if find.MemoFilter != nil { engine, err := filter.DefaultEngine() if err != nil { return nil, err } stmt, err := engine.CompileToStatement(ctx, *find.MemoFilter, filter.RenderOptions{Dialect: filter.DialectSQLite}) if err != nil { return nil, err } if stmt.SQL != "" { where = append(where, fmt.Sprintf("memo_id IN (SELECT id FROM memo WHERE %s)", stmt.SQL)) where = append(where, fmt.Sprintf("related_memo_id IN (SELECT id FROM memo WHERE %s)", stmt.SQL)) args = append(args, append(stmt.Args, stmt.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 } ================================================ FILE: store/db/sqlite/memo_share.go ================================================ package sqlite import ( "context" "database/sql" "errors" "strings" "github.com/usememos/memos/store" ) func (d *DB) CreateMemoShare(ctx context.Context, create *store.MemoShare) (*store.MemoShare, error) { fields := []string{"`uid`", "`memo_id`", "`creator_id`"} placeholders := []string{"?", "?", "?"} args := []any{create.UID, create.MemoID, create.CreatorID} if create.ExpiresTs != nil { fields = append(fields, "`expires_ts`") placeholders = append(placeholders, "?") args = append(args, *create.ExpiresTs) } stmt := "INSERT INTO `memo_share` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholders, ", ") + ") 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) ListMemoShares(ctx context.Context, find *store.FindMemoShare) ([]*store.MemoShare, error) { where, args := []string{"1 = 1"}, []any{} if find.ID != nil { where, args = append(where, "`id` = ?"), append(args, *find.ID) } if find.UID != nil { where, args = append(where, "`uid` = ?"), append(args, *find.UID) } if find.MemoID != nil { where, args = append(where, "`memo_id` = ?"), append(args, *find.MemoID) } rows, err := d.db.QueryContext(ctx, ` SELECT id, uid, memo_id, creator_id, created_ts, expires_ts FROM memo_share WHERE `+strings.Join(where, " AND ")+` ORDER BY id ASC`, args..., ) if err != nil { return nil, err } defer rows.Close() list := []*store.MemoShare{} for rows.Next() { ms := &store.MemoShare{} if err := rows.Scan( &ms.ID, &ms.UID, &ms.MemoID, &ms.CreatorID, &ms.CreatedTs, &ms.ExpiresTs, ); err != nil { return nil, err } list = append(list, ms) } if err := rows.Err(); err != nil { return nil, err } return list, nil } func (d *DB) GetMemoShare(ctx context.Context, find *store.FindMemoShare) (*store.MemoShare, error) { where, args := []string{"1 = 1"}, []any{} if find.ID != nil { where, args = append(where, "`id` = ?"), append(args, *find.ID) } if find.UID != nil { where, args = append(where, "`uid` = ?"), append(args, *find.UID) } if find.MemoID != nil { where, args = append(where, "`memo_id` = ?"), append(args, *find.MemoID) } ms := &store.MemoShare{} if err := d.db.QueryRowContext(ctx, ` SELECT id, uid, memo_id, creator_id, created_ts, expires_ts FROM memo_share WHERE `+strings.Join(where, " AND ")+` LIMIT 1`, args..., ).Scan( &ms.ID, &ms.UID, &ms.MemoID, &ms.CreatorID, &ms.CreatedTs, &ms.ExpiresTs, ); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, err } return ms, nil } func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare) error { where, args := []string{"1 = 1"}, []any{} if delete.ID != nil { where, args = append(where, "`id` = ?"), append(args, *delete.ID) } if delete.UID != nil { where, args = append(where, "`uid` = ?"), append(args, *delete.UID) } _, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...) return err } ================================================ FILE: store/db/sqlite/reaction.go ================================================ package sqlite import ( "context" "database/sql" "errors" "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"}, []any{} 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) } if len(find.ContentIDList) > 0 { placeholders := make([]string, 0, len(find.ContentIDList)) for range find.ContentIDList { placeholders = append(placeholders, "?") } if len(placeholders) > 0 { where = append(where, "content_id IN ("+strings.Join(placeholders, ",")+")") for _, id := range find.ContentIDList { args = append(args, id) } } } 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) GetReaction(ctx context.Context, find *store.FindReaction) (*store.Reaction, error) { where, args := []string{"1 = 1"}, []any{} 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) } reaction := &store.Reaction{} if err := d.db.QueryRowContext(ctx, ` SELECT id, created_ts, creator_id, content_id, reaction_type FROM reaction WHERE `+strings.Join(where, " AND ")+` LIMIT 1`, args..., ).Scan( &reaction.ID, &reaction.CreatedTs, &reaction.CreatorID, &reaction.ContentID, &reaction.ReactionType, ); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, err } 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 } ================================================ FILE: store/db/sqlite/sqlite.go ================================================ package sqlite import ( "context" "database/sql" "github.com/pkg/errors" // Note: modernc.org/sqlite driver is imported in functions.go where // RegisterScalarFunction is used. No blank import needed here. "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") } if err := ensureUnicodeLowerRegistered(); err != nil { return nil, errors.Wrap(err, "failed to register sqlite unicode lower function") } // 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. // - mmap size set to 0: it disables memory mapping, which can cause OOM errors on some systems. // // 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)&_pragma=mmap_size(0)") 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 } ================================================ FILE: store/db/sqlite/user.go ================================================ package sqlite 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, ", ") + ") 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 len(find.Filters) > 0 { return nil, errors.Errorf("user filters are not supported") } 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 } ================================================ FILE: store/db/sqlite/user_setting.go ================================================ package sqlite import ( "context" "strings" "github.com/pkg/errors" 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 } func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) { query := ` SELECT user_setting.user_id, user_setting.value FROM user_setting WHERE user_setting.key = 'PERSONAL_ACCESS_TOKENS' AND EXISTS ( SELECT 1 FROM json_each(json_extract(user_setting.value, '$.tokens')) AS token WHERE json_extract(token.value, '$.tokenHash') = ? ) ` var userID int32 var tokensJSON string err := d.db.QueryRowContext(ctx, query, tokenHash).Scan(&userID, &tokensJSON) if err != nil { return nil, err } patsUserSetting := &storepb.PersonalAccessTokensUserSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(tokensJSON), patsUserSetting); err != nil { return nil, err } for _, pat := range patsUserSetting.Tokens { if pat.TokenHash == tokenHash { return &store.PATQueryResult{ UserID: userID, PAT: pat, }, nil } } return nil, errors.New("PAT not found") } ================================================ FILE: store/driver.go ================================================ package store import ( "context" "database/sql" ) // 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) // 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 // InstanceSetting model related methods. UpsertInstanceSetting(ctx context.Context, upsert *InstanceSetting) (*InstanceSetting, error) ListInstanceSettings(ctx context.Context, find *FindInstanceSetting) ([]*InstanceSetting, error) DeleteInstanceSetting(ctx context.Context, delete *DeleteInstanceSetting) 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) GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, 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) GetReaction(ctx context.Context, find *FindReaction) (*Reaction, error) DeleteReaction(ctx context.Context, delete *DeleteReaction) error // MemoShare model related methods. CreateMemoShare(ctx context.Context, create *MemoShare) (*MemoShare, error) ListMemoShares(ctx context.Context, find *FindMemoShare) ([]*MemoShare, error) GetMemoShare(ctx context.Context, find *FindMemoShare) (*MemoShare, error) DeleteMemoShare(ctx context.Context, delete *DeleteMemoShare) error } ================================================ FILE: store/idp.go ================================================ 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 UID string Name string Type storepb.IdentityProvider_Type IdentifierFilter string Config string } type FindIdentityProvider struct { ID *int32 UID *string } 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, Uid: raw.UID, 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, UID: identityProvider.Uid, 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 } ================================================ FILE: store/inbox.go ================================================ package store import ( "context" storepb "github.com/usememos/memos/proto/gen/store" ) // InboxStatus represents the status of an inbox notification. type InboxStatus string const ( // UNREAD indicates the notification has not been read by the user. UNREAD InboxStatus = "UNREAD" // ARCHIVED indicates the notification has been archived/dismissed by the user. ARCHIVED InboxStatus = "ARCHIVED" ) func (s InboxStatus) String() string { return string(s) } // Inbox represents a notification in a user's inbox. type Inbox struct { ID int32 CreatedTs int64 SenderID int32 // The user who triggered the notification ReceiverID int32 // The user who receives the notification Status InboxStatus // Current status (unread/archived) Message *storepb.InboxMessage // The notification message content } // UpdateInbox contains fields that can be updated for an inbox item. type UpdateInbox struct { ID int32 Status InboxStatus } // FindInbox specifies filter criteria for querying inbox items. type FindInbox struct { ID *int32 SenderID *int32 ReceiverID *int32 Status *InboxStatus MessageType *storepb.InboxMessage_Type // Pagination Limit *int Offset *int } // DeleteInbox specifies which inbox item to delete. type DeleteInbox struct { ID int32 } // CreateInbox creates a new inbox notification. func (s *Store) CreateInbox(ctx context.Context, create *Inbox) (*Inbox, error) { return s.driver.CreateInbox(ctx, create) } // ListInboxes retrieves inbox items matching the filter criteria. func (s *Store) ListInboxes(ctx context.Context, find *FindInbox) ([]*Inbox, error) { return s.driver.ListInboxes(ctx, find) } // UpdateInbox updates an existing inbox item. func (s *Store) UpdateInbox(ctx context.Context, update *UpdateInbox) (*Inbox, error) { return s.driver.UpdateInbox(ctx, update) } // DeleteInbox permanently removes an inbox item. func (s *Store) DeleteInbox(ctx context.Context, delete *DeleteInbox) error { return s.driver.DeleteInbox(ctx, delete) } ================================================ FILE: store/instance_setting.go ================================================ package store import ( "context" "github.com/pkg/errors" "google.golang.org/protobuf/encoding/protojson" storepb "github.com/usememos/memos/proto/gen/store" ) type InstanceSetting struct { Name string Value string Description string } type FindInstanceSetting struct { Name string } type DeleteInstanceSetting struct { Name string } func (s *Store) UpsertInstanceSetting(ctx context.Context, upsert *storepb.InstanceSetting) (*storepb.InstanceSetting, error) { instanceSettingRaw := &InstanceSetting{ Name: upsert.Key.String(), } var valueBytes []byte var err error if upsert.Key == storepb.InstanceSettingKey_BASIC { valueBytes, err = protojson.Marshal(upsert.GetBasicSetting()) } else if upsert.Key == storepb.InstanceSettingKey_GENERAL { valueBytes, err = protojson.Marshal(upsert.GetGeneralSetting()) } else if upsert.Key == storepb.InstanceSettingKey_STORAGE { valueBytes, err = protojson.Marshal(upsert.GetStorageSetting()) } else if upsert.Key == storepb.InstanceSettingKey_MEMO_RELATED { valueBytes, err = protojson.Marshal(upsert.GetMemoRelatedSetting()) } else if upsert.Key == storepb.InstanceSettingKey_TAGS { valueBytes, err = protojson.Marshal(upsert.GetTagsSetting()) } else if upsert.Key == storepb.InstanceSettingKey_NOTIFICATION { valueBytes, err = protojson.Marshal(upsert.GetNotificationSetting()) } else { return nil, errors.Errorf("unsupported instance setting key: %v", upsert.Key) } if err != nil { return nil, errors.Wrap(err, "failed to marshal instance setting value") } valueString := string(valueBytes) instanceSettingRaw.Value = valueString instanceSettingRaw, err = s.driver.UpsertInstanceSetting(ctx, instanceSettingRaw) if err != nil { return nil, errors.Wrap(err, "Failed to upsert instance setting") } instanceSetting, err := convertInstanceSettingFromRaw(instanceSettingRaw) if err != nil { return nil, errors.Wrap(err, "Failed to convert instance setting") } s.instanceSettingCache.Set(ctx, instanceSetting.Key.String(), instanceSetting) return instanceSetting, nil } func (s *Store) ListInstanceSettings(ctx context.Context, find *FindInstanceSetting) ([]*storepb.InstanceSetting, error) { list, err := s.driver.ListInstanceSettings(ctx, find) if err != nil { return nil, err } instanceSettings := []*storepb.InstanceSetting{} for _, instanceSettingRaw := range list { instanceSetting, err := convertInstanceSettingFromRaw(instanceSettingRaw) if err != nil { return nil, errors.Wrap(err, "Failed to convert instance setting") } if instanceSetting == nil { continue } s.instanceSettingCache.Set(ctx, instanceSetting.Key.String(), instanceSetting) instanceSettings = append(instanceSettings, instanceSetting) } return instanceSettings, nil } func (s *Store) GetInstanceSetting(ctx context.Context, find *FindInstanceSetting) (*storepb.InstanceSetting, error) { if cache, ok := s.instanceSettingCache.Get(ctx, find.Name); ok { instanceSetting, ok := cache.(*storepb.InstanceSetting) if ok { return instanceSetting, nil } } list, err := s.ListInstanceSettings(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 instance settings with key %s", find.Name) } return list[0], nil } func (s *Store) GetInstanceBasicSetting(ctx context.Context) (*storepb.InstanceBasicSetting, error) { instanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{ Name: storepb.InstanceSettingKey_BASIC.String(), }) if err != nil { return nil, errors.Wrap(err, "failed to get instance basic setting") } instanceBasicSetting := &storepb.InstanceBasicSetting{} if instanceSetting != nil { instanceBasicSetting = instanceSetting.GetBasicSetting() } s.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_BASIC.String(), &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_BASIC, Value: &storepb.InstanceSetting_BasicSetting{BasicSetting: instanceBasicSetting}, }) return instanceBasicSetting, nil } func (s *Store) GetInstanceGeneralSetting(ctx context.Context) (*storepb.InstanceGeneralSetting, error) { instanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{ Name: storepb.InstanceSettingKey_GENERAL.String(), }) if err != nil { return nil, errors.Wrap(err, "failed to get instance general setting") } instanceGeneralSetting := &storepb.InstanceGeneralSetting{} if instanceSetting != nil { instanceGeneralSetting = instanceSetting.GetGeneralSetting() } s.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_GENERAL.String(), &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_GENERAL, Value: &storepb.InstanceSetting_GeneralSetting{GeneralSetting: instanceGeneralSetting}, }) return instanceGeneralSetting, 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{"👍", "👎", "❤️", "🎉", "😄", "😕", "😢", "😡"} func (s *Store) GetInstanceMemoRelatedSetting(ctx context.Context) (*storepb.InstanceMemoRelatedSetting, error) { instanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{ Name: storepb.InstanceSettingKey_MEMO_RELATED.String(), }) if err != nil { return nil, errors.Wrap(err, "failed to get instance general setting") } instanceMemoRelatedSetting := &storepb.InstanceMemoRelatedSetting{} if instanceSetting != nil { instanceMemoRelatedSetting = instanceSetting.GetMemoRelatedSetting() } if instanceMemoRelatedSetting.ContentLengthLimit < DefaultContentLengthLimit { instanceMemoRelatedSetting.ContentLengthLimit = DefaultContentLengthLimit } if len(instanceMemoRelatedSetting.Reactions) == 0 { instanceMemoRelatedSetting.Reactions = append(instanceMemoRelatedSetting.Reactions, DefaultReactions...) } s.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_MEMO_RELATED.String(), &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_MEMO_RELATED, Value: &storepb.InstanceSetting_MemoRelatedSetting{MemoRelatedSetting: instanceMemoRelatedSetting}, }) return instanceMemoRelatedSetting, nil } func (s *Store) GetInstanceTagsSetting(ctx context.Context) (*storepb.InstanceTagsSetting, error) { instanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{ Name: storepb.InstanceSettingKey_TAGS.String(), }) if err != nil { return nil, errors.Wrap(err, "failed to get instance tags setting") } instanceTagsSetting := &storepb.InstanceTagsSetting{} if instanceSetting != nil { instanceTagsSetting = instanceSetting.GetTagsSetting() } if instanceTagsSetting.Tags == nil { instanceTagsSetting.Tags = map[string]*storepb.InstanceTagMetadata{} } s.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_TAGS.String(), &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_TAGS, Value: &storepb.InstanceSetting_TagsSetting{TagsSetting: instanceTagsSetting}, }) return instanceTagsSetting, nil } func (s *Store) GetInstanceNotificationSetting(ctx context.Context) (*storepb.InstanceNotificationSetting, error) { instanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{ Name: storepb.InstanceSettingKey_NOTIFICATION.String(), }) if err != nil { return nil, errors.Wrap(err, "failed to get instance notification setting") } instanceNotificationSetting := &storepb.InstanceNotificationSetting{} if instanceSetting != nil { instanceNotificationSetting = instanceSetting.GetNotificationSetting() } if instanceNotificationSetting.Email == nil { instanceNotificationSetting.Email = &storepb.InstanceNotificationSetting_EmailSetting{} } s.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_NOTIFICATION.String(), &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_NOTIFICATION, Value: &storepb.InstanceSetting_NotificationSetting{NotificationSetting: instanceNotificationSetting}, }) return instanceNotificationSetting, nil } const ( defaultInstanceStorageType = storepb.InstanceStorageSetting_LOCAL defaultInstanceUploadSizeLimitMb = 30 defaultInstanceFilepathTemplate = "assets/{timestamp}_{filename}" ) func (s *Store) GetInstanceStorageSetting(ctx context.Context) (*storepb.InstanceStorageSetting, error) { instanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{ Name: storepb.InstanceSettingKey_STORAGE.String(), }) if err != nil { return nil, errors.Wrap(err, "failed to get instance storage setting") } instanceStorageSetting := &storepb.InstanceStorageSetting{} if instanceSetting != nil { instanceStorageSetting = instanceSetting.GetStorageSetting() } if instanceStorageSetting.StorageType == storepb.InstanceStorageSetting_STORAGE_TYPE_UNSPECIFIED { instanceStorageSetting.StorageType = defaultInstanceStorageType } if instanceStorageSetting.UploadSizeLimitMb == 0 { instanceStorageSetting.UploadSizeLimitMb = defaultInstanceUploadSizeLimitMb } if instanceStorageSetting.FilepathTemplate == "" { instanceStorageSetting.FilepathTemplate = defaultInstanceFilepathTemplate } s.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_STORAGE.String(), &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_STORAGE, Value: &storepb.InstanceSetting_StorageSetting{StorageSetting: instanceStorageSetting}, }) return instanceStorageSetting, nil } func convertInstanceSettingFromRaw(instanceSettingRaw *InstanceSetting) (*storepb.InstanceSetting, error) { instanceSetting := &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey(storepb.InstanceSettingKey_value[instanceSettingRaw.Name]), } switch instanceSettingRaw.Name { case storepb.InstanceSettingKey_BASIC.String(): basicSetting := &storepb.InstanceBasicSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), basicSetting); err != nil { return nil, err } instanceSetting.Value = &storepb.InstanceSetting_BasicSetting{BasicSetting: basicSetting} case storepb.InstanceSettingKey_GENERAL.String(): generalSetting := &storepb.InstanceGeneralSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), generalSetting); err != nil { return nil, err } instanceSetting.Value = &storepb.InstanceSetting_GeneralSetting{GeneralSetting: generalSetting} case storepb.InstanceSettingKey_STORAGE.String(): storageSetting := &storepb.InstanceStorageSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), storageSetting); err != nil { return nil, err } instanceSetting.Value = &storepb.InstanceSetting_StorageSetting{StorageSetting: storageSetting} case storepb.InstanceSettingKey_MEMO_RELATED.String(): memoRelatedSetting := &storepb.InstanceMemoRelatedSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), memoRelatedSetting); err != nil { return nil, err } instanceSetting.Value = &storepb.InstanceSetting_MemoRelatedSetting{MemoRelatedSetting: memoRelatedSetting} case storepb.InstanceSettingKey_TAGS.String(): tagsSetting := &storepb.InstanceTagsSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), tagsSetting); err != nil { return nil, err } instanceSetting.Value = &storepb.InstanceSetting_TagsSetting{TagsSetting: tagsSetting} case storepb.InstanceSettingKey_NOTIFICATION.String(): notificationSetting := &storepb.InstanceNotificationSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), notificationSetting); err != nil { return nil, err } instanceSetting.Value = &storepb.InstanceSetting_NotificationSetting{NotificationSetting: notificationSetting} default: // Skip unsupported instance setting key. return nil, nil } return instanceSetting, nil } ================================================ FILE: store/memo.go ================================================ 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" default: 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 ParentUID *string } type FindMemo struct { ID *int32 UID *string IDList []int32 UIDList []string // Standard fields RowStatus *RowStatus CreatorID *int32 // Domain specific fields VisibilityList []Visibility ExcludeContent bool ExcludeComments bool Filters []string // Pagination Limit *int Offset *int // Ordering OrderByPinned bool OrderByUpdatedTs 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 { // Clean up memo_relation records where this memo is either the source or target. if err := s.driver.DeleteMemoRelation(ctx, &DeleteMemoRelation{MemoID: &delete.ID}); err != nil { return err } if err := s.driver.DeleteMemoRelation(ctx, &DeleteMemoRelation{RelatedMemoID: &delete.ID}); err != nil { return err } // Clean up attachments linked to this memo. attachments, err := s.ListAttachments(ctx, &FindAttachment{MemoID: &delete.ID}) if err != nil { return err } for _, attachment := range attachments { if err := s.DeleteAttachment(ctx, &DeleteAttachment{ID: attachment.ID}); err != nil { return err } } return s.driver.DeleteMemo(ctx, delete) } ================================================ FILE: store/memo_relation.go ================================================ 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 // MemoIDList matches relations where memo_id OR related_memo_id is in the list. MemoIDList []int32 } 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) } ================================================ FILE: store/memo_share.go ================================================ package store import "context" // MemoShare is an access grant that permits read-only access to a memo via a bearer token. type MemoShare struct { ID int32 UID string MemoID int32 CreatorID int32 CreatedTs int64 ExpiresTs *int64 // nil means the share never expires } // FindMemoShare is used to filter memo shares in list/get queries. type FindMemoShare struct { ID *int32 UID *string MemoID *int32 } // DeleteMemoShare identifies a share grant to remove. type DeleteMemoShare struct { ID *int32 UID *string } // CreateMemoShare creates a new share grant. func (s *Store) CreateMemoShare(ctx context.Context, create *MemoShare) (*MemoShare, error) { return s.driver.CreateMemoShare(ctx, create) } // ListMemoShares returns all share grants matching the filter. func (s *Store) ListMemoShares(ctx context.Context, find *FindMemoShare) ([]*MemoShare, error) { return s.driver.ListMemoShares(ctx, find) } // GetMemoShare returns the first share grant matching the filter, or nil if none found. func (s *Store) GetMemoShare(ctx context.Context, find *FindMemoShare) (*MemoShare, error) { return s.driver.GetMemoShare(ctx, find) } // DeleteMemoShare removes a share grant. func (s *Store) DeleteMemoShare(ctx context.Context, delete *DeleteMemoShare) error { return s.driver.DeleteMemoShare(ctx, delete) } ================================================ FILE: store/migration/mysql/0.17/00__inbox.sql ================================================ -- 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 ); ================================================ FILE: store/migration/mysql/0.17/01__delete_activity.sql ================================================ DELETE FROM `activity`; ================================================ FILE: store/migration/mysql/0.18/00__extend_text.sql ================================================ 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; ================================================ FILE: store/migration/mysql/0.18/01__webhook.sql ================================================ -- 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 ); ================================================ FILE: store/migration/mysql/0.18/02__user_setting.sql ================================================ 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'; ================================================ FILE: store/migration/mysql/0.19/00__add_resource_name.sql ================================================ 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`); ================================================ FILE: store/migration/mysql/0.20/00__reaction.sql ================================================ -- 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`) ); ================================================ FILE: store/migration/mysql/0.21/00__user_description.sql ================================================ ALTER TABLE `user` ADD COLUMN `description` VARCHAR(256) NOT NULL DEFAULT ''; ================================================ FILE: store/migration/mysql/0.21/01__rename_uid.sql ================================================ ALTER TABLE `memo` RENAME COLUMN `resource_name` TO `uid`; ALTER TABLE `resource` RENAME COLUMN `resource_name` TO `uid`; ================================================ FILE: store/migration/mysql/0.22/00__resource_storage_type.sql ================================================ 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`; ================================================ FILE: store/migration/mysql/0.22/01__memo_tags.sql ================================================ ALTER TABLE `memo` ADD COLUMN `tags_temp` JSON; UPDATE `memo` SET `tags_temp` = '[]'; ALTER TABLE `memo` CHANGE COLUMN `tags_temp` `tags` JSON NOT NULL; ================================================ FILE: store/migration/mysql/0.22/02__memo_payload.sql ================================================ ALTER TABLE `memo` ADD COLUMN `payload_temp` JSON; UPDATE `memo` SET `payload_temp` = '{}'; ALTER TABLE `memo` CHANGE COLUMN `payload_temp` `payload` JSON NOT NULL; ================================================ FILE: store/migration/mysql/0.22/03__drop_tag.sql ================================================ DROP TABLE IF EXISTS `tag`; ================================================ FILE: store/migration/mysql/0.23/00__reactions.sql ================================================ 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'; ================================================ FILE: store/migration/mysql/0.24/00__memo.sql ================================================ -- Drop deprecated tags column. ALTER TABLE `memo` DROP COLUMN `tags`; ================================================ FILE: store/migration/mysql/0.24/01__memo_pinned.sql ================================================ -- 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; ================================================ FILE: store/migration/mysql/0.24/02__s3_reference_length.sql ================================================ -- https://github.com/usememos/memos/issues/4322 ALTER TABLE `resource` MODIFY `reference` TEXT NOT NULL DEFAULT (''); ================================================ FILE: store/migration/mysql/0.25/00__remove_webhook.sql ================================================ DROP TABLE IF EXISTS webhook; ================================================ FILE: store/migration/mysql/0.26/00__rename_resource_to_attachment.sql ================================================ RENAME TABLE resource TO attachment; ================================================ FILE: store/migration/mysql/0.26/01__drop_memo_organizer.sql ================================================ DROP TABLE IF EXISTS memo_organizer; ================================================ FILE: store/migration/mysql/0.26/02__migrate_host_to_admin.sql ================================================ UPDATE `user` SET `role` = 'ADMIN' WHERE `role` = 'HOST'; ================================================ FILE: store/migration/mysql/0.27/00__migrate_storage_setting.sql ================================================ -- Set storage type to DATABASE for existing instances that have no storage setting configured. -- This preserves backward-compatible behavior before the default was changed to LOCAL. INSERT INTO system_setting (name, value, description) SELECT 'STORAGE', '{"storageType":"DATABASE"}', '' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_setting WHERE name = 'STORAGE'); ================================================ FILE: store/migration/mysql/0.27/01__add_idp_uid.sql ================================================ -- Add uid column to idp table ALTER TABLE `idp` ADD COLUMN `uid` VARCHAR(256) NOT NULL DEFAULT ''; -- Populate uid for existing rows using hex of id as a fallback UPDATE `idp` SET `uid` = LOWER(LPAD(HEX(`id`), 8, '0')) WHERE `uid` = ''; -- Create unique index on uid ALTER TABLE `idp` ADD UNIQUE INDEX `idx_idp_uid` (`uid`); ================================================ FILE: store/migration/mysql/0.27/02__migrate_inbox_message_payload.sql ================================================ UPDATE `inbox` AS i JOIN `activity` AS a ON a.`id` = CAST(JSON_UNQUOTE(JSON_EXTRACT(i.`message`, '$.activityId')) AS UNSIGNED) SET i.`message` = JSON_SET( JSON_REMOVE(i.`message`, '$.activityId'), '$.memoComment', JSON_OBJECT( 'memoId', JSON_EXTRACT(a.`payload`, '$.memoComment.memoId'), 'relatedMemoId', JSON_EXTRACT(a.`payload`, '$.memoComment.relatedMemoId') ) ) WHERE JSON_EXTRACT(i.`message`, '$.activityId') IS NOT NULL; ================================================ FILE: store/migration/mysql/0.27/03__drop_activity.sql ================================================ DROP TABLE `activity`; ================================================ FILE: store/migration/mysql/0.27/04__memo_share.sql ================================================ -- memo_share stores per-memo share grants (one row per share link). -- uid is the opaque bearer token included in the share URL. -- ON DELETE CASCADE ensures grants are cleaned up when the parent memo is deleted. CREATE TABLE memo_share ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, uid VARCHAR(255) NOT NULL UNIQUE, memo_id INT NOT NULL, creator_id INT NOT NULL, created_ts BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()), expires_ts BIGINT DEFAULT NULL, FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE ); CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id); ================================================ FILE: store/migration/mysql/LATEST.sql ================================================ -- 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_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`) ); -- attachment CREATE TABLE `attachment` ( `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 ); -- idp CREATE TABLE `idp` ( `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `uid` VARCHAR(256) NOT NULL UNIQUE, `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`) ); -- memo_share CREATE TABLE `memo_share` ( `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `uid` VARCHAR(255) NOT NULL UNIQUE, `memo_id` INT NOT NULL, `creator_id` INT NOT NULL, `created_ts` BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()), `expires_ts` BIGINT DEFAULT NULL, FOREIGN KEY (`memo_id`) REFERENCES `memo`(`id`) ON DELETE CASCADE ); CREATE INDEX `idx_memo_share_memo_id` ON `memo_share`(`memo_id`); ================================================ FILE: store/migration/postgres/0.19/00__add_resource_name.sql ================================================ 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); ================================================ FILE: store/migration/postgres/0.20/00__reaction.sql ================================================ -- 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) ); ================================================ FILE: store/migration/postgres/0.21/00__user_description.sql ================================================ ALTER TABLE "user" ADD COLUMN description TEXT NOT NULL DEFAULT ''; ================================================ FILE: store/migration/postgres/0.21/01__rename_uid.sql ================================================ ALTER TABLE memo RENAME COLUMN resource_name TO uid; ALTER TABLE resource RENAME COLUMN resource_name TO uid; ================================================ FILE: store/migration/postgres/0.22/00__resource_storage_type.sql ================================================ 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; ================================================ FILE: store/migration/postgres/0.22/01__memo_tags.sql ================================================ ALTER TABLE memo ADD COLUMN tags JSONB NOT NULL DEFAULT '[]'; ================================================ FILE: store/migration/postgres/0.22/02__memo_payload.sql ================================================ ALTER TABLE memo ADD COLUMN payload JSONB NOT NULL DEFAULT '{}'; ================================================ FILE: store/migration/postgres/0.22/03__drop_tag.sql ================================================ DROP TABLE IF EXISTS tag; ================================================ FILE: store/migration/postgres/0.23/00__reactions.sql ================================================ 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'; ================================================ FILE: store/migration/postgres/0.24/00__memo.sql ================================================ -- Drop deprecated tags column. ALTER TABLE memo DROP COLUMN IF EXISTS tags; ================================================ FILE: store/migration/postgres/0.24/01__memo_pinned.sql ================================================ -- 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; ================================================ FILE: store/migration/postgres/0.25/00__remove_webhook.sql ================================================ DROP TABLE IF EXISTS webhook; ================================================ FILE: store/migration/postgres/0.26/00__rename_resource_to_attachment.sql ================================================ ALTER TABLE resource RENAME TO attachment; ================================================ FILE: store/migration/postgres/0.26/01__drop_memo_organizer.sql ================================================ DROP TABLE IF EXISTS memo_organizer; ================================================ FILE: store/migration/postgres/0.26/02__migrate_host_to_admin.sql ================================================ UPDATE "user" SET role = 'ADMIN' WHERE role = 'HOST'; ================================================ FILE: store/migration/postgres/0.27/00__migrate_storage_setting.sql ================================================ -- Set storage type to DATABASE for existing instances that have no storage setting configured. -- This preserves backward-compatible behavior before the default was changed to LOCAL. INSERT INTO system_setting (name, value, description) SELECT 'STORAGE', '{"storageType":"DATABASE"}', '' WHERE NOT EXISTS (SELECT 1 FROM system_setting WHERE name = 'STORAGE'); ================================================ FILE: store/migration/postgres/0.27/01__add_idp_uid.sql ================================================ -- Add uid column to idp table ALTER TABLE idp ADD COLUMN uid TEXT NOT NULL DEFAULT ''; -- Populate uid for existing rows using hex of id as a fallback UPDATE idp SET uid = LPAD(TO_HEX(id), 8, '0') WHERE uid = ''; -- Create unique index on uid CREATE UNIQUE INDEX IF NOT EXISTS idx_idp_uid ON idp (uid); ================================================ FILE: store/migration/postgres/0.27/02__migrate_inbox_message_payload.sql ================================================ UPDATE inbox AS i SET message = jsonb_set( i.message::jsonb - 'activityId', '{memoComment}', jsonb_build_object( 'memoId', a.payload->'memoComment'->'memoId', 'relatedMemoId', a.payload->'memoComment'->'relatedMemoId' ) )::text FROM activity AS a WHERE (i.message::jsonb->>'activityId')::integer = a.id; ================================================ FILE: store/migration/postgres/0.27/03__drop_activity.sql ================================================ DROP TABLE activity; ================================================ FILE: store/migration/postgres/0.27/04__memo_share.sql ================================================ -- memo_share stores per-memo share grants (one row per share link). -- uid is the opaque bearer token included in the share URL. -- ON DELETE CASCADE ensures grants are cleaned up when the parent memo is deleted. CREATE TABLE memo_share ( id SERIAL PRIMARY KEY, uid TEXT NOT NULL UNIQUE, memo_id INTEGER NOT NULL, creator_id INTEGER NOT NULL, created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), expires_ts BIGINT DEFAULT NULL, FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE ); CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id); ================================================ FILE: store/migration/postgres/LATEST.sql ================================================ -- 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_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) ); -- attachment CREATE TABLE attachment ( 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 '{}' ); -- idp CREATE TABLE idp ( id SERIAL PRIMARY KEY, uid TEXT NOT NULL UNIQUE, 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) ); -- memo_share CREATE TABLE memo_share ( id SERIAL PRIMARY KEY, uid TEXT NOT NULL UNIQUE, memo_id INTEGER NOT NULL, creator_id INTEGER NOT NULL, created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), expires_ts BIGINT DEFAULT NULL, FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE ); CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id); ================================================ FILE: store/migration/sqlite/0.10/00__activity.sql ================================================ -- 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 '{}' ); ================================================ FILE: store/migration/sqlite/0.11/00__user_avatar.sql ================================================ ALTER TABLE user ADD COLUMN avatar_url TEXT NOT NULL DEFAULT ''; ================================================ FILE: store/migration/sqlite/0.11/01__idp.sql ================================================ -- 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 '{}' ); ================================================ FILE: store/migration/sqlite/0.11/02__storage.sql ================================================ -- storage CREATE TABLE storage ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL, config TEXT NOT NULL DEFAULT '{}' ); ================================================ FILE: store/migration/sqlite/0.12/00__user_setting.sql ================================================ UPDATE user_setting SET key = 'memo-visibility' WHERE key = 'memoVisibility'; ================================================ FILE: store/migration/sqlite/0.12/01__system_setting.sql ================================================ 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'; ================================================ FILE: store/migration/sqlite/0.12/03__resource_internal_path.sql ================================================ ALTER TABLE resource ADD COLUMN internal_path TEXT NOT NULL DEFAULT ''; ================================================ FILE: store/migration/sqlite/0.12/04__resource_public_id.sql ================================================ 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))) ); ================================================ FILE: store/migration/sqlite/0.13/00__memo_relation.sql ================================================ -- 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) ); ================================================ FILE: store/migration/sqlite/0.13/01__remove_memo_organizer_id.sql ================================================ 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; ================================================ FILE: store/migration/sqlite/0.14/00__drop_resource_public_id.sql ================================================ 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; ================================================ FILE: store/migration/sqlite/0.14/01__create_indexes.sql ================================================ 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); ================================================ FILE: store/migration/sqlite/0.15/00__drop_user_open_id.sql ================================================ 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; ================================================ FILE: store/migration/sqlite/0.16/00__add_memo_id_to_resource.sql ================================================ 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; ================================================ FILE: store/migration/sqlite/0.16/01__drop_shortcut_table.sql ================================================ DROP TABLE IF EXISTS shortcut; ================================================ FILE: store/migration/sqlite/0.17/00__inbox.sql ================================================ -- 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 '{}' ); ================================================ FILE: store/migration/sqlite/0.17/01__delete_activities.sql ================================================ DELETE FROM activity; ================================================ FILE: store/migration/sqlite/0.18/00__webhook.sql ================================================ -- 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); ================================================ FILE: store/migration/sqlite/0.18/01__user_setting.sql ================================================ 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'; ================================================ FILE: store/migration/sqlite/0.19/00__add_resource_name.sql ================================================ 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); ================================================ FILE: store/migration/sqlite/0.2/00__user_role.sql ================================================ -- 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; ================================================ FILE: store/migration/sqlite/0.2/01__memo_visibility.sql ================================================ ALTER TABLE memo ADD COLUMN visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE'; ================================================ FILE: store/migration/sqlite/0.20/00__reaction.sql ================================================ -- 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) ); ================================================ FILE: store/migration/sqlite/0.21/00__user_description.sql ================================================ ALTER TABLE user ADD COLUMN description TEXT NOT NULL DEFAULT ""; ================================================ FILE: store/migration/sqlite/0.21/01__rename_uid.sql ================================================ ALTER TABLE memo RENAME COLUMN resource_name TO uid; ALTER TABLE resource RENAME COLUMN resource_name TO uid; ================================================ FILE: store/migration/sqlite/0.22/00__resource_storage_type.sql ================================================ 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; ================================================ FILE: store/migration/sqlite/0.22/01__memo_tags.sql ================================================ ALTER TABLE memo ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'; CREATE INDEX idx_memo_tags ON memo (tags); ================================================ FILE: store/migration/sqlite/0.22/02__memo_payload.sql ================================================ ALTER TABLE memo ADD COLUMN payload TEXT NOT NULL DEFAULT '{}'; ================================================ FILE: store/migration/sqlite/0.22/03__drop_tag.sql ================================================ DROP TABLE tag; ================================================ FILE: store/migration/sqlite/0.23/00__reactions.sql ================================================ 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'; ================================================ FILE: store/migration/sqlite/0.24/00__memo.sql ================================================ -- 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; ================================================ FILE: store/migration/sqlite/0.24/01__memo_pinned.sql ================================================ -- 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 ); ================================================ FILE: store/migration/sqlite/0.25/00__remove_webhook.sql ================================================ DROP TABLE IF EXISTS webhook; ================================================ FILE: store/migration/sqlite/0.26/00__rename_resource_to_attachment.sql ================================================ ALTER TABLE `resource` RENAME TO `attachment`; DROP INDEX IF EXISTS `idx_resource_creator_id`; CREATE INDEX `idx_attachment_creator_id` ON `attachment` (`creator_id`); DROP INDEX IF EXISTS `idx_resource_memo_id`; CREATE INDEX `idx_attachment_memo_id` ON `attachment` (`memo_id`); ================================================ FILE: store/migration/sqlite/0.26/01__drop_memo_organizer.sql ================================================ DROP TABLE IF EXISTS memo_organizer; ================================================ FILE: store/migration/sqlite/0.26/02__drop_indexes.sql ================================================ DROP INDEX IF EXISTS idx_user_username; DROP INDEX IF EXISTS idx_memo_creator_id; DROP INDEX IF EXISTS idx_attachment_creator_id; DROP INDEX IF EXISTS idx_attachment_memo_id; ================================================ FILE: store/migration/sqlite/0.26/03__alter_user_role.sql ================================================ 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', 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 DEFAULT '', description TEXT NOT NULL DEFAULT '' ); INSERT INTO user ( id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url, description ) SELECT id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url, description FROM user_old; DROP TABLE user_old; ================================================ FILE: store/migration/sqlite/0.26/04__migrate_host_to_admin.sql ================================================ UPDATE user SET role = 'ADMIN' WHERE role = 'HOST'; ================================================ FILE: store/migration/sqlite/0.27/00__migrate_storage_setting.sql ================================================ -- Set storage type to DATABASE for existing instances that have no storage setting configured. -- This preserves backward-compatible behavior before the default was changed to LOCAL. INSERT INTO system_setting (name, value, description) SELECT 'STORAGE', '{"storageType":"DATABASE"}', '' WHERE NOT EXISTS (SELECT 1 FROM system_setting WHERE name = 'STORAGE'); ================================================ FILE: store/migration/sqlite/0.27/01__add_idp_uid.sql ================================================ -- Add uid column to idp table ALTER TABLE idp ADD COLUMN uid TEXT NOT NULL DEFAULT ''; -- Populate uid for existing rows using hex of id as a fallback UPDATE idp SET uid = printf('%08x', id) WHERE uid = ''; -- Create unique index on uid CREATE UNIQUE INDEX IF NOT EXISTS idx_idp_uid ON idp (uid); ================================================ FILE: store/migration/sqlite/0.27/02__migrate_inbox_message_payload.sql ================================================ UPDATE inbox SET message = json_set( json_remove(message, '$.activityId'), '$.memoComment', json_object( 'memoId', ( SELECT json_extract(activity.payload, '$.memoComment.memoId') FROM activity WHERE activity.id = json_extract(inbox.message, '$.activityId') ), 'relatedMemoId', ( SELECT json_extract(activity.payload, '$.memoComment.relatedMemoId') FROM activity WHERE activity.id = json_extract(inbox.message, '$.activityId') ) ) ) WHERE json_extract(message, '$.activityId') IS NOT NULL AND EXISTS ( SELECT 1 FROM activity WHERE activity.id = json_extract(inbox.message, '$.activityId') ); ================================================ FILE: store/migration/sqlite/0.27/03__drop_activity.sql ================================================ DROP TABLE activity; ================================================ FILE: store/migration/sqlite/0.27/04__memo_share.sql ================================================ -- memo_share stores per-memo share grants (one row per share link). -- uid is the opaque bearer token included in the share URL. -- ON DELETE CASCADE ensures grants are cleaned up when the parent memo is deleted. CREATE TABLE memo_share ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, memo_id INTEGER NOT NULL, creator_id INTEGER NOT NULL, created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), expires_ts BIGINT DEFAULT NULL, FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE ); CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id); ================================================ FILE: store/migration/sqlite/0.3/00__memo_visibility_protected.sql ================================================ -- 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; ================================================ FILE: store/migration/sqlite/0.4/00__user_setting.sql ================================================ -- 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); ================================================ FILE: store/migration/sqlite/0.5/00__regenerate_foreign_keys.sql ================================================ 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; ================================================ FILE: store/migration/sqlite/0.5/01__memo_resource.sql ================================================ -- 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) ); ================================================ FILE: store/migration/sqlite/0.5/02__system_setting.sql ================================================ -- system_setting CREATE TABLE system_setting ( name TEXT NOT NULL, value TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', UNIQUE(name) ); ================================================ FILE: store/migration/sqlite/0.5/03__resource_extermal_link.sql ================================================ ALTER TABLE resource ADD COLUMN external_link TEXT NOT NULL DEFAULT ''; ================================================ FILE: store/migration/sqlite/0.6/00__recreate_triggers.sql ================================================ 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; ================================================ FILE: store/migration/sqlite/0.7/00__remove_fk.sql ================================================ 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; ================================================ FILE: store/migration/sqlite/0.7/01__remove_triggers.sql ================================================ 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`; ================================================ FILE: store/migration/sqlite/0.8/00__migration_history.sql ================================================ -- migration_history CREATE TABLE IF NOT EXISTS migration_history ( version TEXT NOT NULL PRIMARY KEY, created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')) ); ================================================ FILE: store/migration/sqlite/0.8/01__user_username.sql ================================================ -- 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; ================================================ FILE: store/migration/sqlite/0.9/00__tag.sql ================================================ -- tag CREATE TABLE tag ( name TEXT NOT NULL, creator_id INTEGER NOT NULL, UNIQUE(name, creator_id) ); ================================================ FILE: store/migration/sqlite/LATEST.sql ================================================ -- 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 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 '' ); -- 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 '{}' ); -- 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) ); -- attachment CREATE TABLE attachment ( 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 '{}' ); -- idp CREATE TABLE idp ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, 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) ); -- memo_share CREATE TABLE memo_share ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, memo_id INTEGER NOT NULL, creator_id INTEGER NOT NULL, created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), expires_ts BIGINT DEFAULT NULL, FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE ); CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id); ================================================ FILE: store/migrator.go ================================================ package store import ( "context" "database/sql" "embed" "fmt" "io/fs" "log/slog" "path/filepath" "slices" "strconv" "strings" "github.com/pkg/errors" "github.com/usememos/memos/internal/version" storepb "github.com/usememos/memos/proto/gen/store" ) // Migration System Overview: // // The migration system handles database schema versioning and upgrades. // Schema version is stored in system_setting. // // Migration Flow: // 1. preMigrate: Check if DB is initialized. If not, apply LATEST.sql // 2. checkMinimumUpgradeVersion: Verify installation can be upgraded (reject pre-0.22 installations) // 3. Migrate (prod mode): Apply incremental migrations from current to target version // 4. Migrate (demo mode): Seed database with demo data // // Version Tracking: // - New installations: Schema version set in system_setting immediately // - Existing v0.22+ installations: Schema version tracked in system_setting // - Pre-v0.22 installations: Must upgrade to v0.25.x first (migration_history → system_setting migration) // // Migration Files: // - Location: store/migration/{driver}/{version}/NN__description.sql // - Naming: NN is zero-padded patch number, description is human-readable // - Ordering: Files sorted lexicographically and applied in order // - LATEST.sql: Full schema for new installations (faster than incremental migrations) //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 initialize fresh installations with the current schema. LatestSchemaFileName = "LATEST.sql" // defaultSchemaVersion is used when schema version is empty or not set. // This handles edge cases for old installations without version tracking. defaultSchemaVersion = "0.0.0" ) // getSchemaVersionOrDefault returns the schema version or default if empty. // This ensures safe version comparisons and handles old installations. func getSchemaVersionOrDefault(schemaVersion string) string { if schemaVersion == "" { return defaultSchemaVersion } return schemaVersion } // isVersionEmpty checks if the schema version is empty or the default value. func isVersionEmpty(schemaVersion string) bool { return schemaVersion == "" || schemaVersion == defaultSchemaVersion } // shouldApplyMigration determines if a migration file should be applied. // It checks if the file's version is between the current DB version and target version. func shouldApplyMigration(fileVersion, currentDBVersion, targetVersion string) bool { currentDBVersionSafe := getSchemaVersionOrDefault(currentDBVersion) return version.IsVersionGreaterThan(fileVersion, currentDBVersionSafe) && version.IsVersionGreaterOrEqualThan(targetVersion, fileVersion) } // validateMigrationFileName checks if a migration file follows the expected naming convention. // Expected format: "NN__description.sql" where NN is a zero-padded number. func validateMigrationFileName(filename string) error { parts := strings.SplitN(filename, MigrateFileNameSplit, 2) if len(parts) < 2 { return errors.Errorf("invalid migration filename format (missing %s): %s", MigrateFileNameSplit, filename) } if _, err := strconv.Atoi(parts[0]); err != nil { return errors.Errorf("migration filename must start with a number: %s", filename) } return nil } // Migrate migrates the database schema to the latest version. // It checks the current schema version and applies any necessary migrations. // It also seeds the database with initial data if in demo mode. func (s *Store) Migrate(ctx context.Context) error { if err := s.preMigrate(ctx); err != nil { return errors.Wrap(err, "failed to pre-migrate") } instanceBasicSetting, err := s.GetInstanceBasicSetting(ctx) if err != nil { return errors.Wrap(err, "failed to get instance basic setting") } currentSchemaVersion, err := s.GetCurrentSchemaVersion() if err != nil { return errors.Wrap(err, "failed to get current schema version") } // Check for downgrade (but skip if schema version is empty - that means fresh/old installation) if !isVersionEmpty(instanceBasicSetting.SchemaVersion) && version.IsVersionGreaterThan(instanceBasicSetting.SchemaVersion, currentSchemaVersion) { slog.Error("cannot downgrade schema version", slog.String("databaseVersion", instanceBasicSetting.SchemaVersion), slog.String("currentVersion", currentSchemaVersion), ) return errors.Errorf("cannot downgrade schema version from %s to %s", instanceBasicSetting.SchemaVersion, currentSchemaVersion) } // Apply migrations if needed. if isVersionEmpty(instanceBasicSetting.SchemaVersion) || version.IsVersionGreaterThan(currentSchemaVersion, instanceBasicSetting.SchemaVersion) { if err := s.applyMigrations(ctx, instanceBasicSetting.SchemaVersion, currentSchemaVersion); err != nil { return errors.Wrap(err, "failed to apply migrations") } } if s.profile.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 } // applyMigrations applies all necessary migration files between current and target schema versions. // It runs all migrations in a single transaction for atomicity. func (s *Store) applyMigrations(ctx context.Context, currentSchemaVersion, targetSchemaVersion string) error { filePaths, err := fs.Glob(migrationFS, fmt.Sprintf("%s*/*.sql", s.getMigrationBasePath())) if err != nil { return errors.Wrap(err, "failed to read migration files") } slices.Sort(filePaths) // Start a transaction to apply migrations atomically tx, err := s.driver.GetDB().Begin() if err != nil { return errors.Wrap(err, "failed to start transaction") } defer tx.Rollback() // Use safe version for comparison (handles empty version case) schemaVersionForComparison := getSchemaVersionOrDefault(currentSchemaVersion) if isVersionEmpty(currentSchemaVersion) { slog.Warn("schema version is empty, treating as default for migration comparison", slog.String("defaultVersion", defaultSchemaVersion)) } slog.Info("start migration", slog.String("currentSchemaVersion", schemaVersionForComparison), slog.String("targetSchemaVersion", targetSchemaVersion)) migrationsApplied := 0 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 shouldApplyMigration(fileSchemaVersion, currentSchemaVersion, targetSchemaVersion) { // Validate migration filename before applying filename := filepath.Base(filePath) if err := validateMigrationFileName(filename); err != nil { slog.Warn("migration file has invalid name but will be applied", slog.String("file", filePath), slog.String("error", err.Error())) } slog.Info("applying migration", slog.String("file", filePath), slog.String("version", fileSchemaVersion)) bytes, err := migrationFS.ReadFile(filePath) if err != nil { return errors.Wrapf(err, "failed to read migration file: %s", filePath) } stmt := string(bytes) if err := s.execute(ctx, tx, stmt); err != nil { return errors.Wrapf(err, "failed to execute migration %s: %s", filePath, err) } migrationsApplied++ } } if err := tx.Commit(); err != nil { return errors.Wrap(err, "failed to commit migration transaction") } slog.Info("migration completed", slog.Int("migrationsApplied", migrationsApplied)) // Update schema version after successful migration if err := s.updateCurrentSchemaVersion(ctx, targetSchemaVersion); err != nil { return errors.Wrap(err, "failed to update current schema version") } return nil } // preMigrate checks if the database is initialized and applies the latest schema if not. 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() slog.Info("initializing new database with latest schema", slog.String("file", filePath)) 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") } slog.Info("database initialized successfully", slog.String("schemaVersion", schemaVersion)) if err := s.updateCurrentSchemaVersion(ctx, schemaVersion); err != nil { return errors.Wrap(err, "failed to update current schema version") } } if err := s.checkMinimumUpgradeVersion(ctx); err != nil { return err // Error message is already descriptive, don't wrap it } 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) } // seed seeds the database with initial data. // It reads all seed files from the embedded filesystem and executes them in order. // This is only supported for SQLite databases and is used in demo mode. func (s *Store) seed(ctx context.Context) error { // Only seed for SQLite - other databases should use production data if s.profile.Driver != "sqlite" { slog.Warn("seed is only supported for SQLite, skipping for other databases") 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. slices.Sort(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() 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") } slices.Sort(filePaths) if len(filePaths) == 0 { return fmt.Sprintf("%s.0", minorVersion), nil } return s.getSchemaVersionOfMigrateScript(filePaths[len(filePaths)-1]) } // getSchemaVersionOfMigrateScript extracts the schema version from the migration script file path. // It returns the schema version in the format "major.minor.patch". // If the file is the latest schema file, it returns the current schema version. 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 executes a SQL statement within a transaction context. // It returns an error if the execution fails. 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 } // updateCurrentSchemaVersion updates the current schema version in the instance basic setting. // It retrieves the instance basic setting, updates the schema version, and upserts the setting back to the database. func (s *Store) updateCurrentSchemaVersion(ctx context.Context, schemaVersion string) error { instanceBasicSetting, err := s.GetInstanceBasicSetting(ctx) if err != nil { return errors.Wrap(err, "failed to get instance basic setting") } instanceBasicSetting.SchemaVersion = schemaVersion if _, err := s.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_BASIC, Value: &storepb.InstanceSetting_BasicSetting{BasicSetting: instanceBasicSetting}, }); err != nil { return errors.Wrap(err, "failed to upsert instance setting") } return nil } // checkMinimumUpgradeVersion verifies the installation meets minimum version requirements for upgrade. // For very old installations (< v0.22.0), users must upgrade to v0.25.x first before upgrading to current version. // This is necessary because schema version tracking was moved from migration_history to system_setting in v0.22.0. func (s *Store) checkMinimumUpgradeVersion(ctx context.Context) error { instanceBasicSetting, err := s.GetInstanceBasicSetting(ctx) if err != nil { return errors.Wrap(err, "failed to get instance basic setting") } schemaVersion := instanceBasicSetting.SchemaVersion // Modern installation: nothing to check. if !isVersionEmpty(schemaVersion) && version.IsVersionGreaterOrEqualThan(schemaVersion, "0.22.0") { return nil } // Schema version is empty for fresh installs too, but preMigrate sets it before we get here. // So empty schema version on an initialized DB means a pre-v0.22 legacy installation. if isVersionEmpty(schemaVersion) { initialized, err := s.driver.IsInitialized(ctx) if err != nil { return errors.Wrap(err, "failed to check if database is initialized") } if !initialized { return nil } } // schemaVersion is either set but < 0.22.0, or empty on an initialized (legacy) DB. currentVersion, _ := s.GetCurrentSchemaVersion() return errors.Errorf( "Your Memos installation is too old to upgrade directly.\n\n"+ "Your current version: %s\n"+ "Target version: %s\n"+ "Minimum required: v0.22.0 (May 2024)\n\n"+ "Upgrade path:\n"+ "1. First upgrade to v0.25.3: https://github.com/usememos/memos/releases/tag/v0.25.3\n"+ "2. Start the server and verify it works\n"+ "3. Then upgrade to the latest version\n\n"+ "This is required because schema version tracking was moved from migration_history\n"+ "to system_setting in v0.22.0. The intermediate upgrade handles this migration safely.", schemaVersion, currentVersion, ) } ================================================ FILE: store/reaction.go ================================================ 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 ContentIDList []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) GetReaction(ctx context.Context, find *FindReaction) (*Reaction, error) { return s.driver.GetReaction(ctx, find) } func (s *Store) DeleteReaction(ctx context.Context, delete *DeleteReaction) error { return s.driver.DeleteReaction(ctx, delete) } ================================================ FILE: store/seed/DEMO_DATA_GUIDE.md ================================================ # Demo Data Guide This document describes the demo data used to showcase Memos features in demo mode. ## Overview The demo data includes **6 carefully selected memos** that showcase the key features of Memos without overwhelming new users. ## Demo User - **Username**: `demo` - **Password**: `secret` (default password) - **Role**: ADMIN - **Nickname**: Demo User ## Demo Memos (6 total) ### 1. Welcome Message (Pinned) ⭐ **Tags**: `#welcome` `#getting-started` A welcoming introduction that highlights key features of Memos. **Features showcased**: - H1/H2 headings - Bold text - Bullet lists - Horizontal rules - Multiple tags --- ### 2. Task Management Demo **Tags**: `#todo/work` Realistic weekly task list with three categories showing different work contexts. **Features showcased**: - Task lists (checkboxes) - Hierarchical tags (`#todo/work`) - Mixed completed/incomplete tasks - H2/H3 headings - Multiple sections --- ### 3. Code Snippet Reference **Tags**: `#dev/git` Practical Git commands reference with code examples in multiple languages. **Features showcased**: - Multiple code blocks - Bash syntax highlighting - JavaScript syntax highlighting - Inline code - Hierarchical tags (`#dev/git`) --- ### 4. Meeting Notes with Table **Tags**: `#meeting/standup` Professional meeting notes with structured data in a table format. **Features showcased**: - Markdown tables - Bold text - Bullet lists - Hierarchical tags (`#meeting/standup`) - Organized sections --- ### 5. Quick Idea **Tags**: `#ideas/apps` `#ai` Short-form idea capture demonstrating quick note-taking. **Features showcased**: - Brief memo format - Emoji usage - Multiple tags - Bold text --- ### 6. Sponsor Message (Pinned) ⭐ **Tags**: `#sponsor` Sponsor message with image and external link. **Features showcased**: - External links - Markdown image - Pinned memo - Clean formatting --- ## Additional Features ### Memo Relations - Memo #3 (Git commands) references Memo #1 (Welcome) ### Reactions Multiple memos have reactions to showcase the reaction system: - Welcome: 🎉 👍 - Tasks: ✅ - Quick idea: 💡 - Sponsor: 🚀 ### System Settings Configured with popular reactions: - 👍 💛 🔥 👏 😂 👌 🚀 👀 🤔 🤡 ❓ +1 🎉 💡 ✅ ## Coverage of Markdown Features | Feature | Demo Memos | |---------|-----------| | Headings (H1-H3) | 1, 2, 3, 4 | | Bold text | All | | Links | 6 | | Images | 6 | | Code blocks | 3 | | Inline code | 3 | | Task lists | 2 | | Bullet lists | 1, 2, 4 | | Tables | 4 | | Horizontal rules | 1 | | Hierarchical tags | All | | Emoji | 5 | | Pinned memos | 1, 6 | ## Tag Hierarchy The demo showcases hierarchical tag organization: ``` #welcome #getting-started #todo └─ #todo/work #dev └─ #dev/git #meeting └─ #meeting/standup #ideas └─ #ideas/apps #ai #sponsor ``` ## Use Cases Demonstrated 1. **Getting Started**: Welcome message with feature overview 2. **Work Management**: Tasks and meetings 3. **Developer Tools**: Code snippet references 4. **Quick Capture**: Brief idea notes 5. **Sponsor Content**: Product showcases with images ## Design Principles 1. **Quality over Quantity**: 6 focused memos instead of overwhelming users 2. **Realistic Content**: All memos use realistic, relatable scenarios 3. **Diverse Use Cases**: Covers professional, technical, and creative contexts 4. **Visual Appeal**: Clean formatting with emojis used naturally 5. **Feature Coverage**: Core features demonstrated without redundancy 6. **Hierarchical Organization**: Shows multi-level tag organization 7. **Clean and Scannable**: Easy to browse and understand at a glance ## Testing Demo Mode To run with demo data: ```bash # Start in demo mode go run ./cmd/memos --demo --port 8081 # Or use the binary ./memos --demo # Demo database location ./build/memos_demo.db ``` Login with: - Username: `demo` - Password: `secret` ## Updating Demo Data 1. Edit `store/seed/sqlite/01__dump.sql` 2. Delete `build/memos_demo.db` if it exists 3. Restart server in demo mode 4. New demo data will be loaded automatically ## Notes - All memos are set to PUBLIC visibility - **Two memos are pinned**: Welcome (#1) and Sponsor (#6) - User has ADMIN role to showcase all features - Reactions are distributed across memos - One memo relation demonstrates linking - Content is optimized for the compact markdown styles - Demo size is intentionally small (6 memos) to avoid overwhelming new users ================================================ FILE: store/seed/sqlite/01__dump.sql ================================================ -- Demo User (Admin) — password: demo INSERT INTO user (id,username,role,nickname,password_hash) VALUES(1,'demo','ADMIN','Demo User','$2a$10$c.slEVgf5b/3BnAWlLb/vOu7VVSOKJ4ljwMe9xzlx9IhKnvAsJYM6'); -- Alice (User) — password: demo INSERT INTO user (id,username,role,nickname,description,password_hash) VALUES(2,'alice','USER','Alice','Developer & avid reader 📚','$2a$10$c.slEVgf5b/3BnAWlLb/vOu7VVSOKJ4ljwMe9xzlx9IhKnvAsJYM6'); -- 1. Welcome Memo (Pinned) INSERT INTO memo (id,uid,creator_id,content,visibility,pinned,payload) VALUES(1,'welcome2memos001',1,replace('# Welcome to Memos!\n\nAn open-source, self-hosted note-taking tool. Capture thoughts instantly. Own them completely.\n\n## Key Features\n\n- **Write anything**: Quick notes, long-form writing, technical docs\n- **Markdown**: Full CommonMark + GFM syntax\n- **Task Lists**: Track to-dos inline with `- [ ]` syntax\n- **Tags**: Use #hashtags to organize your memos\n- **Attachments**: Images, videos, documents — drag & drop\n- **Location**: Geotag memos to remember where ideas struck\n- **Reactions & Comments**: Engage with any memo\n- **Relations**: Connect and reference related memos\n\n---\n\nExplore the demo memos below to see what''s possible! #welcome #getting-started','\n',char(10)),'PUBLIC',1,'{"tags":["welcome","getting-started"],"property":{"hasLink":false}}'); -- 2. Sponsor Memo (Pinned) INSERT INTO memo (id,uid,creator_id,content,visibility,pinned,payload) VALUES(2,'sponsor0000001',1,replace('Memos is free and open source, made possible by the generous support of our sponsors. 🙏\n\n---\n\n**[Warp — The AI-powered terminal built for speed and collaboration](https://go.warp.dev/memos)**\n\nWarp is a modern terminal reimagined with AI built in — autocomplete commands, debug errors inline, and collaborate with your team without leaving the terminal.\n\nWarp - The AI-powered terminal built for speed and collaboration\n\n---\n\n**[TestMu AI — The world''s first full-stack Agentic AI Quality Engineering platform](https://www.testmuai.com/?utm_medium=sponsor&utm_source=memos)**\n\nTestMu AI brings autonomous AI agents to your QA pipeline — from test generation to execution and reporting, all without manual scripting.\n\nTestMu AI\n\n---\n\n**[SSD Nodes — Affordable VPS hosting for self-hosters](https://ssdnodes.com/?utm_source=memos&utm_medium=sponsor)**\n\nHigh-performance VPS servers at prices that make self-hosting a no-brainer. Perfect for running your own Memos instance.\n\nSSD Nodes\n\n---\n\nInterested in sponsoring? Visit [GitHub Sponsors](https://github.com/sponsors/usememos) to learn more.\n\n#sponsors','\n',char(10)),'PUBLIC',1,'{"tags":["sponsors"],"property":{"hasLink":true}}'); -- 3. AI Skills — boojack/skills workflow, references the example definition doc INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(3,'aiskillsrepo001',1,replace('Been diving into AI agent programming lately — trying to figure out how to make AI actually reliable for complex dev tasks.\n\nThe core problem I keep running into: AI starts writing code before it fully understands the problem, then goes off in the wrong direction. The fix is surprisingly simple — force it through a pipeline: define the issue first, then design, then plan, then execute. Each stage has a concrete artifact, so there''s no room to skip ahead.\n\n**[boojack/skills](https://github.com/boojack/skills)** packages exactly this into four slash commands — `/defining-issues`, `/writing-designs`, `/planning-tasks`, `/executing-tasks` — that work with Claude Code, Cursor, Gemini CLI, and more.\n\n```bash\nnpx skills add boojack/skills\n```\n\n> 📄 Linked below: an example issue definition generated with `/defining-issues`.\n\n#ai #programming','\n',char(10)),'PUBLIC','{"tags":["ai","programming"],"property":{"hasLink":true,"hasCode":true},"location":{"placeholder":"San Francisco, California, United States","latitude":37.7749,"longitude":-122.4194}}'); -- 4. Example issue definition doc produced by /defining-issues (referenced by AI Skills memo) INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(4,'markdownshowcs1',1,replace('## 📄 Issue Definition: Add Full-Text Search to Memos #ai #programming\n\n*Generated with `/defining-issues` from [boojack/skills](https://github.com/boojack/skills)*\n\n---\n\n### Background\n\nUsers rely on tag filtering and manual scrolling to find memos. As the memo count grows, discoverability becomes a pain point with no way to search by keyword.\n\n### Issue Statement\n\nThere is no full-text search capability. Users cannot search memo content by keyword, making it hard to resurface older notes or find related ideas.\n\n### Current State\n\n- Tag-based filtering works via `#hashtag` syntax\n- No search index exists in the database\n- The API has no search endpoint\n- Browsing is limited to chronological scroll or tag drill-down\n\n### Proposed Scope\n\n- Add a search input to the main UI\n- Implement SQLite FTS5 full-text indexing on `memo.content`\n- Return ranked results via `GET /api/memos?search=`\n- Highlight matched terms in search results\n\n### Non-Goals\n\n- Semantic / vector search\n- Search across attachments or comments\n- Cross-user search for admins\n\n### Open Questions\n\n1. Should search respect memo visibility (`PUBLIC` / `PRIVATE`)?\n2. Do we index archived memos?\n3. Real-time results as-you-type, or on submit?\n4. Should tags be weighted higher than body text in ranking?','\n',char(10)),'PUBLIC','{"tags":["ai","programming"],"property":{"hasLink":true,"hasCode":true}}'); -- 5. Travel Bucket List (has location: Paris) INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(5,'travelbucket01',1,replace('## 🌍 My Travel Bucket List #travel #bucketlist\n\n### Places I''ve Been\n- [x] Paris, France — Amazing food and art!\n- [x] Shanghai, China — Modern skyline meets ancient temples\n- [x] Grand Canyon, USA — Breathtaking views\n- [x] Barcelona, Spain — Gaudí''s architecture is incredible\n\n### Dream Destinations\n- [ ] Northern Lights in Iceland\n- [ ] Safari in Tanzania\n- [ ] Great Barrier Reef, Australia\n- [ ] Machu Picchu, Peru\n- [ ] Santorini, Greece\n- [ ] New Zealand road trip\n\n### 2026 Plans\n- [ ] Book tickets to Iceland for winter\n- [ ] Research best time to visit Patagonia\n- [ ] Save up for Australia trip','\n',char(10)),'PUBLIC','{"tags":["travel","bucketlist"],"property":{"hasTaskList":true,"hasIncompleteTasks":true},"location":{"placeholder":"Paris, Île-de-France, France","latitude":48.8566,"longitude":2.3522}}'); -- 6. Movie Watchlist — posted by Alice INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(6,'moviewatch00001',2,replace('## 🎬 February Movie Marathon #movies #watchlist\n\nCatching up on films I''ve been meaning to watch!\n\n### This Month''s Queue\n\n| Movie | Genre | Status | Rating |\n|-------|-------|--------|--------|\n| The Grand Budapest Hotel | Comedy/Drama | ✅ Watched | ⭐⭐⭐⭐⭐ |\n| Inception | Sci-Fi | ✅ Watched | ⭐⭐⭐⭐⭐ |\n| Spirited Away | Animation | ✅ Watched | ⭐⭐⭐⭐⭐ |\n| Dune: Part Two | Sci-Fi | 📅 This weekend | — |\n| Oppenheimer | Biography | 📋 Queued | — |\n\n### Notes\n- Grand Budapest Hotel: Wes Anderson''s visual style is *chef''s kiss* ✨\n- Inception: Need to watch again to catch all the details\n- Spirited Away: Studio Ghibli never disappoints!\n\n---\n\n**Next month**: Planning a full Miyazaki marathon 🎨','\n',char(10)),'PUBLIC','{"tags":["movies","watchlist"],"property":{"hasLink":false}}'); -- 7. Comment on Welcome (by Alice) INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(7,'welcomecmt00001',2,'Just set up my own instance — this is exactly the note-taking app I''ve been looking for! The interface is so clean 🙌','PUBLIC','{"property":{"hasLink":false}}'); -- 8. Comment on AI Skills (by Alice) INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(8,'aiskillscmt0001',2,'Just tried `/defining-issues` on a backlog item that''s been vague for weeks — the output `definition.md` was clearer than anything I''d written by hand. The "no solution language" constraint really forces you to think. 🤯','PUBLIC','{"property":{"hasLink":false}}'); -- 9. Reply on AI Skills (by Demo) INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(9,'aiskillscmt0002',1,'Exactly — and once you have a solid `definition.md`, `/writing-designs` is scary good. It actually cites real engineering references instead of just making things up 🚀','PUBLIC','{"property":{"hasLink":false}}'); -- Memo Relations INSERT INTO memo_relation VALUES(3,4,'REFERENCE'); -- AI Skills references the example issue definition doc INSERT INTO memo_relation VALUES(7,1,'COMMENT'); -- Alice comments on Welcome INSERT INTO memo_relation VALUES(8,3,'COMMENT'); -- Alice comments on AI Skills INSERT INTO memo_relation VALUES(9,3,'COMMENT'); -- Demo replies on AI Skills -- Reactions INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(1,1,'memos/welcome2memos001','🎉'); INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(2,2,'memos/welcome2memos001','👍'); INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(3,1,'memos/welcome2memos001','👏'); INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(4,2,'memos/aiskillsrepo001','🔥'); INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(5,1,'memos/aiskillsrepo001','💡'); INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(6,2,'memos/aiskillsrepo001','👍'); INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(7,1,'memos/sponsor0000001','🚀'); INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(8,2,'memos/sponsor0000001','👍'); INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(9,2,'memos/markdownshowcs1','💡'); INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(10,2,'memos/travelbucket01','👀'); -- System Settings INSERT INTO system_setting VALUES ('MEMO_RELATED', '{"contentLengthLimit":8192,"enableAutoCompact":true,"enableComment":true,"enableLocation":true,"defaultVisibility":"PUBLIC","reactions":["👍","💛","🔥","👏","😂","👌","🚀","👀","🤔","🤡","❓","+1","🎉","💡","✅"]}', ''); ================================================ FILE: store/store.go ================================================ 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 instanceSettingCache *cache.Cache // cache for instance 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, instanceSettingCache: 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.instanceSettingCache.Close() s.userCache.Close() s.userSettingCache.Close() return s.driver.Close() } ================================================ FILE: store/test/README.md ================================================ # 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. ================================================ FILE: store/test/attachment_filter_test.go ================================================ package test import ( "testing" "time" "github.com/stretchr/testify/require" ) // ============================================================================= // Filename Field Tests // Schema: filename (string, supports contains) // ============================================================================= func TestAttachmentFilterFilenameContains(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("report.pdf").MimeType("application/pdf")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("document.pdf").MimeType("application/pdf")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("image.png").MimeType("image/png")) // Test: filename.contains("report") - single match attachments := tc.ListWithFilter(`filename.contains("report")`) require.Len(t, attachments, 1) require.Contains(t, attachments[0].Filename, "report") // Test: filename.contains(".pdf") - multiple matches attachments = tc.ListWithFilter(`filename.contains(".pdf")`) require.Len(t, attachments, 2) // Test: filename.contains("nonexistent") - no matches attachments = tc.ListWithFilter(`filename.contains("nonexistent")`) require.Len(t, attachments, 0) } func TestAttachmentFilterFilenameSpecialCharacters(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID). Filename("file_with-special.chars@2024.pdf").MimeType("application/pdf")) // Test: filename.contains with underscore attachments := tc.ListWithFilter(`filename.contains("_with")`) require.Len(t, attachments, 1) // Test: filename.contains with @ attachments = tc.ListWithFilter(`filename.contains("@2024")`) require.Len(t, attachments, 1) } func TestAttachmentFilterFilenameUnicode(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID). Filename("document_报告.pdf").MimeType("application/pdf")) attachments := tc.ListWithFilter(`filename.contains("报告")`) require.Len(t, attachments, 1) } // ============================================================================= // Mime Type Field Tests // Schema: mime_type (string, ==, !=) // ============================================================================= func TestAttachmentFilterMimeTypeEquals(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("image.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("photo.jpeg").MimeType("image/jpeg")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("document.pdf").MimeType("application/pdf")) // Test: mime_type == "image/png" attachments := tc.ListWithFilter(`mime_type == "image/png"`) require.Len(t, attachments, 1) require.Equal(t, "image/png", attachments[0].Type) // Test: mime_type == "application/pdf" attachments = tc.ListWithFilter(`mime_type == "application/pdf"`) require.Len(t, attachments, 1) require.Equal(t, "application/pdf", attachments[0].Type) } func TestAttachmentFilterMimeTypeNotEquals(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("image.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("document.pdf").MimeType("application/pdf")) attachments := tc.ListWithFilter(`mime_type != "image/png"`) require.Len(t, attachments, 1) require.Equal(t, "application/pdf", attachments[0].Type) } func TestAttachmentFilterMimeTypeInList(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("image.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("photo.jpeg").MimeType("image/jpeg")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("document.pdf").MimeType("application/pdf")) // Test: mime_type in ["image/png", "image/jpeg"] - matches images attachments := tc.ListWithFilter(`mime_type in ["image/png", "image/jpeg"]`) require.Len(t, attachments, 2) // Test: mime_type in ["video/mp4"] - no matches attachments = tc.ListWithFilter(`mime_type in ["video/mp4"]`) require.Len(t, attachments, 0) } // ============================================================================= // Create Time Field Tests // Schema: create_time (timestamp, all comparison operators) // Functions: now(), arithmetic (+, -, *) // ============================================================================= func TestAttachmentFilterCreateTimeComparison(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() now := time.Now().Unix() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("test.png").MimeType("image/png")) // Test: create_time < future (should match) attachments := tc.ListWithFilter(`create_time < ` + formatInt64(now+3600)) require.Len(t, attachments, 1) // Test: create_time > past (should match) attachments = tc.ListWithFilter(`create_time > ` + formatInt64(now-3600)) require.Len(t, attachments, 1) // Test: create_time > future (should not match) attachments = tc.ListWithFilter(`create_time > ` + formatInt64(now+3600)) require.Len(t, attachments, 0) } func TestAttachmentFilterCreateTimeWithNow(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("test.png").MimeType("image/png")) // Test: create_time < now() + 5 (buffer for container clock drift) attachments := tc.ListWithFilter(`create_time < now() + 5`) require.Len(t, attachments, 1) // Test: create_time > now() + 5 (should not match) attachments = tc.ListWithFilter(`create_time > now() + 5`) require.Len(t, attachments, 0) } func TestAttachmentFilterCreateTimeArithmetic(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("test.png").MimeType("image/png")) // Test: create_time >= now() - 3600 (attachments created in last hour) attachments := tc.ListWithFilter(`create_time >= now() - 3600`) require.Len(t, attachments, 1) // Test: create_time < now() - 86400 (attachments older than 1 day - should be empty) attachments = tc.ListWithFilter(`create_time < now() - 86400`) require.Len(t, attachments, 0) // Test: Multiplication - create_time >= now() - 60 * 60 attachments = tc.ListWithFilter(`create_time >= now() - 60 * 60`) require.Len(t, attachments, 1) } func TestAttachmentFilterAllComparisonOperators(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("test.png").MimeType("image/png")) // Test: < (less than) attachments := tc.ListWithFilter(`create_time < now() + 3600`) require.Len(t, attachments, 1) // Test: <= (less than or equal) with buffer for clock drift attachments = tc.ListWithFilter(`create_time < now() + 5`) require.Len(t, attachments, 1) // Test: > (greater than) attachments = tc.ListWithFilter(`create_time > now() - 3600`) require.Len(t, attachments, 1) // Test: >= (greater than or equal) attachments = tc.ListWithFilter(`create_time >= now() - 60`) require.Len(t, attachments, 1) } // ============================================================================= // Memo ID Field Tests // Schema: memo_id (int, ==, !=) // ============================================================================= func TestAttachmentFilterMemoIdEquals(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContextWithUser(t) defer tc.Close() memo1 := tc.CreateMemo("memo-1", "Memo 1") memo2 := tc.CreateMemo("memo-2", "Memo 2") tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("memo1_attachment.png").MimeType("image/png").MemoID(&memo1.ID)) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("memo2_attachment.png").MimeType("image/png").MemoID(&memo2.ID)) attachments := tc.ListWithFilter(`memo_id == ` + formatInt32(memo1.ID)) require.Len(t, attachments, 1) require.Equal(t, &memo1.ID, attachments[0].MemoID) } func TestAttachmentFilterMemoIdNotEquals(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContextWithUser(t) defer tc.Close() memo1 := tc.CreateMemo("memo-1", "Memo 1") memo2 := tc.CreateMemo("memo-2", "Memo 2") tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("memo1_attachment.png").MimeType("image/png").MemoID(&memo1.ID)) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("memo2_attachment.png").MimeType("image/png").MemoID(&memo2.ID)) attachments := tc.ListWithFilter(`memo_id != ` + formatInt32(memo1.ID)) require.Len(t, attachments, 1) require.Equal(t, &memo2.ID, attachments[0].MemoID) } // ============================================================================= // Logical Operator Tests // Operators: && (AND), || (OR), ! (NOT) // ============================================================================= func TestAttachmentFilterLogicalAnd(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("image.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("photo.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("image.pdf").MimeType("application/pdf")) attachments := tc.ListWithFilter(`mime_type == "image/png" && filename.contains("image")`) require.Len(t, attachments, 1) require.Equal(t, "image.png", attachments[0].Filename) } func TestAttachmentFilterLogicalOr(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("image.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("document.pdf").MimeType("application/pdf")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("video.mp4").MimeType("video/mp4")) attachments := tc.ListWithFilter(`mime_type == "image/png" || mime_type == "application/pdf"`) require.Len(t, attachments, 2) } func TestAttachmentFilterLogicalNot(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("image.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("document.pdf").MimeType("application/pdf")) attachments := tc.ListWithFilter(`!(mime_type == "image/png")`) require.Len(t, attachments, 1) require.Equal(t, "application/pdf", attachments[0].Type) } func TestAttachmentFilterComplexLogical(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("report.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("report.pdf").MimeType("application/pdf")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("other.png").MimeType("image/png")) attachments := tc.ListWithFilter(`(mime_type == "image/png" || mime_type == "application/pdf") && filename.contains("report")`) require.Len(t, attachments, 2) } // ============================================================================= // Multiple Filters Tests // ============================================================================= func TestAttachmentFilterMultipleFilters(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("report.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("other.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("report.pdf").MimeType("application/pdf")) // Test: Multiple filters (applied as AND) attachments := tc.ListWithFilters(`filename.contains("report")`, `mime_type == "image/png"`) require.Len(t, attachments, 1) require.Contains(t, attachments[0].Filename, "report") require.Equal(t, "image/png", attachments[0].Type) } // ============================================================================= // Edge Cases // ============================================================================= func TestAttachmentFilterNoMatches(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("test.png").MimeType("image/png")) attachments := tc.ListWithFilter(`filename.contains("nonexistent12345")`) require.Len(t, attachments, 0) } func TestAttachmentFilterNullMemoId(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContextWithUser(t) defer tc.Close() memo := tc.CreateMemo("memo-1", "Memo 1") tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("with_memo.png").MimeType("image/png").MemoID(&memo.ID)) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("no_memo.png").MimeType("image/png")) // Test: memo_id == null attachments := tc.ListWithFilter(`memo_id == null`) require.Len(t, attachments, 1) require.Equal(t, "no_memo.png", attachments[0].Filename) require.Nil(t, attachments[0].MemoID) // Test: memo_id != null attachments = tc.ListWithFilter(`memo_id != null`) require.Len(t, attachments, 1) require.Equal(t, "with_memo.png", attachments[0].Filename) require.NotNil(t, attachments[0].MemoID) require.Equal(t, memo.ID, *attachments[0].MemoID) } func TestAttachmentFilterEmptyFilename(t *testing.T) { t.Parallel() tc := NewAttachmentFilterTestContext(t) defer tc.Close() tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("test.png").MimeType("image/png")) tc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename("other.pdf").MimeType("application/pdf")) // Test: filename.contains("") - should match all attachments := tc.ListWithFilter(`filename.contains("")`) require.Len(t, attachments, 2) } ================================================ FILE: store/test/attachment_test.go ================================================ package test import ( "context" "fmt" "testing" "github.com/lithammer/shortuuid/v4" "github.com/stretchr/testify/require" "github.com/usememos/memos/store" ) func TestAttachmentStore(t *testing.T) { t.Parallel() 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() } func TestAttachmentStoreWithFilter(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) _, err := ts.CreateAttachment(ctx, &store.Attachment{ UID: shortuuid.New(), CreatorID: 101, Filename: "test.png", Blob: []byte("test"), Type: "image/png", Size: 1000, }) require.NoError(t, err) _, err = ts.CreateAttachment(ctx, &store.Attachment{ UID: shortuuid.New(), CreatorID: 101, Filename: "test.jpg", Blob: []byte("test"), Type: "image/jpeg", Size: 2000, }) require.NoError(t, err) _, err = ts.CreateAttachment(ctx, &store.Attachment{ UID: shortuuid.New(), CreatorID: 101, Filename: "test.pdf", Blob: []byte("test"), Type: "application/pdf", Size: 3000, }) require.NoError(t, err) attachments, err := ts.ListAttachments(ctx, &store.FindAttachment{ CreatorID: &[]int32{101}[0], Filters: []string{`mime_type == "image/png"`}, }) require.NoError(t, err) require.Len(t, attachments, 1) require.Equal(t, "image/png", attachments[0].Type) attachments, err = ts.ListAttachments(ctx, &store.FindAttachment{ CreatorID: &[]int32{101}[0], Filters: []string{`mime_type in ["image/png", "image/jpeg"]`}, }) require.NoError(t, err) require.Len(t, attachments, 2) attachments, err = ts.ListAttachments(ctx, &store.FindAttachment{ CreatorID: &[]int32{101}[0], Filters: []string{`filename.contains("test")`}, }) require.NoError(t, err) require.Len(t, attachments, 3) ts.Close() } func TestAttachmentUpdate(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) attachment, err := ts.CreateAttachment(ctx, &store.Attachment{ UID: shortuuid.New(), CreatorID: 101, Filename: "original.png", Blob: []byte("test"), Type: "image/png", Size: 1000, }) require.NoError(t, err) // Update filename newFilename := "updated.png" err = ts.UpdateAttachment(ctx, &store.UpdateAttachment{ ID: attachment.ID, Filename: &newFilename, }) require.NoError(t, err) // Verify update found, err := ts.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID}) require.NoError(t, err) require.Equal(t, newFilename, found.Filename) ts.Close() } func TestAttachmentGetByUID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) uid := shortuuid.New() _, err := ts.CreateAttachment(ctx, &store.Attachment{ UID: uid, CreatorID: 101, Filename: "test.png", Blob: []byte("test"), Type: "image/png", Size: 1000, }) require.NoError(t, err) // Get by UID found, err := ts.GetAttachment(ctx, &store.FindAttachment{UID: &uid}) require.NoError(t, err) require.NotNil(t, found) require.Equal(t, uid, found.UID) // Get non-existent UID nonExistentUID := "non-existent-uid" notFound, err := ts.GetAttachment(ctx, &store.FindAttachment{UID: &nonExistentUID}) require.NoError(t, err) require.Nil(t, notFound) ts.Close() } func TestAttachmentListWithPagination(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create 5 attachments for i := 0; i < 5; i++ { _, err := ts.CreateAttachment(ctx, &store.Attachment{ UID: shortuuid.New(), CreatorID: 101, Filename: fmt.Sprintf("test%d.png", i), Blob: []byte("test"), Type: "image/png", Size: int64(1000 + i), }) require.NoError(t, err) } // Test limit limit := 3 attachments, err := ts.ListAttachments(ctx, &store.FindAttachment{ CreatorID: &[]int32{101}[0], Limit: &limit, }) require.NoError(t, err) require.Equal(t, 3, len(attachments)) // Test offset offset := 2 offsetAttachments, err := ts.ListAttachments(ctx, &store.FindAttachment{ CreatorID: &[]int32{101}[0], Limit: &limit, Offset: &offset, }) require.NoError(t, err) require.Equal(t, 3, len(offsetAttachments)) ts.Close() } func TestAttachmentInvalidUID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create with invalid UID (contains spaces) _, err := ts.CreateAttachment(ctx, &store.Attachment{ UID: "invalid uid with spaces", CreatorID: 101, Filename: "test.png", Blob: []byte("test"), Type: "image/png", Size: 1000, }) require.Error(t, err) require.Contains(t, err.Error(), "invalid uid") ts.Close() } ================================================ FILE: store/test/containers.go ================================================ package test import ( "context" "database/sql" "fmt" "os" "strings" "sync" "sync/atomic" "testing" "time" "github.com/docker/docker/api/types/container" "github.com/pkg/errors" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mysql" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/network" "github.com/testcontainers/testcontainers-go/wait" // Database drivers for connection verification. _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" ) const ( testUser = "root" testPassword = "test" // Memos container settings for migration testing. MemosDockerImage = "neosmemo/memos" StableMemosVersion = "stable" // Always points to the latest stable release ) var ( mysqlContainer atomic.Pointer[mysql.MySQLContainer] postgresContainer atomic.Pointer[postgres.PostgresContainer] mysqlOnce sync.Once postgresOnce sync.Once mysqlBaseDSN atomic.Value // stores string postgresBaseDSN atomic.Value // stores string dbCounter atomic.Int64 dbCreationMutex sync.Mutex // Protects database creation operations // Network for container communication. testDockerNetwork atomic.Pointer[testcontainers.DockerNetwork] testNetworkOnce sync.Once ) // getTestNetwork creates or returns the shared Docker network for container communication. func getTestNetwork(ctx context.Context) (*testcontainers.DockerNetwork, error) { var networkErr error testNetworkOnce.Do(func() { nw, err := network.New(ctx, network.WithDriver("bridge")) if err != nil { networkErr = err return } testDockerNetwork.Store(nw) }) return testDockerNetwork.Load(), networkErr } // GetMySQLDSN starts a MySQL container (if not already running) and creates a fresh database for this test. func GetMySQLDSN(t *testing.T) string { ctx := context.Background() mysqlOnce.Do(func() { nw, err := getTestNetwork(ctx) if err != nil { t.Fatalf("failed to create test network: %v", err) } container, err := mysql.Run(ctx, "mysql:8", mysql.WithDatabase("init_db"), mysql.WithUsername("root"), mysql.WithPassword(testPassword), testcontainers.WithEnv(map[string]string{ "MYSQL_ROOT_PASSWORD": testPassword, }), testcontainers.WithWaitStrategy( wait.ForAll( wait.ForLog("ready for connections").WithOccurrence(2), wait.ForListeningPort("3306/tcp"), ).WithDeadline(120*time.Second), ), network.WithNetwork(nil, nw), ) if err != nil { t.Fatalf("failed to start MySQL container: %v", err) } mysqlContainer.Store(container) dsn, err := container.ConnectionString(ctx, "multiStatements=true") if err != nil { t.Fatalf("failed to get MySQL connection string: %v", err) } if err := waitForDB("mysql", dsn, 30*time.Second); err != nil { t.Fatalf("MySQL not ready for connections: %v", err) } mysqlBaseDSN.Store(dsn) }) dsn, ok := mysqlBaseDSN.Load().(string) if !ok || dsn == "" { t.Fatal("MySQL container failed to start in a previous test") } // Serialize database creation to avoid "table already exists" race conditions dbCreationMutex.Lock() defer dbCreationMutex.Unlock() // Create a fresh database for this test dbName := fmt.Sprintf("memos_test_%d", dbCounter.Add(1)) db, err := sql.Open("mysql", dsn) if err != nil { t.Fatalf("failed to connect to MySQL: %v", err) } defer db.Close() if _, err := db.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE `%s`", dbName)); err != nil { t.Fatalf("failed to create database %s: %v", dbName, err) } // Return DSN pointing to the new database return strings.Replace(dsn, "/init_db?", "/"+dbName+"?", 1) } // waitForDB polls the database until it's ready or timeout is reached. func waitForDB(driver, dsn string, timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() var lastErr error for { select { case <-ctx.Done(): if lastErr != nil { return errors.Errorf("timeout waiting for %s database: %v", driver, lastErr) } return errors.Errorf("timeout waiting for %s database to be ready", driver) case <-ticker.C: db, err := sql.Open(driver, dsn) if err != nil { lastErr = err continue } err = db.PingContext(ctx) db.Close() if err == nil { return nil } lastErr = err } } } // GetPostgresDSN starts a PostgreSQL container (if not already running) and creates a fresh database for this test. func GetPostgresDSN(t *testing.T) string { ctx := context.Background() postgresOnce.Do(func() { nw, err := getTestNetwork(ctx) if err != nil { t.Fatalf("failed to create test network: %v", err) } container, err := postgres.Run(ctx, "postgres:18", postgres.WithDatabase("init_db"), postgres.WithUsername(testUser), postgres.WithPassword(testPassword), testcontainers.WithWaitStrategy( wait.ForAll( wait.ForLog("database system is ready to accept connections").WithOccurrence(2), wait.ForListeningPort("5432/tcp"), ).WithDeadline(120*time.Second), ), network.WithNetwork(nil, nw), ) if err != nil { t.Fatalf("failed to start PostgreSQL container: %v", err) } postgresContainer.Store(container) dsn, err := container.ConnectionString(ctx, "sslmode=disable") if err != nil { t.Fatalf("failed to get PostgreSQL connection string: %v", err) } if err := waitForDB("postgres", dsn, 30*time.Second); err != nil { t.Fatalf("PostgreSQL not ready for connections: %v", err) } postgresBaseDSN.Store(dsn) }) dsn, ok := postgresBaseDSN.Load().(string) if !ok || dsn == "" { t.Fatal("PostgreSQL container failed to start in a previous test") } // Serialize database creation to avoid "table already exists" race conditions dbCreationMutex.Lock() defer dbCreationMutex.Unlock() // Create a fresh database for this test dbName := fmt.Sprintf("memos_test_%d", dbCounter.Add(1)) db, err := sql.Open("postgres", dsn) if err != nil { t.Fatalf("failed to connect to PostgreSQL: %v", err) } defer db.Close() if _, err := db.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE %s", dbName)); err != nil { t.Fatalf("failed to create database %s: %v", dbName, err) } // Return DSN pointing to the new database return strings.Replace(dsn, "/init_db?", "/"+dbName+"?", 1) } // TerminateContainers cleans up all running containers and network. // This is typically called from TestMain. func TerminateContainers() { ctx := context.Background() if container := mysqlContainer.Load(); container != nil { _ = container.Terminate(ctx) } if container := postgresContainer.Load(); container != nil { _ = container.Terminate(ctx) } if network := testDockerNetwork.Load(); network != nil { _ = network.Remove(ctx) } } // MemosContainerConfig holds configuration for starting a Memos container. type MemosContainerConfig struct { Version string // Memos version tag (e.g., "0.24.0") Driver string // Database driver: sqlite, mysql, postgres DSN string // Database DSN (for mysql/postgres) DataDir string // Host directory to mount for SQLite data } // MemosStartupWaitStrategy defines the wait strategy for Memos container startup. // Uses regex to match various log message formats across versions. var MemosStartupWaitStrategy = wait.ForAll( wait.ForLog("(started successfully|has been started on port)").AsRegexp(), wait.ForListeningPort("5230/tcp"), ).WithDeadline(180 * time.Second) // StartMemosContainer starts a Memos container for migration testing. // For SQLite, it mounts the dataDir to /var/opt/memos. func StartMemosContainer(ctx context.Context, cfg MemosContainerConfig) (testcontainers.Container, error) { env := map[string]string{ "MEMOS_MODE": "prod", } var opts []testcontainers.ContainerCustomizer switch cfg.Driver { case "sqlite": env["MEMOS_DRIVER"] = "sqlite" opts = append(opts, testcontainers.WithHostConfigModifier(func(hc *container.HostConfig) { hc.Binds = append(hc.Binds, fmt.Sprintf("%s:%s", cfg.DataDir, "/var/opt/memos")) })) default: return nil, errors.Errorf("unsupported driver for migration testing: %s", cfg.Driver) } req := testcontainers.ContainerRequest{ Image: fmt.Sprintf("%s:%s", MemosDockerImage, cfg.Version), Env: env, ExposedPorts: []string{"5230/tcp"}, WaitingFor: MemosStartupWaitStrategy, User: fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), } // Use local image if specified if cfg.Version == "local" { if os.Getenv("MEMOS_TEST_IMAGE_BUILT") == "1" { req.Image = "memos-test:local" } else { req.Image = "" req.FromDockerfile = testcontainers.FromDockerfile{ Context: "../../", Dockerfile: "scripts/Dockerfile", } } } genericReq := testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, } // Apply options for _, opt := range opts { if err := opt.Customize(&genericReq); err != nil { return nil, errors.Wrap(err, "failed to apply container option") } } ctr, err := testcontainers.GenericContainer(ctx, genericReq) if err != nil { return nil, errors.Wrap(err, "failed to start memos container") } return ctr, nil } ================================================ FILE: store/test/filter_helpers_test.go ================================================ package test import ( "context" "strconv" "testing" "github.com/lithammer/shortuuid/v4" "github.com/stretchr/testify/require" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) // ============================================================================= // Formatting Helpers // ============================================================================= func formatInt64(n int64) string { return strconv.FormatInt(n, 10) } func formatInt32(n int32) string { return strconv.FormatInt(int64(n), 10) } func formatInt(n int) string { return strconv.Itoa(n) } // ============================================================================= // Pointer Helpers // ============================================================================= func boolPtr(b bool) *bool { return &b } // ============================================================================= // Test Fixture Builders // ============================================================================= // MemoBuilder provides a fluent API for creating test memos. type MemoBuilder struct { memo *store.Memo } // NewMemoBuilder creates a new memo builder with required fields. func NewMemoBuilder(uid string, creatorID int32) *MemoBuilder { return &MemoBuilder{ memo: &store.Memo{ UID: uid, CreatorID: creatorID, Visibility: store.Public, }, } } func (b *MemoBuilder) Content(content string) *MemoBuilder { b.memo.Content = content return b } func (b *MemoBuilder) Visibility(v store.Visibility) *MemoBuilder { b.memo.Visibility = v return b } func (b *MemoBuilder) Tags(tags ...string) *MemoBuilder { if b.memo.Payload == nil { b.memo.Payload = &storepb.MemoPayload{} } b.memo.Payload.Tags = tags return b } func (b *MemoBuilder) Property(fn func(*storepb.MemoPayload_Property)) *MemoBuilder { if b.memo.Payload == nil { b.memo.Payload = &storepb.MemoPayload{} } if b.memo.Payload.Property == nil { b.memo.Payload.Property = &storepb.MemoPayload_Property{} } fn(b.memo.Payload.Property) return b } func (b *MemoBuilder) Build() *store.Memo { return b.memo } // AttachmentBuilder provides a fluent API for creating test attachments. type AttachmentBuilder struct { attachment *store.Attachment } // NewAttachmentBuilder creates a new attachment builder with required fields. func NewAttachmentBuilder(creatorID int32) *AttachmentBuilder { return &AttachmentBuilder{ attachment: &store.Attachment{ UID: shortuuid.New(), CreatorID: creatorID, Blob: []byte("test"), Size: 1000, }, } } func (b *AttachmentBuilder) Filename(filename string) *AttachmentBuilder { b.attachment.Filename = filename return b } func (b *AttachmentBuilder) MimeType(mimeType string) *AttachmentBuilder { b.attachment.Type = mimeType return b } func (b *AttachmentBuilder) MemoID(memoID *int32) *AttachmentBuilder { b.attachment.MemoID = memoID return b } func (b *AttachmentBuilder) Size(size int64) *AttachmentBuilder { b.attachment.Size = size return b } func (b *AttachmentBuilder) Build() *store.Attachment { return b.attachment } // ============================================================================= // Test Context Helpers // ============================================================================= // MemoFilterTestContext holds common test dependencies for memo filter tests. type MemoFilterTestContext struct { Ctx context.Context T *testing.T Store *store.Store User *store.User } // NewMemoFilterTestContext creates a new test context with store and user. func NewMemoFilterTestContext(t *testing.T) *MemoFilterTestContext { ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) return &MemoFilterTestContext{ Ctx: ctx, T: t, Store: ts, User: user, } } // CreateMemo creates a memo using the builder pattern. func (tc *MemoFilterTestContext) CreateMemo(b *MemoBuilder) *store.Memo { memo, err := tc.Store.CreateMemo(tc.Ctx, b.Build()) require.NoError(tc.T, err) return memo } // PinMemo pins a memo by ID. func (tc *MemoFilterTestContext) PinMemo(memoID int32) { err := tc.Store.UpdateMemo(tc.Ctx, &store.UpdateMemo{ ID: memoID, Pinned: boolPtr(true), }) require.NoError(tc.T, err) } // ListWithFilter lists memos with the given filter and returns the count. func (tc *MemoFilterTestContext) ListWithFilter(filter string) []*store.Memo { memos, err := tc.Store.ListMemos(tc.Ctx, &store.FindMemo{ Filters: []string{filter}, }) require.NoError(tc.T, err) return memos } // ListWithFilters lists memos with multiple filters and returns the count. func (tc *MemoFilterTestContext) ListWithFilters(filters ...string) []*store.Memo { memos, err := tc.Store.ListMemos(tc.Ctx, &store.FindMemo{ Filters: filters, }) require.NoError(tc.T, err) return memos } // Close closes the test store. func (tc *MemoFilterTestContext) Close() { tc.Store.Close() } // AttachmentFilterTestContext holds common test dependencies for attachment filter tests. type AttachmentFilterTestContext struct { Ctx context.Context T *testing.T Store *store.Store User *store.User CreatorID int32 } // NewAttachmentFilterTestContext creates a new test context for attachments. func NewAttachmentFilterTestContext(t *testing.T) *AttachmentFilterTestContext { ctx := context.Background() ts := NewTestingStore(ctx, t) return &AttachmentFilterTestContext{ Ctx: ctx, T: t, Store: ts, CreatorID: 101, } } // NewAttachmentFilterTestContextWithUser creates a new test context with a user. func NewAttachmentFilterTestContextWithUser(t *testing.T) *AttachmentFilterTestContext { ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) return &AttachmentFilterTestContext{ Ctx: ctx, T: t, Store: ts, User: user, CreatorID: user.ID, } } // CreateAttachment creates an attachment using the builder pattern. func (tc *AttachmentFilterTestContext) CreateAttachment(b *AttachmentBuilder) *store.Attachment { attachment, err := tc.Store.CreateAttachment(tc.Ctx, b.Build()) require.NoError(tc.T, err) return attachment } // CreateMemo creates a memo (for attachment tests that need memos). func (tc *AttachmentFilterTestContext) CreateMemo(uid, content string) *store.Memo { memo, err := tc.Store.CreateMemo(tc.Ctx, &store.Memo{ UID: uid, CreatorID: tc.CreatorID, Content: content, Visibility: store.Public, }) require.NoError(tc.T, err) return memo } // ListWithFilter lists attachments with the given filter. func (tc *AttachmentFilterTestContext) ListWithFilter(filter string) []*store.Attachment { attachments, err := tc.Store.ListAttachments(tc.Ctx, &store.FindAttachment{ CreatorID: &tc.CreatorID, Filters: []string{filter}, }) require.NoError(tc.T, err) return attachments } // ListWithFilters lists attachments with multiple filters. func (tc *AttachmentFilterTestContext) ListWithFilters(filters ...string) []*store.Attachment { attachments, err := tc.Store.ListAttachments(tc.Ctx, &store.FindAttachment{ CreatorID: &tc.CreatorID, Filters: filters, }) require.NoError(tc.T, err) return attachments } // Close closes the test store. func (tc *AttachmentFilterTestContext) Close() { tc.Store.Close() } // ============================================================================= // Filter Test Case Definition // ============================================================================= // FilterTestCase defines a single filter test case for table-driven tests. type FilterTestCase struct { Name string Filter string ExpectedCount int } ================================================ FILE: store/test/idp_test.go ================================================ package test 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) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) createdIDP, err := ts.CreateIdentityProvider(ctx, &storepb.IdentityProvider{ Uid: "test-github-oauth", 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) require.Equal(t, "test-github-oauth", createdIDP.Uid) 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() } func TestIdentityProviderGetByID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create IDP idp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("Test IDP", "test-idp")) require.NoError(t, err) // Get by ID found, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id}) require.NoError(t, err) require.NotNil(t, found) require.Equal(t, idp.Id, found.Id) require.Equal(t, idp.Name, found.Name) // Get by UID foundByUID, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{UID: &idp.Uid}) require.NoError(t, err) require.NotNil(t, foundByUID) require.Equal(t, idp.Id, foundByUID.Id) require.Equal(t, idp.Uid, foundByUID.Uid) // Get by non-existent ID nonExistentID := int32(99999) notFound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &nonExistentID}) require.NoError(t, err) require.Nil(t, notFound) ts.Close() } func TestIdentityProviderListMultiple(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create multiple IDPs _, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("GitHub OAuth", "github-oauth")) require.NoError(t, err) _, err = ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("Google OAuth", "google-oauth")) require.NoError(t, err) _, err = ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("GitLab OAuth", "gitlab-oauth")) require.NoError(t, err) // List all idpList, err := ts.ListIdentityProviders(ctx, &store.FindIdentityProvider{}) require.NoError(t, err) require.Len(t, idpList, 3) ts.Close() } func TestIdentityProviderListByID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create multiple IDPs idp1, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("GitHub OAuth", "github-oauth")) require.NoError(t, err) _, err = ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("Google OAuth", "google-oauth")) require.NoError(t, err) // List by specific ID idpList, err := ts.ListIdentityProviders(ctx, &store.FindIdentityProvider{ID: &idp1.Id}) require.NoError(t, err) require.Len(t, idpList, 1) require.Equal(t, "GitHub OAuth", idpList[0].Name) ts.Close() } func TestIdentityProviderUpdateName(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) idp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("Original Name", "original-name")) require.NoError(t, err) require.Equal(t, "Original Name", idp.Name) // Update name newName := "Updated Name" updated, err := ts.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderV1{ ID: idp.Id, Type: storepb.IdentityProvider_OAUTH2, Name: &newName, }) require.NoError(t, err) require.Equal(t, "Updated Name", updated.Name) // Verify update persisted found, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id}) require.NoError(t, err) require.Equal(t, "Updated Name", found.Name) ts.Close() } func TestIdentityProviderUpdateIdentifierFilter(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) idp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("Test IDP", "test-idp")) require.NoError(t, err) require.Equal(t, "", idp.IdentifierFilter) // Update identifier filter newFilter := "@example.com$" updated, err := ts.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderV1{ ID: idp.Id, Type: storepb.IdentityProvider_OAUTH2, IdentifierFilter: &newFilter, }) require.NoError(t, err) require.Equal(t, "@example.com$", updated.IdentifierFilter) // Verify update persisted found, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id}) require.NoError(t, err) require.Equal(t, "@example.com$", found.IdentifierFilter) ts.Close() } func TestIdentityProviderUpdateConfig(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) idp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("Test IDP", "test-idp")) require.NoError(t, err) // Update config newConfig := &storepb.IdentityProviderConfig{ Config: &storepb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &storepb.OAuth2Config{ ClientId: "new_client_id", ClientSecret: "new_client_secret", AuthUrl: "https://newprovider.com/auth", TokenUrl: "https://newprovider.com/token", UserInfoUrl: "https://newprovider.com/user", Scopes: []string{"openid", "profile", "email"}, FieldMapping: &storepb.FieldMapping{ Identifier: "sub", DisplayName: "name", Email: "email", }, }, }, } updated, err := ts.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderV1{ ID: idp.Id, Type: storepb.IdentityProvider_OAUTH2, Config: newConfig, }) require.NoError(t, err) require.Equal(t, "new_client_id", updated.Config.GetOauth2Config().ClientId) require.Equal(t, "new_client_secret", updated.Config.GetOauth2Config().ClientSecret) require.Contains(t, updated.Config.GetOauth2Config().Scopes, "openid") // Verify update persisted found, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id}) require.NoError(t, err) require.Equal(t, "new_client_id", found.Config.GetOauth2Config().ClientId) ts.Close() } func TestIdentityProviderUpdateMultipleFields(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) idp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("Original", "original")) require.NoError(t, err) // Update multiple fields at once newName := "Updated IDP" newFilter := "^admin@" updated, err := ts.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderV1{ ID: idp.Id, Type: storepb.IdentityProvider_OAUTH2, Name: &newName, IdentifierFilter: &newFilter, }) require.NoError(t, err) require.Equal(t, "Updated IDP", updated.Name) require.Equal(t, "^admin@", updated.IdentifierFilter) ts.Close() } func TestIdentityProviderDelete(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) idp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("Test IDP", "test-idp")) require.NoError(t, err) // Delete err = ts.DeleteIdentityProvider(ctx, &store.DeleteIdentityProvider{ID: idp.Id}) require.NoError(t, err) // Verify deletion found, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id}) require.NoError(t, err) require.Nil(t, found) ts.Close() } func TestIdentityProviderDeleteNotAffectOthers(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create multiple IDPs idp1, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("IDP 1", "idp-1")) require.NoError(t, err) idp2, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP("IDP 2", "idp-2")) require.NoError(t, err) // Delete first one err = ts.DeleteIdentityProvider(ctx, &store.DeleteIdentityProvider{ID: idp1.Id}) require.NoError(t, err) // Verify second still exists found, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp2.Id}) require.NoError(t, err) require.NotNil(t, found) require.Equal(t, "IDP 2", found.Name) // Verify list only contains second idpList, err := ts.ListIdentityProviders(ctx, &store.FindIdentityProvider{}) require.NoError(t, err) require.Len(t, idpList, 1) ts.Close() } func TestIdentityProviderOAuth2ConfigScopes(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create IDP with multiple scopes idp, err := ts.CreateIdentityProvider(ctx, &storepb.IdentityProvider{ Uid: "multi-scope-oauth", Name: "Multi-Scope OAuth", Type: storepb.IdentityProvider_OAUTH2, Config: &storepb.IdentityProviderConfig{ Config: &storepb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &storepb.OAuth2Config{ ClientId: "client_id", ClientSecret: "client_secret", AuthUrl: "https://provider.com/auth", TokenUrl: "https://provider.com/token", UserInfoUrl: "https://provider.com/userinfo", Scopes: []string{"openid", "profile", "email", "groups"}, FieldMapping: &storepb.FieldMapping{ Identifier: "sub", DisplayName: "name", Email: "email", }, }, }, }, }) require.NoError(t, err) // Verify scopes are preserved found, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id}) require.NoError(t, err) require.Len(t, found.Config.GetOauth2Config().Scopes, 4) require.Contains(t, found.Config.GetOauth2Config().Scopes, "openid") require.Contains(t, found.Config.GetOauth2Config().Scopes, "groups") ts.Close() } func TestIdentityProviderFieldMapping(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create IDP with custom field mapping idp, err := ts.CreateIdentityProvider(ctx, &storepb.IdentityProvider{ Uid: "custom-field-mapping", Name: "Custom Field Mapping", Type: storepb.IdentityProvider_OAUTH2, Config: &storepb.IdentityProviderConfig{ Config: &storepb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &storepb.OAuth2Config{ ClientId: "client_id", ClientSecret: "client_secret", AuthUrl: "https://provider.com/auth", TokenUrl: "https://provider.com/token", UserInfoUrl: "https://provider.com/userinfo", Scopes: []string{"login"}, FieldMapping: &storepb.FieldMapping{ Identifier: "preferred_username", DisplayName: "full_name", Email: "email_address", }, }, }, }, }) require.NoError(t, err) // Verify field mapping is preserved found, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id}) require.NoError(t, err) require.Equal(t, "preferred_username", found.Config.GetOauth2Config().FieldMapping.Identifier) require.Equal(t, "full_name", found.Config.GetOauth2Config().FieldMapping.DisplayName) require.Equal(t, "email_address", found.Config.GetOauth2Config().FieldMapping.Email) ts.Close() } func TestIdentityProviderIdentifierFilterPatterns(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) testCases := []struct { name string uid string filter string }{ {"Domain filter", "domain-filter", "@company\\.com$"}, {"Prefix filter", "prefix-filter", "^admin_"}, {"Complex regex", "complex-regex", "^[a-z]+@(dept1|dept2)\\.example\\.com$"}, {"Empty filter", "empty-filter", ""}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { idp, err := ts.CreateIdentityProvider(ctx, &storepb.IdentityProvider{ Uid: tc.uid, Name: tc.name, Type: storepb.IdentityProvider_OAUTH2, IdentifierFilter: tc.filter, Config: &storepb.IdentityProviderConfig{ Config: &storepb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &storepb.OAuth2Config{ ClientId: "client_id", ClientSecret: "client_secret", AuthUrl: "https://provider.com/auth", TokenUrl: "https://provider.com/token", UserInfoUrl: "https://provider.com/userinfo", Scopes: []string{"login"}, FieldMapping: &storepb.FieldMapping{ Identifier: "sub", }, }, }, }, }) require.NoError(t, err) found, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id}) require.NoError(t, err) require.Equal(t, tc.filter, found.IdentifierFilter) // Cleanup err = ts.DeleteIdentityProvider(ctx, &store.DeleteIdentityProvider{ID: idp.Id}) require.NoError(t, err) }) } ts.Close() } // Helper function to create a test OAuth2 IDP. func createTestOAuth2IDP(name, uid string) *storepb.IdentityProvider { return &storepb.IdentityProvider{ Uid: uid, Name: name, Type: storepb.IdentityProvider_OAUTH2, IdentifierFilter: "", Config: &storepb.IdentityProviderConfig{ Config: &storepb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &storepb.OAuth2Config{ ClientId: "client_id", ClientSecret: "client_secret", AuthUrl: "https://provider.com/auth", TokenUrl: "https://provider.com/token", UserInfoUrl: "https://provider.com/userinfo", Scopes: []string{"login"}, FieldMapping: &storepb.FieldMapping{ Identifier: "login", DisplayName: "name", Email: "email", }, }, }, }, } } ================================================ FILE: store/test/inbox_test.go ================================================ package test 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) { t.Parallel() 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() } func TestInboxListByID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) inbox, err := ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) // List by ID inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ID: &inbox.ID}) require.NoError(t, err) require.Len(t, inboxes, 1) require.Equal(t, inbox.ID, inboxes[0].ID) // List by non-existent ID nonExistentID := int32(99999) inboxes, err = ts.ListInboxes(ctx, &store.FindInbox{ID: &nonExistentID}) require.NoError(t, err) require.Len(t, inboxes, 0) ts.Close() } func TestInboxListBySenderID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user1, err := createTestingHostUser(ctx, ts) require.NoError(t, err) user2, err := createTestingUserWithRole(ctx, ts, "user2", store.RoleUser) require.NoError(t, err) // Create inbox from system bot (senderID = 0) _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user1.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) // Create inbox from user2 _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: user2.ID, ReceiverID: user1.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) // List by sender ID = user2 inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{SenderID: &user2.ID}) require.NoError(t, err) require.Len(t, inboxes, 1) require.Equal(t, user2.ID, inboxes[0].SenderID) // List by sender ID = 0 (system bot) systemBotID := int32(0) inboxes, err = ts.ListInboxes(ctx, &store.FindInbox{SenderID: &systemBotID}) require.NoError(t, err) require.Len(t, inboxes, 1) require.Equal(t, int32(0), inboxes[0].SenderID) ts.Close() } func TestInboxListByStatus(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create UNREAD inbox _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) // Create another inbox and archive it inbox2, err := ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) _, err = ts.UpdateInbox(ctx, &store.UpdateInbox{ID: inbox2.ID, Status: store.ARCHIVED}) require.NoError(t, err) // List by UNREAD status unreadStatus := store.UNREAD inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{Status: &unreadStatus}) require.NoError(t, err) require.Len(t, inboxes, 1) require.Equal(t, store.UNREAD, inboxes[0].Status) // List by ARCHIVED status archivedStatus := store.ARCHIVED inboxes, err = ts.ListInboxes(ctx, &store.FindInbox{Status: &archivedStatus}) require.NoError(t, err) require.Len(t, inboxes, 1) require.Equal(t, store.ARCHIVED, inboxes[0].Status) ts.Close() } func TestInboxListByMessageType(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create MEMO_COMMENT inboxes _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) // List by MEMO_COMMENT type memoCommentType := storepb.InboxMessage_MEMO_COMMENT inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{MessageType: &memoCommentType}) require.NoError(t, err) require.Len(t, inboxes, 2) for _, inbox := range inboxes { require.Equal(t, storepb.InboxMessage_MEMO_COMMENT, inbox.Message.Type) } ts.Close() } func TestInboxListPagination(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create 5 inboxes for i := 0; i < 5; i++ { _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) } // Test Limit only limit := 3 inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user.ID, Limit: &limit, }) require.NoError(t, err) require.Len(t, inboxes, 3) // Test Limit + Offset (offset requires limit in the implementation) limit = 2 offset := 2 inboxes, err = ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user.ID, Limit: &limit, Offset: &offset, }) require.NoError(t, err) require.Len(t, inboxes, 2) // Test Limit + Offset skipping to end limit = 10 offset = 3 inboxes, err = ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user.ID, Limit: &limit, Offset: &offset, }) require.NoError(t, err) require.Len(t, inboxes, 2) // Only 2 remaining after offset of 3 ts.Close() } func TestInboxListCombinedFilters(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user1, err := createTestingHostUser(ctx, ts) require.NoError(t, err) user2, err := createTestingUserWithRole(ctx, ts, "user2", store.RoleUser) require.NoError(t, err) // Create various inboxes // user2 -> user1, MEMO_COMMENT, UNREAD _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: user2.ID, ReceiverID: user1.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) // user2 -> user1, TYPE_UNSPECIFIED, UNREAD _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: user2.ID, ReceiverID: user1.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_TYPE_UNSPECIFIED}, }) require.NoError(t, err) // system -> user1, MEMO_COMMENT, ARCHIVED inbox3, err := ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user1.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) _, err = ts.UpdateInbox(ctx, &store.UpdateInbox{ID: inbox3.ID, Status: store.ARCHIVED}) require.NoError(t, err) // Combined filter: ReceiverID + SenderID + Status unreadStatus := store.UNREAD inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user1.ID, SenderID: &user2.ID, Status: &unreadStatus, }) require.NoError(t, err) require.Len(t, inboxes, 2) // Combined filter: ReceiverID + MessageType + Status memoCommentType := storepb.InboxMessage_MEMO_COMMENT inboxes, err = ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user1.ID, MessageType: &memoCommentType, Status: &unreadStatus, }) require.NoError(t, err) require.Len(t, inboxes, 1) require.Equal(t, user2.ID, inboxes[0].SenderID) ts.Close() } func TestInboxMessagePayload(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create inbox with message payload containing memo references. memoID := int32(123) relatedMemoID := int32(456) inbox, err := ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{ Type: storepb.InboxMessage_MEMO_COMMENT, Payload: &storepb.InboxMessage_MemoComment{ MemoComment: &storepb.InboxMessage_MemoCommentPayload{ MemoId: memoID, RelatedMemoId: relatedMemoID, }, }, }, }) require.NoError(t, err) require.NotNil(t, inbox.Message) require.Equal(t, storepb.InboxMessage_MEMO_COMMENT, inbox.Message.Type) require.NotNil(t, inbox.Message.GetMemoComment()) require.Equal(t, memoID, inbox.Message.GetMemoComment().MemoId) require.Equal(t, relatedMemoID, inbox.Message.GetMemoComment().RelatedMemoId) // List and verify payload is preserved inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ReceiverID: &user.ID}) require.NoError(t, err) require.Len(t, inboxes, 1) require.NotNil(t, inboxes[0].Message.GetMemoComment()) require.Equal(t, memoID, inboxes[0].Message.GetMemoComment().MemoId) require.Equal(t, relatedMemoID, inboxes[0].Message.GetMemoComment().RelatedMemoId) ts.Close() } func TestInboxUpdateStatus(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) inbox, err := ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) require.Equal(t, store.UNREAD, inbox.Status) // Update to ARCHIVED updated, err := ts.UpdateInbox(ctx, &store.UpdateInbox{ ID: inbox.ID, Status: store.ARCHIVED, }) require.NoError(t, err) require.Equal(t, store.ARCHIVED, updated.Status) require.Equal(t, inbox.ID, updated.ID) // Verify the update persisted inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ID: &inbox.ID}) require.NoError(t, err) require.Len(t, inboxes, 1) require.Equal(t, store.ARCHIVED, inboxes[0].Status) ts.Close() } func TestInboxListByMessageTypeMultipleTypes(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create inboxes with different message types _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_TYPE_UNSPECIFIED}, }) require.NoError(t, err) _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) // Filter by MEMO_COMMENT - should get 2 memoCommentType := storepb.InboxMessage_MEMO_COMMENT inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user.ID, MessageType: &memoCommentType, }) require.NoError(t, err) require.Len(t, inboxes, 2) for _, inbox := range inboxes { require.Equal(t, storepb.InboxMessage_MEMO_COMMENT, inbox.Message.Type) } // Filter by TYPE_UNSPECIFIED - should get 1 unspecifiedType := storepb.InboxMessage_TYPE_UNSPECIFIED inboxes, err = ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user.ID, MessageType: &unspecifiedType, }) require.NoError(t, err) require.Len(t, inboxes, 1) require.Equal(t, storepb.InboxMessage_TYPE_UNSPECIFIED, inboxes[0].Message.Type) ts.Close() } func TestInboxMessageTypeFilterWithPayload(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create inbox with full payload. memoID := int32(456) _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{ Type: storepb.InboxMessage_MEMO_COMMENT, Payload: &storepb.InboxMessage_MemoComment{ MemoComment: &storepb.InboxMessage_MemoCommentPayload{ MemoId: memoID, RelatedMemoId: 654, }, }, }, }) require.NoError(t, err) // Create inbox with different type but also has payload. otherMemoID := int32(789) _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{ Type: storepb.InboxMessage_TYPE_UNSPECIFIED, Payload: &storepb.InboxMessage_MemoComment{ MemoComment: &storepb.InboxMessage_MemoCommentPayload{ MemoId: otherMemoID, RelatedMemoId: 987, }, }, }, }) require.NoError(t, err) // Filter by type should work correctly even with complex JSON payload. memoCommentType := storepb.InboxMessage_MEMO_COMMENT inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user.ID, MessageType: &memoCommentType, }) require.NoError(t, err) require.Len(t, inboxes, 1) require.NotNil(t, inboxes[0].Message.GetMemoComment()) require.Equal(t, memoID, inboxes[0].Message.GetMemoComment().MemoId) ts.Close() } func TestInboxMessageTypeFilterWithStatusAndPagination(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create multiple inboxes with various combinations for i := 0; i < 5; i++ { _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) } // Archive 2 of them allInboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ReceiverID: &user.ID}) require.NoError(t, err) for i := 0; i < 2; i++ { _, err = ts.UpdateInbox(ctx, &store.UpdateInbox{ID: allInboxes[i].ID, Status: store.ARCHIVED}) require.NoError(t, err) } // Filter by type + status + pagination memoCommentType := storepb.InboxMessage_MEMO_COMMENT unreadStatus := store.UNREAD limit := 2 inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user.ID, MessageType: &memoCommentType, Status: &unreadStatus, Limit: &limit, }) require.NoError(t, err) require.Len(t, inboxes, 2) for _, inbox := range inboxes { require.Equal(t, storepb.InboxMessage_MEMO_COMMENT, inbox.Message.Type) require.Equal(t, store.UNREAD, inbox.Status) } // Get next page offset := 2 inboxes, err = ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user.ID, MessageType: &memoCommentType, Status: &unreadStatus, Limit: &limit, Offset: &offset, }) require.NoError(t, err) require.Len(t, inboxes, 1) // Only 1 remaining (3 unread total, got 2, now 1 left) ts.Close() } func TestInboxMultipleReceivers(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user1, err := createTestingHostUser(ctx, ts) require.NoError(t, err) user2, err := createTestingUserWithRole(ctx, ts, "user2", store.RoleUser) require.NoError(t, err) // Create inbox for user1 _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user1.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) // Create inbox for user2 _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user2.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT}, }) require.NoError(t, err) // User1 should only see their inbox inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ReceiverID: &user1.ID}) require.NoError(t, err) require.Len(t, inboxes, 1) require.Equal(t, user1.ID, inboxes[0].ReceiverID) // User2 should only see their inbox inboxes, err = ts.ListInboxes(ctx, &store.FindInbox{ReceiverID: &user2.ID}) require.NoError(t, err) require.Len(t, inboxes, 1) require.Equal(t, user2.ID, inboxes[0].ReceiverID) ts.Close() } ================================================ FILE: store/test/instance_setting_test.go ================================================ package test import ( "context" "testing" "github.com/stretchr/testify/require" colorpb "google.golang.org/genproto/googleapis/type/color" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) func TestInstanceSettingV1Store(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) instanceSetting, err := ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_GENERAL, Value: &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: &storepb.InstanceGeneralSetting{ AdditionalScript: "", }, }, }) require.NoError(t, err) setting, err := ts.GetInstanceSetting(ctx, &store.FindInstanceSetting{ Name: storepb.InstanceSettingKey_GENERAL.String(), }) require.NoError(t, err) require.Equal(t, instanceSetting, setting) ts.Close() } func TestInstanceSettingGetNonExistent(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Get non-existent setting setting, err := ts.GetInstanceSetting(ctx, &store.FindInstanceSetting{ Name: storepb.InstanceSettingKey_STORAGE.String(), }) require.NoError(t, err) require.Nil(t, setting) ts.Close() } func TestInstanceSettingUpsertUpdate(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create setting _, err := ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_GENERAL, Value: &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: &storepb.InstanceGeneralSetting{ AdditionalScript: "console.log('v1')", }, }, }) require.NoError(t, err) // Update setting _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_GENERAL, Value: &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: &storepb.InstanceGeneralSetting{ AdditionalScript: "console.log('v2')", }, }, }) require.NoError(t, err) // Verify update setting, err := ts.GetInstanceSetting(ctx, &store.FindInstanceSetting{ Name: storepb.InstanceSettingKey_GENERAL.String(), }) require.NoError(t, err) require.Equal(t, "console.log('v2')", setting.GetGeneralSetting().AdditionalScript) // Verify only one setting exists list, err := ts.ListInstanceSettings(ctx, &store.FindInstanceSetting{ Name: storepb.InstanceSettingKey_GENERAL.String(), }) require.NoError(t, err) require.Equal(t, 1, len(list)) ts.Close() } func TestInstanceSettingBasicSetting(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Get default basic setting (should return empty defaults) basicSetting, err := ts.GetInstanceBasicSetting(ctx) require.NoError(t, err) require.NotNil(t, basicSetting) // Set basic setting _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_BASIC, Value: &storepb.InstanceSetting_BasicSetting{ BasicSetting: &storepb.InstanceBasicSetting{ SecretKey: "my-secret-key", }, }, }) require.NoError(t, err) // Verify basicSetting, err = ts.GetInstanceBasicSetting(ctx) require.NoError(t, err) require.Equal(t, "my-secret-key", basicSetting.SecretKey) ts.Close() } func TestInstanceSettingGeneralSetting(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Get default general setting generalSetting, err := ts.GetInstanceGeneralSetting(ctx) require.NoError(t, err) require.NotNil(t, generalSetting) // Set general setting _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_GENERAL, Value: &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: &storepb.InstanceGeneralSetting{ AdditionalScript: "console.log('test')", AdditionalStyle: "body { color: red; }", }, }, }) require.NoError(t, err) // Verify generalSetting, err = ts.GetInstanceGeneralSetting(ctx) require.NoError(t, err) require.Equal(t, "console.log('test')", generalSetting.AdditionalScript) require.Equal(t, "body { color: red; }", generalSetting.AdditionalStyle) ts.Close() } func TestInstanceSettingMemoRelatedSetting(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Get default memo related setting (should have defaults) memoSetting, err := ts.GetInstanceMemoRelatedSetting(ctx) require.NoError(t, err) require.NotNil(t, memoSetting) require.GreaterOrEqual(t, memoSetting.ContentLengthLimit, int32(store.DefaultContentLengthLimit)) require.NotEmpty(t, memoSetting.Reactions) // Set custom memo related setting customReactions := []string{"👍", "👎", "🚀"} _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_MEMO_RELATED, Value: &storepb.InstanceSetting_MemoRelatedSetting{ MemoRelatedSetting: &storepb.InstanceMemoRelatedSetting{ ContentLengthLimit: 16384, Reactions: customReactions, }, }, }) require.NoError(t, err) // Verify memoSetting, err = ts.GetInstanceMemoRelatedSetting(ctx) require.NoError(t, err) require.Equal(t, int32(16384), memoSetting.ContentLengthLimit) require.Equal(t, customReactions, memoSetting.Reactions) ts.Close() } func TestInstanceSettingStorageSetting(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Get default storage setting (should have defaults) storageSetting, err := ts.GetInstanceStorageSetting(ctx) require.NoError(t, err) require.NotNil(t, storageSetting) require.Equal(t, storepb.InstanceStorageSetting_LOCAL, storageSetting.StorageType) require.Equal(t, int64(30), storageSetting.UploadSizeLimitMb) require.Equal(t, "assets/{timestamp}_{filename}", storageSetting.FilepathTemplate) // Set custom storage setting _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_STORAGE, Value: &storepb.InstanceSetting_StorageSetting{ StorageSetting: &storepb.InstanceStorageSetting{ StorageType: storepb.InstanceStorageSetting_LOCAL, UploadSizeLimitMb: 100, FilepathTemplate: "uploads/{date}/{filename}", }, }, }) require.NoError(t, err) // Verify storageSetting, err = ts.GetInstanceStorageSetting(ctx) require.NoError(t, err) require.Equal(t, storepb.InstanceStorageSetting_LOCAL, storageSetting.StorageType) require.Equal(t, int64(100), storageSetting.UploadSizeLimitMb) require.Equal(t, "uploads/{date}/{filename}", storageSetting.FilepathTemplate) ts.Close() } func TestInstanceSettingTagsSetting(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) tagsSetting, err := ts.GetInstanceTagsSetting(ctx) require.NoError(t, err) require.NotNil(t, tagsSetting) require.Empty(t, tagsSetting.Tags) _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_TAGS, Value: &storepb.InstanceSetting_TagsSetting{ TagsSetting: &storepb.InstanceTagsSetting{ Tags: map[string]*storepb.InstanceTagMetadata{ "bug": { BackgroundColor: &colorpb.Color{ Red: 0.9, Green: 0.1, Blue: 0.1, }, }, }, }, }, }) require.NoError(t, err) tagsSetting, err = ts.GetInstanceTagsSetting(ctx) require.NoError(t, err) require.Contains(t, tagsSetting.Tags, "bug") require.InDelta(t, 0.9, tagsSetting.Tags["bug"].GetBackgroundColor().GetRed(), 0.0001) ts.Close() } func TestInstanceSettingNotificationSetting(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) notificationSetting, err := ts.GetInstanceNotificationSetting(ctx) require.NoError(t, err) require.NotNil(t, notificationSetting) require.NotNil(t, notificationSetting.Email) require.False(t, notificationSetting.Email.Enabled) _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_NOTIFICATION, Value: &storepb.InstanceSetting_NotificationSetting{ NotificationSetting: &storepb.InstanceNotificationSetting{ Email: &storepb.InstanceNotificationSetting_EmailSetting{ Enabled: true, SmtpHost: "smtp.example.com", SmtpPort: 587, SmtpUsername: "bot@example.com", SmtpPassword: "secret", FromEmail: "bot@example.com", FromName: "Memos Bot", ReplyTo: "support@example.com", UseTls: true, }, }, }, }) require.NoError(t, err) notificationSetting, err = ts.GetInstanceNotificationSetting(ctx) require.NoError(t, err) require.True(t, notificationSetting.Email.Enabled) require.Equal(t, "smtp.example.com", notificationSetting.Email.SmtpHost) require.Equal(t, int32(587), notificationSetting.Email.SmtpPort) require.Equal(t, "bot@example.com", notificationSetting.Email.FromEmail) ts.Close() } func TestInstanceSettingListAll(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Count initial settings initialList, err := ts.ListInstanceSettings(ctx, &store.FindInstanceSetting{}) require.NoError(t, err) initialCount := len(initialList) // Create multiple settings _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_GENERAL, Value: &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: &storepb.InstanceGeneralSetting{}, }, }) require.NoError(t, err) _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_STORAGE, Value: &storepb.InstanceSetting_StorageSetting{ StorageSetting: &storepb.InstanceStorageSetting{}, }, }) require.NoError(t, err) _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_NOTIFICATION, Value: &storepb.InstanceSetting_NotificationSetting{ NotificationSetting: &storepb.InstanceNotificationSetting{}, }, }) require.NoError(t, err) // List all - should have 3 more than initial list, err := ts.ListInstanceSettings(ctx, &store.FindInstanceSetting{}) require.NoError(t, err) require.Equal(t, initialCount+3, len(list)) ts.Close() } func TestInstanceSettingEdgeCases(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Case 1: General Setting with special characters and Unicode specialScript := `` specialStyle := `body { font-family: "Noto Sans SC", sans-serif; content: "\u2764"; }` _, err := ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_GENERAL, Value: &storepb.InstanceSetting_GeneralSetting{ GeneralSetting: &storepb.InstanceGeneralSetting{ AdditionalScript: specialScript, AdditionalStyle: specialStyle, }, }, }) require.NoError(t, err) generalSetting, err := ts.GetInstanceGeneralSetting(ctx) require.NoError(t, err) require.Equal(t, specialScript, generalSetting.AdditionalScript) require.Equal(t, specialStyle, generalSetting.AdditionalStyle) // Case 2: Memo Related Setting with Unicode reactions unicodeReactions := []string{"🐱", "🐶", "🦊", "🦄"} _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_MEMO_RELATED, Value: &storepb.InstanceSetting_MemoRelatedSetting{ MemoRelatedSetting: &storepb.InstanceMemoRelatedSetting{ ContentLengthLimit: 1000, Reactions: unicodeReactions, }, }, }) require.NoError(t, err) memoSetting, err := ts.GetInstanceMemoRelatedSetting(ctx) require.NoError(t, err) require.Equal(t, unicodeReactions, memoSetting.Reactions) ts.Close() } ================================================ FILE: store/test/main_test.go ================================================ package test import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "testing" ) func TestMain(m *testing.M) { // If DRIVER is set, run tests for that driver only if os.Getenv("DRIVER") != "" { defer TerminateContainers() m.Run() //nolint:revive // Exit code is handled by test runner return } // No DRIVER set - run tests for all drivers sequentially runAllDrivers() } func runAllDrivers() { drivers := []string{"sqlite", "mysql", "postgres"} _, currentFile, _, _ := runtime.Caller(0) projectRoot := filepath.Dir(filepath.Dir(filepath.Dir(currentFile))) var failed []string for _, driver := range drivers { fmt.Printf("\n==================== %s ====================\n\n", driver) cmd := exec.Command("go", "test", "-v", "-count=1", "./store/test/...") cmd.Dir = projectRoot cmd.Env = append(os.Environ(), "DRIVER="+driver) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { failed = append(failed, driver) } } fmt.Println() if len(failed) > 0 { fmt.Printf("FAIL: %v\n", failed) panic("some drivers failed") } fmt.Println("PASS: all drivers") } ================================================ FILE: store/test/memo_filter_test.go ================================================ package test import ( "testing" "time" "github.com/stretchr/testify/require" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) // ============================================================================= // Content Field Tests // Schema: content (string, supports contains) // ============================================================================= func TestMemoFilterContentContains(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() // Create memos with different content tc.CreateMemo(NewMemoBuilder("memo-hello", tc.User.ID).Content("Hello world")) tc.CreateMemo(NewMemoBuilder("memo-goodbye", tc.User.ID).Content("Goodbye world")) tc.CreateMemo(NewMemoBuilder("memo-test", tc.User.ID).Content("Testing content")) // Test: content.contains("Hello") - single match memos := tc.ListWithFilter(`content.contains("Hello")`) require.Len(t, memos, 1) require.Contains(t, memos[0].Content, "Hello") // Test: content.contains("world") - multiple matches memos = tc.ListWithFilter(`content.contains("world")`) require.Len(t, memos, 2) // Test: content.contains("nonexistent") - no matches memos = tc.ListWithFilter(`content.contains("nonexistent")`) require.Len(t, memos, 0) } func TestMemoFilterContentSpecialCharacters(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-special", tc.User.ID).Content("Special chars: @#$%^&*()")) memos := tc.ListWithFilter(`content.contains("@#$%")`) require.Len(t, memos, 1) } func TestMemoFilterContentUnicode(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-unicode", tc.User.ID).Content("Unicode test: 你好世界 🌍")) memos := tc.ListWithFilter(`content.contains("你好")`) require.Len(t, memos, 1) } func TestMemoFilterContentUnicodeCaseFold(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-unicode-case", tc.User.ID).Content("Привет Мир")) memos := tc.ListWithFilter(`content.contains("привет")`) require.Len(t, memos, 1) } func TestMemoFilterContentCaseSensitivity(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-case", tc.User.ID).Content("MixedCase Content")) // Exact match memos := tc.ListWithFilter(`content.contains("MixedCase")`) require.Len(t, memos, 1) // Lowercase match (depends on DB collation, usually case-insensitive in default installs but good to verify behavior) // SQLite default LIKE is case-insensitive for ASCII. memosLower := tc.ListWithFilter(`content.contains("mixedcase")`) // We just verify it doesn't crash; strict case sensitivity expectation depends on DB config. // For standard Memos setup (SQLite), it's often case-insensitive. // Let's check if we get a result or not to characterize current behavior. if len(memosLower) > 0 { require.Equal(t, "MixedCase Content", memosLower[0].Content) } } // ============================================================================= // Visibility Field Tests // Schema: visibility (string, ==, !=) // ============================================================================= func TestMemoFilterVisibilityEquals(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-public", tc.User.ID).Content("Public memo").Visibility(store.Public)) tc.CreateMemo(NewMemoBuilder("memo-private", tc.User.ID).Content("Private memo").Visibility(store.Private)) tc.CreateMemo(NewMemoBuilder("memo-protected", tc.User.ID).Content("Protected memo").Visibility(store.Protected)) // Test: visibility == "PUBLIC" memos := tc.ListWithFilter(`visibility == "PUBLIC"`) require.Len(t, memos, 1) require.Equal(t, store.Public, memos[0].Visibility) // Test: visibility == "PRIVATE" memos = tc.ListWithFilter(`visibility == "PRIVATE"`) require.Len(t, memos, 1) require.Equal(t, store.Private, memos[0].Visibility) } func TestMemoFilterVisibilityNotEquals(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-public", tc.User.ID).Content("Public memo").Visibility(store.Public)) tc.CreateMemo(NewMemoBuilder("memo-private", tc.User.ID).Content("Private memo").Visibility(store.Private)) memos := tc.ListWithFilter(`visibility != "PUBLIC"`) require.Len(t, memos, 1) require.Equal(t, store.Private, memos[0].Visibility) } func TestMemoFilterVisibilityInList(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-pub", tc.User.ID).Visibility(store.Public)) tc.CreateMemo(NewMemoBuilder("memo-priv", tc.User.ID).Visibility(store.Private)) tc.CreateMemo(NewMemoBuilder("memo-prot", tc.User.ID).Visibility(store.Protected)) memos := tc.ListWithFilter(`visibility in ["PUBLIC", "PRIVATE"]`) require.Len(t, memos, 2) } // ============================================================================= // Pinned Field Tests // Schema: pinned (bool column, ==, !=, predicate) // ============================================================================= func TestMemoFilterPinnedEquals(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() pinnedMemo := tc.CreateMemo(NewMemoBuilder("memo-pinned", tc.User.ID).Content("Pinned memo")) tc.PinMemo(pinnedMemo.ID) tc.CreateMemo(NewMemoBuilder("memo-unpinned", tc.User.ID).Content("Unpinned memo")) // Test: pinned == true memos := tc.ListWithFilter(`pinned == true`) require.Len(t, memos, 1) require.True(t, memos[0].Pinned) // Test: pinned == false memos = tc.ListWithFilter(`pinned == false`) require.Len(t, memos, 1) require.False(t, memos[0].Pinned) } func TestMemoFilterPinnedPredicate(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() pinnedMemo := tc.CreateMemo(NewMemoBuilder("memo-pinned", tc.User.ID).Content("Pinned memo")) tc.PinMemo(pinnedMemo.ID) tc.CreateMemo(NewMemoBuilder("memo-unpinned", tc.User.ID).Content("Unpinned memo")) memos := tc.ListWithFilter(`pinned`) require.Len(t, memos, 1) require.True(t, memos[0].Pinned) } // ============================================================================= // Creator ID Field Tests // Schema: creator_id (int, ==, !=) // ============================================================================= func TestMemoFilterCreatorIdEquals(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() user2, err := tc.Store.CreateUser(tc.Ctx, &store.User{ Username: "user2", Role: store.RoleUser, Email: "user2@example.com", Nickname: "User 2", }) require.NoError(t, err) tc.CreateMemo(NewMemoBuilder("memo-user1", tc.User.ID).Content("User 1 memo")) tc.CreateMemo(NewMemoBuilder("memo-user2", user2.ID).Content("User 2 memo")) memos := tc.ListWithFilter(`creator_id == ` + formatInt(int(tc.User.ID))) require.Len(t, memos, 1) require.Equal(t, tc.User.ID, memos[0].CreatorID) } func TestMemoFilterCreatorIdNotEquals(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() user2, err := tc.Store.CreateUser(tc.Ctx, &store.User{ Username: "user2", Role: store.RoleUser, Email: "user2@example.com", Nickname: "User 2", }) require.NoError(t, err) tc.CreateMemo(NewMemoBuilder("memo-user1", tc.User.ID).Content("User 1 memo")) tc.CreateMemo(NewMemoBuilder("memo-user2", user2.ID).Content("User 2 memo")) memos := tc.ListWithFilter(`creator_id != ` + formatInt(int(tc.User.ID))) require.Len(t, memos, 1) require.Equal(t, user2.ID, memos[0].CreatorID) } // ============================================================================= // Tags Field Tests // Schema: tags (JSON list), tag (virtual alias) // Operators: tag in [...], "value" in tags // ============================================================================= func TestMemoFilterTagInList(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-work", tc.User.ID).Content("Work memo").Tags("work", "important")) tc.CreateMemo(NewMemoBuilder("memo-personal", tc.User.ID).Content("Personal memo").Tags("personal", "fun")) tc.CreateMemo(NewMemoBuilder("memo-no-tags", tc.User.ID).Content("No tags")) // Test: tag in ["work"] memos := tc.ListWithFilter(`tag in ["work"]`) require.Len(t, memos, 1) require.Contains(t, memos[0].Payload.Tags, "work") // Test: tag in ["work", "personal"] memos = tc.ListWithFilter(`tag in ["work", "personal"]`) require.Len(t, memos, 2) } func TestMemoFilterElementInTags(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-tagged", tc.User.ID).Content("Tagged memo").Tags("project", "todo")) tc.CreateMemo(NewMemoBuilder("memo-untagged", tc.User.ID).Content("Untagged memo")) // Test: "project" in tags memos := tc.ListWithFilter(`"project" in tags`) require.Len(t, memos, 1) // Test: "nonexistent" in tags memos = tc.ListWithFilter(`"nonexistent" in tags`) require.Len(t, memos, 0) } func TestMemoFilterHierarchicalTags(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-book", tc.User.ID).Content("Book memo").Tags("book")) tc.CreateMemo(NewMemoBuilder("memo-book-fiction", tc.User.ID).Content("Fiction book memo").Tags("book/fiction")) // Test: tag in ["book"] should match both (hierarchical matching) memos := tc.ListWithFilter(`tag in ["book"]`) require.Len(t, memos, 2) } func TestMemoFilterEmptyTags(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-empty-tags", tc.User.ID).Content("Empty tags").Tags()) memos := tc.ListWithFilter(`tag in ["anything"]`) require.Len(t, memos, 0) } // ============================================================================= // JSON Bool Field Tests // Schema: has_task_list, has_link, has_code, has_incomplete_tasks // Operators: ==, !=, predicate // ============================================================================= func TestMemoFilterHasTaskList(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-with-tasks", tc.User.ID). Content("- [ ] Task 1\n- [x] Task 2"). Property(func(p *storepb.MemoPayload_Property) { p.HasTaskList = true })) tc.CreateMemo(NewMemoBuilder("memo-no-tasks", tc.User.ID).Content("No tasks here")) // Test: has_task_list (predicate) memos := tc.ListWithFilter(`has_task_list`) require.Len(t, memos, 1) require.True(t, memos[0].Payload.Property.HasTaskList) // Test: has_task_list == true memos = tc.ListWithFilter(`has_task_list == true`) require.Len(t, memos, 1) // Note: has_task_list == false is not tested because JSON boolean fields // with false value may not be queryable when the field is not present in JSON } func TestMemoFilterHasLink(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-with-link", tc.User.ID). Content("Check out https://example.com"). Property(func(p *storepb.MemoPayload_Property) { p.HasLink = true })) tc.CreateMemo(NewMemoBuilder("memo-no-link", tc.User.ID).Content("No links")) memos := tc.ListWithFilter(`has_link`) require.Len(t, memos, 1) require.True(t, memos[0].Payload.Property.HasLink) } func TestMemoFilterHasCode(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-with-code", tc.User.ID). Content("```go\nfmt.Println(\"Hello\")\n```"). Property(func(p *storepb.MemoPayload_Property) { p.HasCode = true })) tc.CreateMemo(NewMemoBuilder("memo-no-code", tc.User.ID).Content("No code")) memos := tc.ListWithFilter(`has_code`) require.Len(t, memos, 1) require.True(t, memos[0].Payload.Property.HasCode) } func TestMemoFilterHasIncompleteTasks(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-incomplete", tc.User.ID). Content("- [ ] Incomplete task"). Property(func(p *storepb.MemoPayload_Property) { p.HasTaskList = true p.HasIncompleteTasks = true })) tc.CreateMemo(NewMemoBuilder("memo-complete", tc.User.ID). Content("- [x] Complete task"). Property(func(p *storepb.MemoPayload_Property) { p.HasTaskList = true p.HasIncompleteTasks = false })) memos := tc.ListWithFilter(`has_incomplete_tasks`) require.Len(t, memos, 1) require.True(t, memos[0].Payload.Property.HasIncompleteTasks) } func TestMemoFilterCombinedJSONBool(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() // Memo with all properties tc.CreateMemo(NewMemoBuilder("memo-all-props", tc.User.ID). Content("All properties"). Property(func(p *storepb.MemoPayload_Property) { p.HasLink = true p.HasTaskList = true p.HasCode = true p.HasIncompleteTasks = true })) // Memo with only link tc.CreateMemo(NewMemoBuilder("memo-only-link", tc.User.ID). Content("Only link"). Property(func(p *storepb.MemoPayload_Property) { p.HasLink = true })) // Test: has_link && has_code memos := tc.ListWithFilter(`has_link && has_code`) require.Len(t, memos, 1) // Test: has_task_list && has_incomplete_tasks memos = tc.ListWithFilter(`has_task_list && has_incomplete_tasks`) require.Len(t, memos, 1) // Test: has_link || has_code memos = tc.ListWithFilter(`has_link || has_code`) require.Len(t, memos, 2) } // ============================================================================= // Timestamp Field Tests // Schema: created_ts, updated_ts (timestamp, all comparison operators) // Functions: now(), arithmetic (+, -, *) // ============================================================================= func TestMemoFilterCreatedTsComparison(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() now := time.Now().Unix() tc.CreateMemo(NewMemoBuilder("memo-ts", tc.User.ID).Content("Timestamp test")) // Test: created_ts < future (should match) memos := tc.ListWithFilter(`created_ts < ` + formatInt64(now+3600)) require.Len(t, memos, 1) // Test: created_ts > past (should match) memos = tc.ListWithFilter(`created_ts > ` + formatInt64(now-3600)) require.Len(t, memos, 1) // Test: created_ts > future (should not match) memos = tc.ListWithFilter(`created_ts > ` + formatInt64(now+3600)) require.Len(t, memos, 0) } func TestMemoFilterCreatedTsWithNow(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-ts-test", tc.User.ID).Content("Timestamp test")) // Test: created_ts < now() + 5 (buffer for container clock drift) memos := tc.ListWithFilter(`created_ts < now() + 5`) require.Len(t, memos, 1) // Test: created_ts > now() + 5 (should not match) memos = tc.ListWithFilter(`created_ts > now() + 5`) require.Len(t, memos, 0) } func TestMemoFilterCreatedTsArithmetic(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-ts-arith", tc.User.ID).Content("Timestamp arithmetic test")) // Test: created_ts >= now() - 3600 (memos created in last hour) memos := tc.ListWithFilter(`created_ts >= now() - 3600`) require.Len(t, memos, 1) // Test: created_ts < now() - 86400 (memos older than 1 day - should be empty) memos = tc.ListWithFilter(`created_ts < now() - 86400`) require.Len(t, memos, 0) // Test: Multiplication - created_ts >= now() - 60 * 60 memos = tc.ListWithFilter(`created_ts >= now() - 60 * 60`) require.Len(t, memos, 1) } func TestMemoFilterUpdatedTs(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() memo := tc.CreateMemo(NewMemoBuilder("memo-updated", tc.User.ID).Content("Will be updated")) // Update the memo newContent := "Updated content" err := tc.Store.UpdateMemo(tc.Ctx, &store.UpdateMemo{ ID: memo.ID, Content: &newContent, }) require.NoError(t, err) // Test: updated_ts >= now() - 60 (updated in last minute) memos := tc.ListWithFilter(`updated_ts >= now() - 60`) require.Len(t, memos, 1) // Test: updated_ts > now() + 3600 (should be empty) memos = tc.ListWithFilter(`updated_ts > now() + 3600`) require.Len(t, memos, 0) } func TestMemoFilterAllComparisonOperators(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-ops", tc.User.ID).Content("Comparison operators test")) // Test: < (less than) memos := tc.ListWithFilter(`created_ts < now() + 3600`) require.Len(t, memos, 1) // Test: <= (less than or equal) with buffer for clock drift memos = tc.ListWithFilter(`created_ts < now() + 5`) require.Len(t, memos, 1) // Test: > (greater than) memos = tc.ListWithFilter(`created_ts > now() - 3600`) require.Len(t, memos, 1) // Test: >= (greater than or equal) memos = tc.ListWithFilter(`created_ts >= now() - 60`) require.Len(t, memos, 1) } // ============================================================================= // Logical Operator Tests // Operators: && (AND), || (OR), ! (NOT) // ============================================================================= func TestMemoFilterLogicalAnd(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() pinnedMemo := tc.CreateMemo(NewMemoBuilder("memo-pinned-public", tc.User.ID).Content("Pinned public")) tc.PinMemo(pinnedMemo.ID) tc.CreateMemo(NewMemoBuilder("memo-unpinned-public", tc.User.ID).Content("Unpinned public")) memos := tc.ListWithFilter(`pinned && visibility == "PUBLIC"`) require.Len(t, memos, 1) require.True(t, memos[0].Pinned) } func TestMemoFilterLogicalOr(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-public", tc.User.ID).Visibility(store.Public)) tc.CreateMemo(NewMemoBuilder("memo-private", tc.User.ID).Visibility(store.Private)) tc.CreateMemo(NewMemoBuilder("memo-protected", tc.User.ID).Visibility(store.Protected)) memos := tc.ListWithFilter(`visibility == "PUBLIC" || visibility == "PRIVATE"`) require.Len(t, memos, 2) } func TestMemoFilterLogicalNot(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() pinnedMemo := tc.CreateMemo(NewMemoBuilder("memo-pinned", tc.User.ID).Content("Pinned")) tc.PinMemo(pinnedMemo.ID) tc.CreateMemo(NewMemoBuilder("memo-unpinned", tc.User.ID).Content("Unpinned")) memos := tc.ListWithFilter(`!pinned`) require.Len(t, memos, 1) require.False(t, memos[0].Pinned) } func TestMemoFilterNegatedComparison(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-public", tc.User.ID).Visibility(store.Public)) tc.CreateMemo(NewMemoBuilder("memo-private", tc.User.ID).Visibility(store.Private)) memos := tc.ListWithFilter(`!(visibility == "PUBLIC")`) require.Len(t, memos, 1) require.Equal(t, store.Private, memos[0].Visibility) } func TestMemoFilterComplexLogical(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() // Create pinned public memo with tags pinnedMemo := tc.CreateMemo(NewMemoBuilder("memo-pinned-tagged", tc.User.ID). Content("Pinned and tagged").Tags("important")) tc.PinMemo(pinnedMemo.ID) // Create unpinned memo with same tag tc.CreateMemo(NewMemoBuilder("memo-unpinned-tagged", tc.User.ID). Content("Unpinned but tagged").Tags("important")) // Create pinned memo without tag pinned2 := tc.CreateMemo(NewMemoBuilder("memo-pinned-untagged", tc.User.ID).Content("Pinned but untagged")) tc.PinMemo(pinned2.ID) // Test: pinned && tag in ["important"] memos := tc.ListWithFilter(`pinned && tag in ["important"]`) require.Len(t, memos, 1) // Test: (pinned || tag in ["important"]) && visibility == "PUBLIC" memos = tc.ListWithFilter(`(pinned || tag in ["important"]) && visibility == "PUBLIC"`) require.Len(t, memos, 3) // Test: De Morgan's Law ! (A || B) == !A && !B // ! (pinned || has_task_list) tc.CreateMemo(NewMemoBuilder("memo-no-props", tc.User.ID).Content("Nothing special")) memos = tc.ListWithFilter(`!(pinned || has_task_list)`) require.Len(t, memos, 2) // Unpinned-tagged + Nothing special (pinned-untagged is pinned) } // ============================================================================= // Tag Comprehension Tests (exists macro) // Schema: tags (list of strings, supports exists/all macros with predicates) // ============================================================================= func TestMemoFilterTagsExistsStartsWith(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() // Create memos with different tags tc.CreateMemo(NewMemoBuilder("memo-archive1", tc.User.ID). Content("Archived project memo"). Tags("archive/project", "done")) tc.CreateMemo(NewMemoBuilder("memo-archive2", tc.User.ID). Content("Archived work memo"). Tags("archive/work", "old")) tc.CreateMemo(NewMemoBuilder("memo-active", tc.User.ID). Content("Active project memo"). Tags("project/active", "todo")) tc.CreateMemo(NewMemoBuilder("memo-homelab", tc.User.ID). Content("Homelab memo"). Tags("homelab/memos", "tech")) // Test: tags.exists(t, t.startsWith("archive")) - should match archived memos memos := tc.ListWithFilter(`tags.exists(t, t.startsWith("archive"))`) require.Len(t, memos, 2, "Should find 2 archived memos") for _, memo := range memos { hasArchiveTag := false for _, tag := range memo.Payload.Tags { if len(tag) >= 7 && tag[:7] == "archive" { hasArchiveTag = true break } } require.True(t, hasArchiveTag, "Memo should have tag starting with 'archive'") } // Test: !tags.exists(t, t.startsWith("archive")) - should match non-archived memos memos = tc.ListWithFilter(`!tags.exists(t, t.startsWith("archive"))`) require.Len(t, memos, 2, "Should find 2 non-archived memos") // Test: tags.exists(t, t.startsWith("project")) - should match project memos memos = tc.ListWithFilter(`tags.exists(t, t.startsWith("project"))`) require.Len(t, memos, 1, "Should find 1 project memo") // Test: tags.exists(t, t.startsWith("homelab")) - should match homelab memos memos = tc.ListWithFilter(`tags.exists(t, t.startsWith("homelab"))`) require.Len(t, memos, 1, "Should find 1 homelab memo") // Test: tags.exists(t, t.startsWith("nonexistent")) - should match nothing memos = tc.ListWithFilter(`tags.exists(t, t.startsWith("nonexistent"))`) require.Len(t, memos, 0, "Should find no memos") } func TestMemoFilterTagsExistsContains(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() // Create memos with different tags tc.CreateMemo(NewMemoBuilder("memo-todo1", tc.User.ID). Content("Todo task 1"). Tags("project/todo", "urgent")) tc.CreateMemo(NewMemoBuilder("memo-todo2", tc.User.ID). Content("Todo task 2"). Tags("work/todo-list", "pending")) tc.CreateMemo(NewMemoBuilder("memo-done", tc.User.ID). Content("Done task"). Tags("project/completed", "done")) // Test: tags.exists(t, t.contains("todo")) - should match todos memos := tc.ListWithFilter(`tags.exists(t, t.contains("todo"))`) require.Len(t, memos, 2, "Should find 2 todo memos") // Test: tags.exists(t, t.contains("done")) - should match done memos = tc.ListWithFilter(`tags.exists(t, t.contains("done"))`) require.Len(t, memos, 1, "Should find 1 done memo") // Test: !tags.exists(t, t.contains("todo")) - should exclude todos memos = tc.ListWithFilter(`!tags.exists(t, t.contains("todo"))`) require.Len(t, memos, 1, "Should find 1 non-todo memo") } func TestMemoFilterTagsExistsEndsWith(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() // Create memos with different tag endings tc.CreateMemo(NewMemoBuilder("memo-bug", tc.User.ID). Content("Bug report"). Tags("project/bug", "critical")) tc.CreateMemo(NewMemoBuilder("memo-debug", tc.User.ID). Content("Debug session"). Tags("work/debug", "dev")) tc.CreateMemo(NewMemoBuilder("memo-feature", tc.User.ID). Content("New feature"). Tags("project/feature", "new")) // Test: tags.exists(t, t.endsWith("bug")) - should match bug-related tags memos := tc.ListWithFilter(`tags.exists(t, t.endsWith("bug"))`) require.Len(t, memos, 2, "Should find 2 bug-related memos") // Test: tags.exists(t, t.endsWith("feature")) - should match feature memos = tc.ListWithFilter(`tags.exists(t, t.endsWith("feature"))`) require.Len(t, memos, 1, "Should find 1 feature memo") // Test: !tags.exists(t, t.endsWith("bug")) - should exclude bug-related memos = tc.ListWithFilter(`!tags.exists(t, t.endsWith("bug"))`) require.Len(t, memos, 1, "Should find 1 non-bug memo") } func TestMemoFilterTagsExistsCombinedWithOtherFilters(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() // Create memos with tags and other properties tc.CreateMemo(NewMemoBuilder("memo-archived-old", tc.User.ID). Content("Old archived memo"). Tags("archive/old", "done")) tc.CreateMemo(NewMemoBuilder("memo-archived-recent", tc.User.ID). Content("Recent archived memo with TODO"). Tags("archive/recent", "done")) tc.CreateMemo(NewMemoBuilder("memo-active-todo", tc.User.ID). Content("Active TODO"). Tags("project/active", "todo")) // Test: Combine tag filter with content filter memos := tc.ListWithFilter(`tags.exists(t, t.startsWith("archive")) && content.contains("TODO")`) require.Len(t, memos, 1, "Should find 1 archived memo with TODO in content") // Test: OR condition with tag filters memos = tc.ListWithFilter(`tags.exists(t, t.startsWith("archive")) || tags.exists(t, t.contains("todo"))`) require.Len(t, memos, 3, "Should find all memos (archived or with todo tag)") // Test: Complex filter - archived but not containing "Recent" memos = tc.ListWithFilter(`tags.exists(t, t.startsWith("archive")) && !content.contains("Recent")`) require.Len(t, memos, 1, "Should find 1 old archived memo") } func TestMemoFilterTagsExistsEmptyAndNullCases(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() // Create memo with no tags tc.CreateMemo(NewMemoBuilder("memo-no-tags", tc.User.ID). Content("Memo without tags")) // Create memo with tags tc.CreateMemo(NewMemoBuilder("memo-with-tags", tc.User.ID). Content("Memo with tags"). Tags("tag1", "tag2")) // Test: tags.exists should not match memos without tags memos := tc.ListWithFilter(`tags.exists(t, t.startsWith("tag"))`) require.Len(t, memos, 1, "Should only find memo with tags") // Test: Negation should match memos without matching tags memos = tc.ListWithFilter(`!tags.exists(t, t.startsWith("tag"))`) require.Len(t, memos, 1, "Should find memo without matching tags") } func TestMemoFilterIssue5480_ArchiveWorkflow(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() // Create a realistic scenario as described in issue #5480 // User has hierarchical tags and archives memos by prefixing with "archive" // Active memos tc.CreateMemo(NewMemoBuilder("memo-homelab", tc.User.ID). Content("Setting up Memos"). Tags("homelab/memos", "tech")) tc.CreateMemo(NewMemoBuilder("memo-project-alpha", tc.User.ID). Content("Project Alpha notes"). Tags("work/project-alpha", "active")) // Archived memos (user prefixed tags with "archive") tc.CreateMemo(NewMemoBuilder("memo-old-homelab", tc.User.ID). Content("Old homelab setup"). Tags("archive/homelab/old-server", "done")) tc.CreateMemo(NewMemoBuilder("memo-old-project", tc.User.ID). Content("Old project beta"). Tags("archive/work/project-beta", "completed")) tc.CreateMemo(NewMemoBuilder("memo-archived-personal", tc.User.ID). Content("Archived personal note"). Tags("archive/personal/2024", "old")) // Test: Filter out ALL archived memos using startsWith memos := tc.ListWithFilter(`!tags.exists(t, t.startsWith("archive"))`) require.Len(t, memos, 2, "Should only show active memos (not archived)") for _, memo := range memos { for _, tag := range memo.Payload.Tags { require.NotContains(t, tag, "archive", "Active memos should not have archive prefix") } } // Test: Show ONLY archived memos memos = tc.ListWithFilter(`tags.exists(t, t.startsWith("archive"))`) require.Len(t, memos, 3, "Should find all archived memos") for _, memo := range memos { hasArchiveTag := false for _, tag := range memo.Payload.Tags { if len(tag) >= 7 && tag[:7] == "archive" { hasArchiveTag = true break } } require.True(t, hasArchiveTag, "All returned memos should have archive prefix") } // Test: Filter archived homelab memos specifically memos = tc.ListWithFilter(`tags.exists(t, t.startsWith("archive/homelab"))`) require.Len(t, memos, 1, "Should find only archived homelab memos") } // ============================================================================= // Multiple Filters Tests // ============================================================================= func TestMemoFilterMultipleFilters(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-public-hello", tc.User.ID).Content("Hello world").Visibility(store.Public)) tc.CreateMemo(NewMemoBuilder("memo-private-hello", tc.User.ID).Content("Hello private").Visibility(store.Private)) // Test: Multiple filters (applied as AND) memos := tc.ListWithFilters(`content.contains("Hello")`, `visibility == "PUBLIC"`) require.Len(t, memos, 1) require.Contains(t, memos[0].Content, "Hello") require.Equal(t, store.Public, memos[0].Visibility) } // ============================================================================= // Edge Cases // ============================================================================= func TestMemoFilterNullPayload(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-null-payload", tc.User.ID).Content("Null payload")) // Test: has_link should not crash and return no results memos := tc.ListWithFilter(`has_link`) require.Len(t, memos, 0) } func TestMemoFilterNoMatches(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() tc.CreateMemo(NewMemoBuilder("memo-test", tc.User.ID).Content("Test content")) memos := tc.ListWithFilter(`content.contains("nonexistent12345")`) require.Len(t, memos, 0) } func TestMemoFilterJSONBooleanLogic(t *testing.T) { t.Parallel() tc := NewMemoFilterTestContext(t) defer tc.Close() // 1. Memo with task list (true) and NO link (null) tc.CreateMemo(NewMemoBuilder("memo-task-only", tc.User.ID). Content("Task only"). Property(func(p *storepb.MemoPayload_Property) { p.HasTaskList = true })) // 2. Memo with link (true) and NO task list (null) tc.CreateMemo(NewMemoBuilder("memo-link-only", tc.User.ID). Content("Link only"). Property(func(p *storepb.MemoPayload_Property) { p.HasLink = true })) // 3. Memo with both (true) tc.CreateMemo(NewMemoBuilder("memo-both", tc.User.ID). Content("Both"). Property(func(p *storepb.MemoPayload_Property) { p.HasTaskList = true p.HasLink = true })) // 4. Memo with neither (null) tc.CreateMemo(NewMemoBuilder("memo-neither", tc.User.ID).Content("Neither")) // Test A: has_task_list || has_link // Expected: 3 memos (task-only, link-only, both). Neither should be excluded. // This specifically tests the NULL handling in OR logic (NULL || TRUE should be TRUE) memos := tc.ListWithFilter(`has_task_list || has_link`) require.Len(t, memos, 3, "Should find 3 memos with OR logic") // Test B: !has_task_list // Expected: 2 memos (link-only, neither). Memos where has_task_list is NULL or FALSE. // Note: If NULL is not handled, !NULL is still NULL (false-y in WHERE), so "neither" might be missed depending on logic. // In our implementation, we want missing fields to behave as false. memos = tc.ListWithFilter(`!has_task_list`) require.Len(t, memos, 2, "Should find 2 memos where task list is false or missing") // Test C: has_task_list && !has_link // Expected: 1 memo (task-only). memos = tc.ListWithFilter(`has_task_list && !has_link`) require.Len(t, memos, 1, "Should find 1 memo (task only)") } ================================================ FILE: store/test/memo_relation_test.go ================================================ package test import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/usememos/memos/store" ) func TestMemoRelationStore(t *testing.T) { t.Parallel() 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() } func TestMemoRelationListByMemoID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create main memo mainMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "main-memo", CreatorID: user.ID, Content: "main memo content", Visibility: store.Public, }) require.NoError(t, err) // Create related memos relatedMemo1, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo-1", CreatorID: user.ID, Content: "related memo 1 content", Visibility: store.Public, }) require.NoError(t, err) relatedMemo2, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo-2", CreatorID: user.ID, Content: "related memo 2 content", Visibility: store.Public, }) require.NoError(t, err) // Create relations _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo1.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo2.ID, Type: store.MemoRelationComment, }) require.NoError(t, err) // List by memo ID relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &mainMemo.ID, }) require.NoError(t, err) require.Equal(t, 2, len(relations)) // List by type refType := store.MemoRelationReference refRelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &mainMemo.ID, Type: &refType, }) require.NoError(t, err) require.Equal(t, 1, len(refRelations)) require.Equal(t, store.MemoRelationReference, refRelations[0].Type) // List by related memo ID relations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{ RelatedMemoID: &relatedMemo1.ID, }) require.NoError(t, err) require.Equal(t, 1, len(relations)) ts.Close() } func TestMemoRelationDelete(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create memos mainMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "main-memo", CreatorID: user.ID, Content: "main memo content", Visibility: store.Public, }) require.NoError(t, err) relatedMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo", CreatorID: user.ID, Content: "related memo content", Visibility: store.Public, }) require.NoError(t, err) // Create relation _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // Verify relation exists relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &mainMemo.ID, }) require.NoError(t, err) require.Equal(t, 1, len(relations)) // Delete relation by memo ID relType := store.MemoRelationReference err = ts.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{ MemoID: &mainMemo.ID, RelatedMemoID: &relatedMemo.ID, Type: &relType, }) require.NoError(t, err) // Verify relation is deleted relations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &mainMemo.ID, }) require.NoError(t, err) require.Equal(t, 0, len(relations)) ts.Close() } func TestMemoRelationDifferentTypes(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) mainMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "main-memo", CreatorID: user.ID, Content: "main memo content", Visibility: store.Public, }) require.NoError(t, err) relatedMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo", CreatorID: user.ID, Content: "related memo content", Visibility: store.Public, }) require.NoError(t, err) // Create reference relation _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // Create comment relation (same memos, different type - should be allowed) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo.ID, Type: store.MemoRelationComment, }) require.NoError(t, err) // Verify both relations exist relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &mainMemo.ID, }) require.NoError(t, err) require.Equal(t, 2, len(relations)) ts.Close() } func TestMemoRelationUpsertSameRelation(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) mainMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "main-memo", CreatorID: user.ID, Content: "main memo content", Visibility: store.Public, }) require.NoError(t, err) relatedMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo", CreatorID: user.ID, Content: "related memo content", Visibility: store.Public, }) require.NoError(t, err) // Create relation _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // Upsert the same relation again (should not create duplicate) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // Verify only one relation exists relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &mainMemo.ID, }) require.NoError(t, err) require.Len(t, relations, 1) ts.Close() } func TestMemoRelationDeleteByType(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) mainMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "main-memo", CreatorID: user.ID, Content: "main memo content", Visibility: store.Public, }) require.NoError(t, err) relatedMemo1, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo-1", CreatorID: user.ID, Content: "related memo 1 content", Visibility: store.Public, }) require.NoError(t, err) relatedMemo2, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo-2", CreatorID: user.ID, Content: "related memo 2 content", Visibility: store.Public, }) require.NoError(t, err) // Create reference relations _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo1.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // Create comment relation _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo2.ID, Type: store.MemoRelationComment, }) require.NoError(t, err) // Delete only reference type relations refType := store.MemoRelationReference err = ts.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{ MemoID: &mainMemo.ID, Type: &refType, }) require.NoError(t, err) // Verify only comment relation remains relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &mainMemo.ID, }) require.NoError(t, err) require.Len(t, relations, 1) require.Equal(t, store.MemoRelationComment, relations[0].Type) ts.Close() } func TestMemoRelationDeleteByMemoID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) memo1, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-1", CreatorID: user.ID, Content: "memo 1 content", Visibility: store.Public, }) require.NoError(t, err) memo2, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-2", CreatorID: user.ID, Content: "memo 2 content", Visibility: store.Public, }) require.NoError(t, err) relatedMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo", CreatorID: user.ID, Content: "related memo content", Visibility: store.Public, }) require.NoError(t, err) // Create relations for both memos _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memo1.ID, RelatedMemoID: relatedMemo.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memo2.ID, RelatedMemoID: relatedMemo.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // Delete all relations for memo1 err = ts.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{ MemoID: &memo1.ID, }) require.NoError(t, err) // Verify memo1's relations are gone relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &memo1.ID, }) require.NoError(t, err) require.Len(t, relations, 0) // Verify memo2's relations still exist relations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &memo2.ID, }) require.NoError(t, err) require.Len(t, relations, 1) ts.Close() } func TestMemoRelationListByRelatedMemoID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create a memo that will be referenced by others targetMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "target-memo", CreatorID: user.ID, Content: "target memo content", Visibility: store.Public, }) require.NoError(t, err) // Create memos that reference the target referrer1, err := ts.CreateMemo(ctx, &store.Memo{ UID: "referrer-1", CreatorID: user.ID, Content: "referrer 1 content", Visibility: store.Public, }) require.NoError(t, err) referrer2, err := ts.CreateMemo(ctx, &store.Memo{ UID: "referrer-2", CreatorID: user.ID, Content: "referrer 2 content", Visibility: store.Public, }) require.NoError(t, err) // Create relations pointing to target _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: referrer1.ID, RelatedMemoID: targetMemo.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: referrer2.ID, RelatedMemoID: targetMemo.ID, Type: store.MemoRelationComment, }) require.NoError(t, err) // List by related memo ID (find all memos that reference the target) relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ RelatedMemoID: &targetMemo.ID, }) require.NoError(t, err) require.Len(t, relations, 2) ts.Close() } func TestMemoRelationListCombinedFilters(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) mainMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "main-memo", CreatorID: user.ID, Content: "main memo content", Visibility: store.Public, }) require.NoError(t, err) relatedMemo1, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo-1", CreatorID: user.ID, Content: "related memo 1 content", Visibility: store.Public, }) require.NoError(t, err) relatedMemo2, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo-2", CreatorID: user.ID, Content: "related memo 2 content", Visibility: store.Public, }) require.NoError(t, err) // Create multiple relations _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo1.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo2.ID, Type: store.MemoRelationComment, }) require.NoError(t, err) // List with MemoID and Type filter refType := store.MemoRelationReference relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &mainMemo.ID, Type: &refType, }) require.NoError(t, err) require.Len(t, relations, 1) require.Equal(t, relatedMemo1.ID, relations[0].RelatedMemoID) // List with MemoID, RelatedMemoID, and Type filter commentType := store.MemoRelationComment relations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &mainMemo.ID, RelatedMemoID: &relatedMemo2.ID, Type: &commentType, }) require.NoError(t, err) require.Len(t, relations, 1) ts.Close() } func TestMemoRelationListEmpty(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) memo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-no-relations", CreatorID: user.ID, Content: "memo with no relations", Visibility: store.Public, }) require.NoError(t, err) // List relations for memo with none relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &memo.ID, }) require.NoError(t, err) require.Len(t, relations, 0) ts.Close() } func TestMemoRelationBidirectional(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) memoA, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-a", CreatorID: user.ID, Content: "memo A content", Visibility: store.Public, }) require.NoError(t, err) memoB, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-b", CreatorID: user.ID, Content: "memo B content", Visibility: store.Public, }) require.NoError(t, err) // Create relation A -> B _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memoA.ID, RelatedMemoID: memoB.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // Create relation B -> A (reverse direction) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memoB.ID, RelatedMemoID: memoA.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // Verify A -> B exists relationsFromA, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &memoA.ID, }) require.NoError(t, err) require.Len(t, relationsFromA, 1) require.Equal(t, memoB.ID, relationsFromA[0].RelatedMemoID) // Verify B -> A exists relationsFromB, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &memoB.ID, }) require.NoError(t, err) require.Len(t, relationsFromB, 1) require.Equal(t, memoA.ID, relationsFromB[0].RelatedMemoID) ts.Close() } func TestMemoRelationListByMemoIDList(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create 3 memos. memoA, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-a", CreatorID: user.ID, Content: "memo A content", Visibility: store.Public, }) require.NoError(t, err) memoB, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-b", CreatorID: user.ID, Content: "memo B content", Visibility: store.Public, }) require.NoError(t, err) memoC, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-c", CreatorID: user.ID, Content: "memo C content", Visibility: store.Public, }) require.NoError(t, err) memoD, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-d", CreatorID: user.ID, Content: "memo D content", Visibility: store.Public, }) require.NoError(t, err) // A -> B (reference) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memoA.ID, RelatedMemoID: memoB.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // A -> C (comment) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memoA.ID, RelatedMemoID: memoC.ID, Type: store.MemoRelationComment, }) require.NoError(t, err) // D -> B (reference) — B appears as related_memo_id _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memoD.ID, RelatedMemoID: memoB.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // Batch query for memos A and B: should return all 3 relations // (A->B because A is in list, A->C because A is in list, D->B because B is in list) relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoIDList: []int32{memoA.ID, memoB.ID}, }) require.NoError(t, err) require.Len(t, relations, 3) // Batch query for memo C only: should return 1 relation (A->C because C is related_memo_id) relations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoIDList: []int32{memoC.ID}, }) require.NoError(t, err) require.Len(t, relations, 1) require.Equal(t, memoA.ID, relations[0].MemoID) require.Equal(t, memoC.ID, relations[0].RelatedMemoID) // Batch query for memo D only: should return 1 relation (D->B because D is memo_id) relations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoIDList: []int32{memoD.ID}, }) require.NoError(t, err) require.Len(t, relations, 1) require.Equal(t, memoD.ID, relations[0].MemoID) require.Equal(t, memoB.ID, relations[0].RelatedMemoID) ts.Close() } func TestMemoRelationListByMemoIDListEmpty(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) memo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-no-relations", CreatorID: user.ID, Content: "memo with no relations", Visibility: store.Public, }) require.NoError(t, err) // Batch query with a memo that has no relations. relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoIDList: []int32{memo.ID}, }) require.NoError(t, err) require.Len(t, relations, 0) // Empty MemoIDList should not filter by MemoIDList (returns based on other filters). relations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoIDList: []int32{}, }) require.NoError(t, err) require.Len(t, relations, 0) ts.Close() } func TestMemoRelationListByMemoIDListWithTypeFilter(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) memoA, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-a", CreatorID: user.ID, Content: "memo A content", Visibility: store.Public, }) require.NoError(t, err) memoB, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-b", CreatorID: user.ID, Content: "memo B content", Visibility: store.Public, }) require.NoError(t, err) memoC, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-c", CreatorID: user.ID, Content: "memo C content", Visibility: store.Public, }) require.NoError(t, err) // A -> B (reference) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memoA.ID, RelatedMemoID: memoB.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // A -> C (comment) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memoA.ID, RelatedMemoID: memoC.ID, Type: store.MemoRelationComment, }) require.NoError(t, err) // Batch query with type filter: only references refType := store.MemoRelationReference relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoIDList: []int32{memoA.ID}, Type: &refType, }) require.NoError(t, err) require.Len(t, relations, 1) require.Equal(t, store.MemoRelationReference, relations[0].Type) // Batch query with type filter: only comments commentType := store.MemoRelationComment relations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoIDList: []int32{memoA.ID}, Type: &commentType, }) require.NoError(t, err) require.Len(t, relations, 1) require.Equal(t, store.MemoRelationComment, relations[0].Type) ts.Close() } func TestMemoRelationListByMemoIDListBothDirections(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) memoA, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-a", CreatorID: user.ID, Content: "memo A content", Visibility: store.Public, }) require.NoError(t, err) memoB, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-b", CreatorID: user.ID, Content: "memo B content", Visibility: store.Public, }) require.NoError(t, err) memoX, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-x", CreatorID: user.ID, Content: "memo X content", Visibility: store.Public, }) require.NoError(t, err) // X -> A (A appears as related_memo_id) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memoX.ID, RelatedMemoID: memoA.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // A -> B (A appears as memo_id) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: memoA.ID, RelatedMemoID: memoB.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) // Query with MemoIDList=[A]: should find both relations (A as source and A as target). relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoIDList: []int32{memoA.ID}, }) require.NoError(t, err) require.Len(t, relations, 2) // Verify we got both directions. memoIDs := map[int32]bool{} relatedIDs := map[int32]bool{} for _, r := range relations { memoIDs[r.MemoID] = true relatedIDs[r.RelatedMemoID] = true } require.True(t, memoIDs[memoX.ID], "should include X->A relation") require.True(t, memoIDs[memoA.ID], "should include A->B relation") require.True(t, relatedIDs[memoA.ID], "should include X->A relation") require.True(t, relatedIDs[memoB.ID], "should include A->B relation") ts.Close() } func TestMemoRelationMultipleRelationsToSameMemo(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) mainMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "main-memo", CreatorID: user.ID, Content: "main memo content", Visibility: store.Public, }) require.NoError(t, err) // Create multiple memos that all relate to the main memo for i := 1; i <= 5; i++ { relatedMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "related-memo-" + string(rune('0'+i)), CreatorID: user.ID, Content: "related memo content", Visibility: store.Public, }) require.NoError(t, err) _, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{ MemoID: mainMemo.ID, RelatedMemoID: relatedMemo.ID, Type: store.MemoRelationReference, }) require.NoError(t, err) } // Verify all 5 relations exist relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{ MemoID: &mainMemo.ID, }) require.NoError(t, err) require.Len(t, relations, 5) ts.Close() } ================================================ FILE: store/test/memo_test.go ================================================ package test import ( "context" "fmt" "testing" "github.com/stretchr/testify/require" "github.com/usememos/memos/store" storepb "github.com/usememos/memos/proto/gen/store" ) func TestMemoStore(t *testing.T) { t.Parallel() 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) { t.Parallel() 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{ Filters: []string{"tag in [\"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) { t.Parallel() 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() } func TestMemoGetByID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) memo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "test-memo-1", CreatorID: user.ID, Content: "test content", Visibility: store.Public, }) require.NoError(t, err) // Get by ID found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID}) require.NoError(t, err) require.NotNil(t, found) require.Equal(t, memo.ID, found.ID) require.Equal(t, memo.Content, found.Content) // Get non-existent nonExistentID := int32(99999) notFound, err := ts.GetMemo(ctx, &store.FindMemo{ID: &nonExistentID}) require.NoError(t, err) require.Nil(t, notFound) ts.Close() } func TestMemoGetByUID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) uid := "unique-memo-uid" memo, err := ts.CreateMemo(ctx, &store.Memo{ UID: uid, CreatorID: user.ID, Content: "test content", Visibility: store.Public, }) require.NoError(t, err) // Get by UID found, err := ts.GetMemo(ctx, &store.FindMemo{UID: &uid}) require.NoError(t, err) require.NotNil(t, found) require.Equal(t, memo.UID, found.UID) // Get non-existent UID nonExistentUID := "non-existent-uid" notFound, err := ts.GetMemo(ctx, &store.FindMemo{UID: &nonExistentUID}) require.NoError(t, err) require.Nil(t, notFound) ts.Close() } func TestMemoListByVisibility(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create memos with different visibilities _, err = ts.CreateMemo(ctx, &store.Memo{ UID: "public-memo", CreatorID: user.ID, Content: "public content", Visibility: store.Public, }) require.NoError(t, err) _, err = ts.CreateMemo(ctx, &store.Memo{ UID: "protected-memo", CreatorID: user.ID, Content: "protected content", Visibility: store.Protected, }) require.NoError(t, err) _, err = ts.CreateMemo(ctx, &store.Memo{ UID: "private-memo", CreatorID: user.ID, Content: "private content", Visibility: store.Private, }) require.NoError(t, err) // List public memos only publicMemos, err := ts.ListMemos(ctx, &store.FindMemo{ VisibilityList: []store.Visibility{store.Public}, }) require.NoError(t, err) require.Equal(t, 1, len(publicMemos)) require.Equal(t, store.Public, publicMemos[0].Visibility) // List protected memos only protectedMemos, err := ts.ListMemos(ctx, &store.FindMemo{ VisibilityList: []store.Visibility{store.Protected}, }) require.NoError(t, err) require.Equal(t, 1, len(protectedMemos)) require.Equal(t, store.Protected, protectedMemos[0].Visibility) // List public and protected (multiple visibility) publicAndProtected, err := ts.ListMemos(ctx, &store.FindMemo{ VisibilityList: []store.Visibility{store.Public, store.Protected}, }) require.NoError(t, err) require.Equal(t, 2, len(publicAndProtected)) // List all allMemos, err := ts.ListMemos(ctx, &store.FindMemo{}) require.NoError(t, err) require.Equal(t, 3, len(allMemos)) ts.Close() } func TestMemoListWithPagination(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create 10 memos for i := 0; i < 10; i++ { _, err := ts.CreateMemo(ctx, &store.Memo{ UID: fmt.Sprintf("memo-%d", i), CreatorID: user.ID, Content: fmt.Sprintf("content %d", i), Visibility: store.Public, }) require.NoError(t, err) } // Test limit limit := 5 limitedMemos, err := ts.ListMemos(ctx, &store.FindMemo{Limit: &limit}) require.NoError(t, err) require.Equal(t, 5, len(limitedMemos)) // Test offset offset := 3 offsetMemos, err := ts.ListMemos(ctx, &store.FindMemo{Limit: &limit, Offset: &offset}) require.NoError(t, err) require.Equal(t, 5, len(offsetMemos)) // Verify offset works correctly (different memos) require.NotEqual(t, limitedMemos[0].ID, offsetMemos[0].ID) ts.Close() } func TestMemoUpdatePinned(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) memo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "pinnable-memo", CreatorID: user.ID, Content: "content", Visibility: store.Public, }) require.NoError(t, err) require.False(t, memo.Pinned) // Pin the memo pinned := true err = ts.UpdateMemo(ctx, &store.UpdateMemo{ ID: memo.ID, Pinned: &pinned, }) require.NoError(t, err) // Verify pinned found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID}) require.NoError(t, err) require.True(t, found.Pinned) // Unpin unpinned := false err = ts.UpdateMemo(ctx, &store.UpdateMemo{ ID: memo.ID, Pinned: &unpinned, }) require.NoError(t, err) found, err = ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID}) require.NoError(t, err) require.False(t, found.Pinned) ts.Close() } func TestMemoUpdateVisibility(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) memo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "visibility-memo", CreatorID: user.ID, Content: "content", Visibility: store.Public, }) require.NoError(t, err) require.Equal(t, store.Public, memo.Visibility) // Change to private privateVisibility := store.Private err = ts.UpdateMemo(ctx, &store.UpdateMemo{ ID: memo.ID, Visibility: &privateVisibility, }) require.NoError(t, err) found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID}) require.NoError(t, err) require.Equal(t, store.Private, found.Visibility) // Change to protected protectedVisibility := store.Protected err = ts.UpdateMemo(ctx, &store.UpdateMemo{ ID: memo.ID, Visibility: &protectedVisibility, }) require.NoError(t, err) found, err = ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID}) require.NoError(t, err) require.Equal(t, store.Protected, found.Visibility) ts.Close() } func TestMemoInvalidUID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create memo with invalid UID (contains special characters) _, err = ts.CreateMemo(ctx, &store.Memo{ UID: "invalid uid with spaces", CreatorID: user.ID, Content: "content", Visibility: store.Public, }) require.Error(t, err) require.Contains(t, err.Error(), "invalid uid") ts.Close() } func TestMemoCreateWithCustomTimestamps(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) customCreatedTs := int64(1700000000) // 2023-11-14 22:13:20 UTC customUpdatedTs := int64(1700000001) memo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "custom-timestamp-memo", CreatorID: user.ID, Content: "content with custom timestamps", Visibility: store.Public, CreatedTs: customCreatedTs, UpdatedTs: customUpdatedTs, }) require.NoError(t, err) require.Equal(t, customCreatedTs, memo.CreatedTs) require.Equal(t, customUpdatedTs, memo.UpdatedTs) // Fetch and verify timestamps are preserved found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID}) require.NoError(t, err) require.NotNil(t, found) require.Equal(t, customCreatedTs, found.CreatedTs) require.Equal(t, customUpdatedTs, found.UpdatedTs) ts.Close() } func TestMemoCreateWithOnlyCreatedTs(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) customCreatedTs := int64(1609459200) // 2021-01-01 00:00:00 UTC memo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "custom-created-ts-only", CreatorID: user.ID, Content: "content with custom created_ts only", Visibility: store.Public, CreatedTs: customCreatedTs, }) require.NoError(t, err) require.Equal(t, customCreatedTs, memo.CreatedTs) found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID}) require.NoError(t, err) require.NotNil(t, found) require.Equal(t, customCreatedTs, found.CreatedTs) ts.Close() } func TestMemoWithPayload(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create memo with tags in payload tags := []string{"tag1", "tag2", "tag3"} memo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "memo-with-payload", CreatorID: user.ID, Content: "content with tags", Visibility: store.Public, Payload: &storepb.MemoPayload{ Tags: tags, }, }) require.NoError(t, err) require.NotNil(t, memo.Payload) require.Equal(t, tags, memo.Payload.Tags) // Fetch and verify found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID}) require.NoError(t, err) require.NotNil(t, found.Payload) require.Equal(t, tags, found.Payload.Tags) ts.Close() } ================================================ FILE: store/test/migrator_test.go ================================================ package test import ( "context" "fmt" "os" "testing" "time" "github.com/stretchr/testify/require" "github.com/usememos/memos/store" ) // TestFreshInstall verifies that LATEST.sql applies correctly on a fresh database. // This is essentially what NewTestingStore already does, but we make it explicit. func TestFreshInstall(t *testing.T) { t.Parallel() ctx := context.Background() // NewTestingStore creates a fresh database and runs Migrate() // which applies LATEST.sql for uninitialized databases ts := NewTestingStore(ctx, t) // Verify migration completed successfully currentSchemaVersion, err := ts.GetCurrentSchemaVersion() require.NoError(t, err) require.NotEmpty(t, currentSchemaVersion, "schema version should be set after fresh install") // Verify we can read instance settings (basic sanity check) instanceSetting, err := ts.GetInstanceBasicSetting(ctx) require.NoError(t, err) require.Equal(t, currentSchemaVersion, instanceSetting.SchemaVersion) } // TestMigrationReRun verifies that re-running the migration on an already // migrated database does not fail or cause issues. This simulates a // scenario where the server is restarted. func TestMigrationReRun(t *testing.T) { t.Parallel() ctx := context.Background() // Use the shared testing store which already runs migrations on init ts := NewTestingStore(ctx, t) // Get current version initialVersion, err := ts.GetCurrentSchemaVersion() require.NoError(t, err) // Manually trigger migration again err = ts.Migrate(ctx) require.NoError(t, err, "re-running migration should not fail") // Verify version hasn't changed (or at least is valid) finalVersion, err := ts.GetCurrentSchemaVersion() require.NoError(t, err) require.Equal(t, initialVersion, finalVersion, "version should match after re-run") } // TestMigrationWithData verifies that migration preserves data integrity. // Creates data, then re-runs migration and verifies data is still accessible. func TestMigrationWithData(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create a user and memo before re-running migration user, err := createTestingHostUser(ctx, ts) require.NoError(t, err, "should create user") originalMemo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "migration-data-test", CreatorID: user.ID, Content: "Data before migration re-run", Visibility: store.Public, }) require.NoError(t, err, "should create memo") // Re-run migration err = ts.Migrate(ctx) require.NoError(t, err, "re-running migration should not fail") // Verify data is still accessible memo, err := ts.GetMemo(ctx, &store.FindMemo{UID: &originalMemo.UID}) require.NoError(t, err, "should retrieve memo after migration") require.Equal(t, "Data before migration re-run", memo.Content, "memo content should be preserved") } // TestMigrationMultipleReRuns verifies that migration is idempotent // even when run multiple times in succession. func TestMigrationMultipleReRuns(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Get initial version initialVersion, err := ts.GetCurrentSchemaVersion() require.NoError(t, err) // Run migration multiple times for i := 0; i < 3; i++ { err = ts.Migrate(ctx) require.NoError(t, err, "migration run %d should not fail", i+1) } // Verify version is still correct finalVersion, err := ts.GetCurrentSchemaVersion() require.NoError(t, err) require.Equal(t, initialVersion, finalVersion, "version should remain unchanged after multiple re-runs") } // TestMigrationFromStableVersion verifies that upgrading from a stable Memos version // to the current version works correctly. This is the critical upgrade path test. // // Test flow: // 1. Start a stable Memos container to create a database with the old schema // 2. Stop the container and wait for cleanup // 3. Use the store directly to run migration with current code // 4. Verify the migration succeeded and data can be written // // Note: This test is skipped when running with -race flag because testcontainers // has known race conditions in its reaper code that are outside our control. func TestMigrationFromStableVersion(t *testing.T) { // Skip for non-SQLite drivers (simplifies the test) if getDriverFromEnv() != "sqlite" { t.Skip("skipping upgrade test for non-sqlite driver") } // Skip if explicitly disabled (e.g., in environments without Docker) if os.Getenv("SKIP_CONTAINER_TESTS") == "1" { t.Skip("skipping container-based test (SKIP_CONTAINER_TESTS=1)") } ctx := context.Background() dataDir := t.TempDir() // 1. Start stable Memos container to create database with old schema cfg := MemosContainerConfig{ Driver: "sqlite", DataDir: dataDir, Version: StableMemosVersion, } t.Logf("Starting Memos %s container to create old-schema database...", cfg.Version) container, err := StartMemosContainer(ctx, cfg) require.NoError(t, err, "failed to start stable memos container") // Wait for the container to fully initialize the database time.Sleep(10 * time.Second) // Stop the container gracefully t.Log("Stopping stable Memos container...") err = container.Terminate(ctx) require.NoError(t, err, "failed to stop memos container") // Wait for file handles to be released time.Sleep(2 * time.Second) // 2. Connect to the database directly and run migration with current code dsn := fmt.Sprintf("%s/memos_prod.db", dataDir) t.Logf("Connecting to database at %s...", dsn) ts := NewTestingStoreWithDSN(ctx, t, "sqlite", dsn) // Get the schema version before migration oldSetting, err := ts.GetInstanceBasicSetting(ctx) require.NoError(t, err) t.Logf("Old schema version: %s", oldSetting.SchemaVersion) // 3. Run migration with current code t.Log("Running migration with current code...") err = ts.Migrate(ctx) require.NoError(t, err, "migration from stable version should succeed") // 4. Verify migration succeeded newVersion, err := ts.GetCurrentSchemaVersion() require.NoError(t, err) t.Logf("New schema version: %s", newVersion) newSetting, err := ts.GetInstanceBasicSetting(ctx) require.NoError(t, err) require.Equal(t, newVersion, newSetting.SchemaVersion, "schema version should be updated") // Verify we can write data to the migrated database user, err := createTestingHostUser(ctx, ts) require.NoError(t, err, "should create user after migration") memo, err := ts.CreateMemo(ctx, &store.Memo{ UID: "post-upgrade-memo", CreatorID: user.ID, Content: "Content after upgrade from stable", Visibility: store.Public, }) require.NoError(t, err, "should create memo after migration") require.Equal(t, "Content after upgrade from stable", memo.Content) t.Logf("Migration successful: %s -> %s", oldSetting.SchemaVersion, newVersion) } ================================================ FILE: store/test/reaction_test.go ================================================ package test import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/usememos/memos/store" ) func TestReactionStore(t *testing.T) { t.Parallel() 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]) // Test GetReaction. gotReaction, err := ts.GetReaction(ctx, &store.FindReaction{ ID: &reaction.ID, }) require.NoError(t, err) require.NotNil(t, gotReaction) require.Equal(t, reaction.ID, gotReaction.ID) require.Equal(t, reaction.CreatorID, gotReaction.CreatorID) require.Equal(t, reaction.ContentID, gotReaction.ContentID) require.Equal(t, reaction.ReactionType, gotReaction.ReactionType) // Test GetReaction with non-existent ID. nonExistentID := int32(99999) notFoundReaction, err := ts.GetReaction(ctx, &store.FindReaction{ ID: &nonExistentID, }) require.NoError(t, err) require.Nil(t, notFoundReaction) 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() } func TestReactionListByCreatorID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user1, err := createTestingHostUser(ctx, ts) require.NoError(t, err) user2, err := createTestingUserWithRole(ctx, ts, "user2", store.RoleUser) require.NoError(t, err) contentID := "shared_content" // User 1 creates reaction _, err = ts.UpsertReaction(ctx, &store.Reaction{ CreatorID: user1.ID, ContentID: contentID, ReactionType: "👍", }) require.NoError(t, err) // User 2 creates reaction _, err = ts.UpsertReaction(ctx, &store.Reaction{ CreatorID: user2.ID, ContentID: contentID, ReactionType: "❤️", }) require.NoError(t, err) // List all reactions for content reactions, err := ts.ListReactions(ctx, &store.FindReaction{ ContentID: &contentID, }) require.NoError(t, err) require.Len(t, reactions, 2) // List by creator ID user1Reactions, err := ts.ListReactions(ctx, &store.FindReaction{ CreatorID: &user1.ID, }) require.NoError(t, err) require.Len(t, user1Reactions, 1) require.Equal(t, "👍", user1Reactions[0].ReactionType) ts.Close() } func TestReactionMultipleContentIDs(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) contentID1 := "content_1" contentID2 := "content_2" // Create reactions for different contents _, err = ts.UpsertReaction(ctx, &store.Reaction{ CreatorID: user.ID, ContentID: contentID1, ReactionType: "👍", }) require.NoError(t, err) _, err = ts.UpsertReaction(ctx, &store.Reaction{ CreatorID: user.ID, ContentID: contentID2, ReactionType: "❤️", }) require.NoError(t, err) // List by content ID list reactions, err := ts.ListReactions(ctx, &store.FindReaction{ ContentIDList: []string{contentID1, contentID2}, }) require.NoError(t, err) require.Len(t, reactions, 2) ts.Close() } func TestReactionUpsertDifferentTypes(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) contentID := "test_content" // Create first reaction reaction1, err := ts.UpsertReaction(ctx, &store.Reaction{ CreatorID: user.ID, ContentID: contentID, ReactionType: "👍", }) require.NoError(t, err) // Create second reaction with different type (should create new, not update) reaction2, err := ts.UpsertReaction(ctx, &store.Reaction{ CreatorID: user.ID, ContentID: contentID, ReactionType: "❤️", }) require.NoError(t, err) // Both reactions should exist require.NotEqual(t, reaction1.ID, reaction2.ID) reactions, err := ts.ListReactions(ctx, &store.FindReaction{ ContentID: &contentID, }) require.NoError(t, err) require.Len(t, reactions, 2) ts.Close() } ================================================ FILE: store/test/store.go ================================================ package test import ( "context" "fmt" "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" ) // NewTestingStore creates a new testing store with a fresh database. // Each test gets its own isolated database: // - SQLite: new temp file per test // - MySQL/PostgreSQL: new database per test in shared container func NewTestingStore(ctx context.Context, t *testing.T) *store.Store { driver := getDriverFromEnv() profile := getTestingProfileForDriver(t, driver) dbDriver, err := db.NewDBDriver(profile) if err != nil { t.Fatalf("failed to create db driver: %v", err) } store := store.New(dbDriver, profile) if err := store.Migrate(ctx); err != nil { t.Fatalf("failed to migrate db: %v", err) } return store } // NewTestingStoreWithDSN creates a testing store connected to a specific DSN. // This is useful for testing migrations on existing data. func NewTestingStoreWithDSN(_ context.Context, t *testing.T, driver, dsn string) *store.Store { profile := &profile.Profile{ Port: getUnusedPort(), Data: t.TempDir(), // Dummy dir, DSN matters DSN: dsn, Driver: driver, Version: version.GetCurrentVersion(), } dbDriver, err := db.NewDBDriver(profile) if err != nil { t.Fatalf("failed to create db driver: %v", err) } store := store.New(dbDriver, profile) // Do not run Migrate() automatically, as we might be testing pre-migration state // or want to run it manually. return store } 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 } // getTestingProfileForDriver creates a testing profile for a specific driver. func getTestingProfileForDriver(t *testing.T, driver string) *profile.Profile { // Attempt to load .env file if present (optional, for local development) _ = godotenv.Load(".env") // Get a temporary directory for the test data. dir := t.TempDir() mode := "prod" port := getUnusedPort() var dsn string switch driver { case "sqlite": dsn = fmt.Sprintf("%s/memos_%s.db", dir, mode) case "mysql": dsn = GetMySQLDSN(t) case "postgres": dsn = GetPostgresDSN(t) default: t.Fatalf("unsupported driver: %s", driver) } return &profile.Profile{ Port: port, Data: dir, DSN: dsn, Driver: driver, Version: version.GetCurrentVersion(), } } func getDriverFromEnv() string { driver := os.Getenv("DRIVER") if driver == "" { driver = "sqlite" } return driver } ================================================ FILE: store/test/user_setting_test.go ================================================ package test import ( "context" "strings" "testing" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) func TestUserSettingStore(t *testing.T) { t.Parallel() 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() } func TestUserSettingGetByUserID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create setting _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_GENERAL, Value: &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: "zh"}}, }) require.NoError(t, err) // Get by user ID setting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &user.ID, Key: storepb.UserSetting_GENERAL, }) require.NoError(t, err) require.NotNil(t, setting) require.Equal(t, "zh", setting.GetGeneral().Locale) // Get non-existent key nonExistentSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &user.ID, Key: storepb.UserSetting_SHORTCUTS, }) require.NoError(t, err) require.Nil(t, nonExistentSetting) ts.Close() } func TestUserSettingUpsertUpdate(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create initial setting _, 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) // Update setting _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_GENERAL, Value: &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: "fr"}}, }) require.NoError(t, err) // Verify update setting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &user.ID, Key: storepb.UserSetting_GENERAL, }) require.NoError(t, err) require.Equal(t, "fr", setting.GetGeneral().Locale) // Verify only one setting exists list, err := ts.ListUserSettings(ctx, &store.FindUserSetting{UserID: &user.ID}) require.NoError(t, err) require.Equal(t, 1, len(list)) ts.Close() } func TestUserSettingRefreshTokens(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Initially no tokens tokens, err := ts.GetUserRefreshTokens(ctx, user.ID) require.NoError(t, err) require.Empty(t, tokens) // Add a refresh token token1 := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: "token-1", Description: "Chrome browser session", } err = ts.AddUserRefreshToken(ctx, user.ID, token1) require.NoError(t, err) // Verify token was added tokens, err = ts.GetUserRefreshTokens(ctx, user.ID) require.NoError(t, err) require.Len(t, tokens, 1) require.Equal(t, "token-1", tokens[0].TokenId) // Add another token token2 := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: "token-2", Description: "Firefox browser session", } err = ts.AddUserRefreshToken(ctx, user.ID, token2) require.NoError(t, err) tokens, err = ts.GetUserRefreshTokens(ctx, user.ID) require.NoError(t, err) require.Len(t, tokens, 2) // Get specific token by ID foundToken, err := ts.GetUserRefreshTokenByID(ctx, user.ID, "token-1") require.NoError(t, err) require.NotNil(t, foundToken) require.Equal(t, "Chrome browser session", foundToken.Description) // Get non-existent token notFound, err := ts.GetUserRefreshTokenByID(ctx, user.ID, "non-existent") require.NoError(t, err) require.Nil(t, notFound) // Remove token err = ts.RemoveUserRefreshToken(ctx, user.ID, "token-1") require.NoError(t, err) tokens, err = ts.GetUserRefreshTokens(ctx, user.ID) require.NoError(t, err) require.Len(t, tokens, 1) require.Equal(t, "token-2", tokens[0].TokenId) ts.Close() } func TestUserSettingPersonalAccessTokens(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Initially no PATs pats, err := ts.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) require.Empty(t, pats) // Add a PAT pat1 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-1", TokenHash: "pat-hash-1", Description: "API Token for external access", } err = ts.AddUserPersonalAccessToken(ctx, user.ID, pat1) require.NoError(t, err) // Verify PAT was added pats, err = ts.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) require.Len(t, pats, 1) require.Equal(t, "API Token for external access", pats[0].Description) // Add another PAT pat2 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-2", TokenHash: "pat-hash-2", Description: "CI Token", } err = ts.AddUserPersonalAccessToken(ctx, user.ID, pat2) require.NoError(t, err) pats, err = ts.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) require.Len(t, pats, 2) // Remove PAT err = ts.RemoveUserPersonalAccessToken(ctx, user.ID, "pat-1") require.NoError(t, err) pats, err = ts.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) require.Len(t, pats, 1) require.Equal(t, "pat-2", pats[0].TokenId) ts.Close() } func TestUserSettingWebhooks(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Initially no webhooks webhooks, err := ts.GetUserWebhooks(ctx, user.ID) require.NoError(t, err) require.Empty(t, webhooks) // Add a webhook webhook1 := &storepb.WebhooksUserSetting_Webhook{ Id: "webhook-1", Title: "Deploy Hook", Url: "https://example.com/webhook", } err = ts.AddUserWebhook(ctx, user.ID, webhook1) require.NoError(t, err) // Verify webhook was added webhooks, err = ts.GetUserWebhooks(ctx, user.ID) require.NoError(t, err) require.Len(t, webhooks, 1) require.Equal(t, "Deploy Hook", webhooks[0].Title) // Update webhook webhook1Updated := &storepb.WebhooksUserSetting_Webhook{ Id: "webhook-1", Title: "Updated Deploy Hook", Url: "https://example.com/webhook/v2", } err = ts.UpdateUserWebhook(ctx, user.ID, webhook1Updated) require.NoError(t, err) webhooks, err = ts.GetUserWebhooks(ctx, user.ID) require.NoError(t, err) require.Len(t, webhooks, 1) require.Equal(t, "Updated Deploy Hook", webhooks[0].Title) require.Equal(t, "https://example.com/webhook/v2", webhooks[0].Url) // Add another webhook webhook2 := &storepb.WebhooksUserSetting_Webhook{ Id: "webhook-2", Title: "Notification Hook", Url: "https://slack.example.com/webhook", } err = ts.AddUserWebhook(ctx, user.ID, webhook2) require.NoError(t, err) webhooks, err = ts.GetUserWebhooks(ctx, user.ID) require.NoError(t, err) require.Len(t, webhooks, 2) // Remove webhook err = ts.RemoveUserWebhook(ctx, user.ID, "webhook-1") require.NoError(t, err) webhooks, err = ts.GetUserWebhooks(ctx, user.ID) require.NoError(t, err) require.Len(t, webhooks, 1) require.Equal(t, "webhook-2", webhooks[0].Id) ts.Close() } func TestUserSettingShortcuts(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create shortcuts setting shortcuts := &storepb.ShortcutsUserSetting{ Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{ {Id: "shortcut-1", Title: "Work Notes", Filter: "tag:work"}, {Id: "shortcut-2", Title: "Personal", Filter: "tag:personal"}, }, } _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_SHORTCUTS, Value: &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts}, }) require.NoError(t, err) // Retrieve and verify setting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &user.ID, Key: storepb.UserSetting_SHORTCUTS, }) require.NoError(t, err) require.NotNil(t, setting) require.Len(t, setting.GetShortcuts().Shortcuts, 2) require.Equal(t, "Work Notes", setting.GetShortcuts().Shortcuts[0].Title) ts.Close() } func TestUserSettingGetUserByPATHash(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create a PAT with a known hash patHash := "test-pat-hash-12345" pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-test-1", TokenHash: patHash, Description: "Test PAT for lookup", } err = ts.AddUserPersonalAccessToken(ctx, user.ID, pat) require.NoError(t, err) // Lookup user by PAT hash result, err := ts.GetUserByPATHash(ctx, patHash) require.NoError(t, err) require.NotNil(t, result) require.Equal(t, user.ID, result.UserID) require.NotNil(t, result.User) require.Equal(t, user.Username, result.User.Username) require.NotNil(t, result.PAT) require.Equal(t, "pat-test-1", result.PAT.TokenId) require.Equal(t, "Test PAT for lookup", result.PAT.Description) ts.Close() } func TestUserSettingGetUserByPATHashNotFound(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) _, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Lookup non-existent PAT hash result, err := ts.GetUserByPATHash(ctx, "non-existent-hash") require.Error(t, err) require.Nil(t, result) ts.Close() } func TestUserSettingGetUserByPATHashNoTokensKey(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // User exists but has no PERSONAL_ACCESS_TOKENS key at all // This simulates fresh users or users upgraded from v0.25.3 result, err := ts.GetUserByPATHash(ctx, "any-hash") require.Error(t, err) require.Nil(t, result) // Error could be "PAT not found" (Postgres) or "sql: no rows in result set" (SQLite/MySQL) require.True(t, strings.Contains(err.Error(), "PAT not found") || strings.Contains(err.Error(), "no rows"), "expected PAT not found or no rows error, got: %v", err) // Now add a PAT for the user pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-new", TokenHash: "hash-new", Description: "New PAT", } err = ts.AddUserPersonalAccessToken(ctx, user.ID, pat) require.NoError(t, err) // Now the lookup should succeed result, err = ts.GetUserByPATHash(ctx, "hash-new") require.NoError(t, err) require.NotNil(t, result) require.Equal(t, user.ID, result.UserID) ts.Close() } func TestUserSettingGetUserByPATHashEmptyTokensArray(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Add a PAT setting with empty tokens array _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS, Value: &storepb.UserSetting_PersonalAccessTokens{ PersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{ Tokens: []*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{}, }, }, }) require.NoError(t, err) // Lookup should fail gracefully, not crash result, err := ts.GetUserByPATHash(ctx, "any-hash") require.Error(t, err) require.Nil(t, result) // Error could be "PAT not found" (Postgres) or "sql: no rows in result set" (SQLite/MySQL) require.True(t, strings.Contains(err.Error(), "PAT not found") || strings.Contains(err.Error(), "no rows"), "expected PAT not found or no rows error, got: %v", err) ts.Close() } func TestUserSettingGetUserByPATHashWithOtherUsers(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create multiple users - some with PATs, some without user1, err := createTestingHostUser(ctx, ts) require.NoError(t, err) _, err = createTestingUserWithRole(ctx, ts, "user2", store.RoleUser) require.NoError(t, err) user3, err := createTestingUserWithRole(ctx, ts, "user3", store.RoleUser) require.NoError(t, err) // User1: Has PAT pat1Hash := "user1-pat-hash-unique" err = ts.AddUserPersonalAccessToken(ctx, user1.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-user1", TokenHash: pat1Hash, Description: "User 1 PAT", }) require.NoError(t, err) // User2: Has no PERSONAL_ACCESS_TOKENS key (fresh user) // User3: Has empty tokens array _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user3.ID, Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS, Value: &storepb.UserSetting_PersonalAccessTokens{ PersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{ Tokens: []*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{}, }, }, }) require.NoError(t, err) // Should find user1's PAT despite user2 having no key and user3 having empty array result, err := ts.GetUserByPATHash(ctx, pat1Hash) require.NoError(t, err) require.NotNil(t, result) require.Equal(t, user1.ID, result.UserID) require.Equal(t, "pat-user1", result.PAT.TokenId) // Should not find non-existent hash even with mixed user states result, err = ts.GetUserByPATHash(ctx, "non-existent") require.Error(t, err) require.Nil(t, result) ts.Close() } func TestUserSettingGetUserByPATHashMultipleUsers(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user1, err := createTestingHostUser(ctx, ts) require.NoError(t, err) user2, err := createTestingUserWithRole(ctx, ts, "user2", store.RoleUser) require.NoError(t, err) // Create PATs for both users pat1Hash := "user1-pat-hash" err = ts.AddUserPersonalAccessToken(ctx, user1.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-user1", TokenHash: pat1Hash, Description: "User 1 PAT", }) require.NoError(t, err) pat2Hash := "user2-pat-hash" err = ts.AddUserPersonalAccessToken(ctx, user2.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-user2", TokenHash: pat2Hash, Description: "User 2 PAT", }) require.NoError(t, err) // Lookup user1's PAT result1, err := ts.GetUserByPATHash(ctx, pat1Hash) require.NoError(t, err) require.Equal(t, user1.ID, result1.UserID) require.Equal(t, user1.Username, result1.User.Username) // Lookup user2's PAT result2, err := ts.GetUserByPATHash(ctx, pat2Hash) require.NoError(t, err) require.Equal(t, user2.ID, result2.UserID) require.Equal(t, user2.Username, result2.User.Username) ts.Close() } func TestUserSettingGetUserByPATHashMultiplePATsSameUser(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create multiple PATs for the same user pat1Hash := "first-pat-hash" err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-1", TokenHash: pat1Hash, Description: "First PAT", }) require.NoError(t, err) pat2Hash := "second-pat-hash" err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-2", TokenHash: pat2Hash, Description: "Second PAT", }) require.NoError(t, err) // Both PATs should resolve to the same user result1, err := ts.GetUserByPATHash(ctx, pat1Hash) require.NoError(t, err) require.Equal(t, user.ID, result1.UserID) require.Equal(t, "pat-1", result1.PAT.TokenId) result2, err := ts.GetUserByPATHash(ctx, pat2Hash) require.NoError(t, err) require.Equal(t, user.ID, result2.UserID) require.Equal(t, "pat-2", result2.PAT.TokenId) ts.Close() } func TestUserSettingUpdatePATLastUsed(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create a PAT patHash := "pat-hash-for-update" err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-update-test", TokenHash: patHash, Description: "PAT for update test", }) require.NoError(t, err) // Update last used timestamp now := timestamppb.Now() err = ts.UpdatePATLastUsed(ctx, user.ID, "pat-update-test", now) require.NoError(t, err) // Verify the update pats, err := ts.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) require.Len(t, pats, 1) require.NotNil(t, pats[0].LastUsedAt) ts.Close() } func TestUserSettingGetUserByPATHashWithExpiredToken(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create a PAT with expiration info patHash := "pat-hash-with-expiry" expiresAt := timestamppb.Now() pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-expiry-test", TokenHash: patHash, Description: "PAT with expiry", ExpiresAt: expiresAt, } err = ts.AddUserPersonalAccessToken(ctx, user.ID, pat) require.NoError(t, err) // Should still be able to look up by hash (expiry check is done at auth level) result, err := ts.GetUserByPATHash(ctx, patHash) require.NoError(t, err) require.NotNil(t, result) require.Equal(t, user.ID, result.UserID) require.NotNil(t, result.PAT.ExpiresAt) ts.Close() } func TestUserSettingGetUserByPATHashAfterRemoval(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create a PAT patHash := "pat-hash-to-remove" err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-remove-test", TokenHash: patHash, Description: "PAT to be removed", }) require.NoError(t, err) // Verify it exists result, err := ts.GetUserByPATHash(ctx, patHash) require.NoError(t, err) require.NotNil(t, result) // Remove the PAT err = ts.RemoveUserPersonalAccessToken(ctx, user.ID, "pat-remove-test") require.NoError(t, err) // Should no longer be found result, err = ts.GetUserByPATHash(ctx, patHash) require.Error(t, err) require.Nil(t, result) ts.Close() } func TestUserSettingGetUserByPATHashSpecialCharacters(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create PATs with special characters in hash (simulating real hash values) testCases := []struct { tokenID string tokenHash string }{ {"pat-special-1", "abc123+/=XYZ"}, {"pat-special-2", "sha256:abcdef1234567890"}, {"pat-special-3", "$2a$10$N9qo8uLOickgx2ZMRZoMy"}, } for _, tc := range testCases { err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: tc.tokenID, TokenHash: tc.tokenHash, Description: "PAT with special chars", }) require.NoError(t, err) // Verify lookup works with special characters result, err := ts.GetUserByPATHash(ctx, tc.tokenHash) require.NoError(t, err) require.NotNil(t, result) require.Equal(t, tc.tokenID, result.PAT.TokenId) } ts.Close() } func TestUserSettingGetUserByPATHashLargeTokenCount(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create many PATs for the same user tokenCount := 10 hashes := make([]string, tokenCount) for i := 0; i < tokenCount; i++ { hashes[i] = "pat-hash-" + string(rune('A'+i)) + "-large-test" err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-large-" + string(rune('A'+i)), TokenHash: hashes[i], Description: "PAT for large count test", }) require.NoError(t, err) } // Verify each hash can be looked up for i, hash := range hashes { result, err := ts.GetUserByPATHash(ctx, hash) require.NoError(t, err) require.NotNil(t, result) require.Equal(t, user.ID, result.UserID) require.Equal(t, "pat-large-"+string(rune('A'+i)), result.PAT.TokenId) } ts.Close() } func TestUserSettingMultipleSettingTypes(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Create GENERAL setting _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_GENERAL, Value: &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: "ja"}}, }) require.NoError(t, err) // Create SHORTCUTS setting _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_SHORTCUTS, Value: &storepb.UserSetting_Shortcuts{Shortcuts: &storepb.ShortcutsUserSetting{ Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{ {Id: "s1", Title: "Shortcut 1"}, }, }}, }) require.NoError(t, err) // Add a PAT err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-multi", TokenHash: "hash-multi", }) require.NoError(t, err) // List all settings for user settings, err := ts.ListUserSettings(ctx, &store.FindUserSetting{UserID: &user.ID}) require.NoError(t, err) require.Len(t, settings, 3) // Verify each setting type generalSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_GENERAL}) require.NoError(t, err) require.Equal(t, "ja", generalSetting.GetGeneral().Locale) shortcutsSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_SHORTCUTS}) require.NoError(t, err) require.Len(t, shortcutsSetting.GetShortcuts().Shortcuts, 1) patsSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS}) require.NoError(t, err) require.Len(t, patsSetting.GetPersonalAccessTokens().Tokens, 1) ts.Close() } func TestUserSettingShortcutsEdgeCases(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Case 1: Special characters in Filter and Title // Includes quotes, backslashes, newlines, and other JSON-sensitive characters specialCharsFilter := `tag in ["work", "project"] && content.contains("urgent")` specialCharsTitle := `Work "Urgent" \ Notes` shortcuts := &storepb.ShortcutsUserSetting{ Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{ {Id: "s1", Title: specialCharsTitle, Filter: specialCharsFilter}, }, } _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_SHORTCUTS, Value: &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts}, }) require.NoError(t, err) setting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &user.ID, Key: storepb.UserSetting_SHORTCUTS, }) require.NoError(t, err) require.NotNil(t, setting) require.Len(t, setting.GetShortcuts().Shortcuts, 1) require.Equal(t, specialCharsTitle, setting.GetShortcuts().Shortcuts[0].Title) require.Equal(t, specialCharsFilter, setting.GetShortcuts().Shortcuts[0].Filter) // Case 2: Unicode characters unicodeFilter := `tag in ["你好", "世界"]` unicodeTitle := `My 🚀 Shortcuts` shortcuts = &storepb.ShortcutsUserSetting{ Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{ {Id: "s2", Title: unicodeTitle, Filter: unicodeFilter}, }, } _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_SHORTCUTS, Value: &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts}, }) require.NoError(t, err) setting, err = ts.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &user.ID, Key: storepb.UserSetting_SHORTCUTS, }) require.NoError(t, err) require.NotNil(t, setting) require.Len(t, setting.GetShortcuts().Shortcuts, 1) require.Equal(t, unicodeTitle, setting.GetShortcuts().Shortcuts[0].Title) require.Equal(t, unicodeFilter, setting.GetShortcuts().Shortcuts[0].Filter) // Case 3: Empty shortcuts list // Should allow saving an empty list (clearing shortcuts) shortcuts = &storepb.ShortcutsUserSetting{ Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{}, } _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_SHORTCUTS, Value: &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts}, }) require.NoError(t, err) setting, err = ts.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &user.ID, Key: storepb.UserSetting_SHORTCUTS, }) require.NoError(t, err) require.NotNil(t, setting) require.NotNil(t, setting.GetShortcuts()) require.Len(t, setting.GetShortcuts().Shortcuts, 0) // Case 4: Large filter string // Test reasonable large string handling (e.g. 4KB) largeFilter := strings.Repeat("tag:long_tag_name ", 200) shortcuts = &storepb.ShortcutsUserSetting{ Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{ {Id: "s3", Title: "Large Filter", Filter: largeFilter}, }, } _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_SHORTCUTS, Value: &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts}, }) require.NoError(t, err) setting, err = ts.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &user.ID, Key: storepb.UserSetting_SHORTCUTS, }) require.NoError(t, err) require.NotNil(t, setting) require.Equal(t, largeFilter, setting.GetShortcuts().Shortcuts[0].Filter) ts.Close() } func TestUserSettingShortcutsPartialUpdate(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Initial set shortcuts := &storepb.ShortcutsUserSetting{ Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{ {Id: "s1", Title: "Note 1", Filter: "tag:1"}, {Id: "s2", Title: "Note 2", Filter: "tag:2"}, }, } _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_SHORTCUTS, Value: &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts}, }) require.NoError(t, err) // Update by replacing the whole list (Store Upsert replaces the value for the key) // We want to verify that we can "update" a single item by sending the modified list updatedShortcuts := &storepb.ShortcutsUserSetting{ Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{ {Id: "s1", Title: "Note 1 Updated", Filter: "tag:1_updated"}, {Id: "s2", Title: "Note 2", Filter: "tag:2"}, {Id: "s3", Title: "Note 3", Filter: "tag:3"}, // Add new one }, } _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSetting_SHORTCUTS, Value: &storepb.UserSetting_Shortcuts{Shortcuts: updatedShortcuts}, }) require.NoError(t, err) setting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &user.ID, Key: storepb.UserSetting_SHORTCUTS, }) require.NoError(t, err) require.NotNil(t, setting) require.Len(t, setting.GetShortcuts().Shortcuts, 3) // Verify updates for _, s := range setting.GetShortcuts().Shortcuts { if s.Id == "s1" { require.Equal(t, "Note 1 Updated", s.Title) require.Equal(t, "tag:1_updated", s.Filter) } else if s.Id == "s2" { require.Equal(t, "Note 2", s.Title) } else if s.Id == "s3" { require.Equal(t, "Note 3", s.Title) } } ts.Close() } func TestUserSettingJSONFieldsEdgeCases(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Case 1: Webhook with special characters and Unicode in Title and URL specialWebhook := &storepb.WebhooksUserSetting_Webhook{ Id: "wh-special", Title: `My "Special" & 🚀`, Url: "https://example.com/hook?query=你好¶m=\"value\"", } err = ts.AddUserWebhook(ctx, user.ID, specialWebhook) require.NoError(t, err) webhooks, err := ts.GetUserWebhooks(ctx, user.ID) require.NoError(t, err) require.Len(t, webhooks, 1) require.Equal(t, specialWebhook.Title, webhooks[0].Title) require.Equal(t, specialWebhook.Url, webhooks[0].Url) // Case 2: PAT with special description specialPAT := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{ TokenId: "pat-special", TokenHash: "hash-special", Description: "Token for 'CLI' \n & \"API\" \t with unicode 🔑", } err = ts.AddUserPersonalAccessToken(ctx, user.ID, specialPAT) require.NoError(t, err) pats, err := ts.GetUserPersonalAccessTokens(ctx, user.ID) require.NoError(t, err) require.Len(t, pats, 1) require.Equal(t, specialPAT.Description, pats[0].Description) // Case 3: Refresh Token with special description specialRefreshToken := &storepb.RefreshTokensUserSetting_RefreshToken{ TokenId: "rt-special", Description: "Browser: Firefox (Nightly) / OS: Linux 🐧", } err = ts.AddUserRefreshToken(ctx, user.ID, specialRefreshToken) require.NoError(t, err) tokens, err := ts.GetUserRefreshTokens(ctx, user.ID) require.NoError(t, err) require.Len(t, tokens, 1) require.Equal(t, specialRefreshToken.Description, tokens[0].Description) ts.Close() } ================================================ FILE: store/test/user_test.go ================================================ package test import ( "context" "fmt" "testing" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" "github.com/usememos/memos/store" ) func TestUserStore(t *testing.T) { t.Parallel() 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.RoleAdmin, 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 TestUserGetByID(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Get user by ID found, err := ts.GetUser(ctx, &store.FindUser{ID: &user.ID}) require.NoError(t, err) require.NotNil(t, found) require.Equal(t, user.ID, found.ID) require.Equal(t, user.Username, found.Username) // Get non-existent user nonExistentID := int32(99999) notFound, err := ts.GetUser(ctx, &store.FindUser{ID: &nonExistentID}) require.NoError(t, err) require.Nil(t, notFound) // Get system bot systemBotID := store.SystemBotID systemBot, err := ts.GetUser(ctx, &store.FindUser{ID: &systemBotID}) require.NoError(t, err) require.NotNil(t, systemBot) require.Equal(t, store.SystemBotID, systemBot.ID) require.Equal(t, "system_bot", systemBot.Username) ts.Close() } func TestUserGetByUsername(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Get user by username found, err := ts.GetUser(ctx, &store.FindUser{Username: &user.Username}) require.NoError(t, err) require.NotNil(t, found) require.Equal(t, user.Username, found.Username) // Get non-existent username nonExistent := "nonexistent" notFound, err := ts.GetUser(ctx, &store.FindUser{Username: &nonExistent}) require.NoError(t, err) require.Nil(t, notFound) ts.Close() } func TestUserListByRole(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create users with different roles _, err := createTestingHostUser(ctx, ts) require.NoError(t, err) _, err = createTestingUserWithRole(ctx, ts, "admin_user", store.RoleAdmin) require.NoError(t, err) regularUser, err := createTestingUserWithRole(ctx, ts, "regular_user", store.RoleUser) require.NoError(t, err) // List all users allUsers, err := ts.ListUsers(ctx, &store.FindUser{}) require.NoError(t, err) require.Equal(t, 3, len(allUsers)) // List only ADMIN users adminRole := store.RoleAdmin adminOnlyUsers, err := ts.ListUsers(ctx, &store.FindUser{Role: &adminRole}) require.NoError(t, err) require.Equal(t, 2, len(adminOnlyUsers)) // List only USER role users userRole := store.RoleUser regularUsers, err := ts.ListUsers(ctx, &store.FindUser{Role: &userRole}) require.NoError(t, err) require.Equal(t, 1, len(regularUsers)) require.Equal(t, regularUser.ID, regularUsers[0].ID) ts.Close() } func TestUserUpdateRowStatus(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) require.Equal(t, store.Normal, user.RowStatus) // Archive user archivedStatus := store.Archived updated, err := ts.UpdateUser(ctx, &store.UpdateUser{ ID: user.ID, RowStatus: &archivedStatus, }) require.NoError(t, err) require.Equal(t, store.Archived, updated.RowStatus) // Verify by fetching fetched, err := ts.GetUser(ctx, &store.FindUser{ID: &user.ID}) require.NoError(t, err) require.Equal(t, store.Archived, fetched.RowStatus) // Restore to normal normalStatus := store.Normal restored, err := ts.UpdateUser(ctx, &store.UpdateUser{ ID: user.ID, RowStatus: &normalStatus, }) require.NoError(t, err) require.Equal(t, store.Normal, restored.RowStatus) ts.Close() } func TestUserUpdateAllFields(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) // Update all fields newUsername := "updated_username" newEmail := "updated@test.com" newNickname := "Updated Nickname" newAvatarURL := "https://example.com/avatar.png" newDescription := "Updated description" newRole := store.RoleAdmin newPasswordHash := "new_password_hash" updated, err := ts.UpdateUser(ctx, &store.UpdateUser{ ID: user.ID, Username: &newUsername, Email: &newEmail, Nickname: &newNickname, AvatarURL: &newAvatarURL, Description: &newDescription, Role: &newRole, PasswordHash: &newPasswordHash, }) require.NoError(t, err) require.Equal(t, newUsername, updated.Username) require.Equal(t, newEmail, updated.Email) require.Equal(t, newNickname, updated.Nickname) require.Equal(t, newAvatarURL, updated.AvatarURL) require.Equal(t, newDescription, updated.Description) require.Equal(t, newRole, updated.Role) require.Equal(t, newPasswordHash, updated.PasswordHash) // Verify by fetching again fetched, err := ts.GetUser(ctx, &store.FindUser{ID: &user.ID}) require.NoError(t, err) require.Equal(t, newUsername, fetched.Username) ts.Close() } func TestUserListWithLimit(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) // Create 5 users for i := 0; i < 5; i++ { role := store.RoleUser if i == 0 { role = store.RoleAdmin } _, err := createTestingUserWithRole(ctx, ts, fmt.Sprintf("user%d", i), role) require.NoError(t, err) } // List with limit limit := 3 users, err := ts.ListUsers(ctx, &store.FindUser{Limit: &limit}) require.NoError(t, err) require.Equal(t, 3, len(users)) ts.Close() } func createTestingHostUser(ctx context.Context, ts *store.Store) (*store.User, error) { return createTestingUserWithRole(ctx, ts, "test", store.RoleAdmin) } func createTestingUserWithRole(ctx context.Context, ts *store.Store, username string, role store.Role) (*store.User, error) { userCreate := &store.User{ Username: username, Role: role, Email: username + "@test.com", Nickname: username + "_nickname", Description: username + "_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 } ================================================ FILE: store/user.go ================================================ package store import ( "context" ) // Role is the type of a role. type Role string const ( // RoleAdmin is the ADMIN role. RoleAdmin Role = "ADMIN" // RoleUser is the USER role. RoleUser Role = "USER" ) func (e Role) String() string { switch e { case RoleAdmin: return "ADMIN" default: 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 // Domain specific fields Filters []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 } ================================================ FILE: store/user_setting.go ================================================ 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 } // RefreshTokenQueryResult contains the result of querying a refresh token. type RefreshTokenQueryResult struct { UserID int32 RefreshToken *storepb.RefreshTokensUserSetting_RefreshToken } // PATQueryResult contains the result of querying a PAT by hash. type PATQueryResult struct { UserID int32 User *User PAT *storepb.PersonalAccessTokensUserSetting_PersonalAccessToken } 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 } // GetUserByPATHash finds a user by PAT hash. func (s *Store) GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error) { result, err := s.driver.GetUserByPATHash(ctx, tokenHash) if err != nil { return nil, err } // Fetch user info user, err := s.GetUser(ctx, &FindUser{ID: &result.UserID}) if err != nil { return nil, err } if user == nil { return nil, errors.New("user not found for PAT") } result.User = user return result, nil } // GetUserRefreshTokens returns the refresh tokens of the user. func (s *Store) GetUserRefreshTokens(ctx context.Context, userID int32) ([]*storepb.RefreshTokensUserSetting_RefreshToken, error) { userSetting, err := s.GetUserSetting(ctx, &FindUserSetting{ UserID: &userID, Key: storepb.UserSetting_REFRESH_TOKENS, }) if err != nil { return nil, err } if userSetting == nil { return []*storepb.RefreshTokensUserSetting_RefreshToken{}, nil } return userSetting.GetRefreshTokens().RefreshTokens, nil } // AddUserRefreshToken adds a new refresh token for the user. func (s *Store) AddUserRefreshToken(ctx context.Context, userID int32, token *storepb.RefreshTokensUserSetting_RefreshToken) error { tokens, err := s.GetUserRefreshTokens(ctx, userID) if err != nil { return err } tokens = append(tokens, token) _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: userID, Key: storepb.UserSetting_REFRESH_TOKENS, Value: &storepb.UserSetting_RefreshTokens{ RefreshTokens: &storepb.RefreshTokensUserSetting{ RefreshTokens: tokens, }, }, }) return err } // RemoveUserRefreshToken removes a refresh token from the user. func (s *Store) RemoveUserRefreshToken(ctx context.Context, userID int32, tokenID string) error { existingTokens, err := s.GetUserRefreshTokens(ctx, userID) if err != nil { return err } newTokens := make([]*storepb.RefreshTokensUserSetting_RefreshToken, 0, len(existingTokens)) for _, token := range existingTokens { if token.TokenId != tokenID { newTokens = append(newTokens, token) } } _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: userID, Key: storepb.UserSetting_REFRESH_TOKENS, Value: &storepb.UserSetting_RefreshTokens{ RefreshTokens: &storepb.RefreshTokensUserSetting{ RefreshTokens: newTokens, }, }, }) return err } // GetUserRefreshTokenByID returns a specific refresh token. func (s *Store) GetUserRefreshTokenByID(ctx context.Context, userID int32, tokenID string) (*storepb.RefreshTokensUserSetting_RefreshToken, error) { tokens, err := s.GetUserRefreshTokens(ctx, userID) if err != nil { return nil, err } for _, token := range tokens { if token.TokenId == tokenID { return token, nil } } return nil, nil } // GetUserPersonalAccessTokens returns the PATs of the user. func (s *Store) GetUserPersonalAccessTokens(ctx context.Context, userID int32) ([]*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken, error) { userSetting, err := s.GetUserSetting(ctx, &FindUserSetting{ UserID: &userID, Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS, }) if err != nil { return nil, err } if userSetting == nil { return []*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{}, nil } return userSetting.GetPersonalAccessTokens().Tokens, nil } // AddUserPersonalAccessToken adds a new PAT for the user. func (s *Store) AddUserPersonalAccessToken(ctx context.Context, userID int32, token *storepb.PersonalAccessTokensUserSetting_PersonalAccessToken) error { tokens, err := s.GetUserPersonalAccessTokens(ctx, userID) if err != nil { return err } tokens = append(tokens, token) _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: userID, Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS, Value: &storepb.UserSetting_PersonalAccessTokens{ PersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{ Tokens: tokens, }, }, }) return err } // RemoveUserPersonalAccessToken removes a PAT from the user. func (s *Store) RemoveUserPersonalAccessToken(ctx context.Context, userID int32, tokenID string) error { existingTokens, err := s.GetUserPersonalAccessTokens(ctx, userID) if err != nil { return err } newTokens := make([]*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken, 0, len(existingTokens)) for _, token := range existingTokens { if token.TokenId != tokenID { newTokens = append(newTokens, token) } } _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: userID, Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS, Value: &storepb.UserSetting_PersonalAccessTokens{ PersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{ Tokens: newTokens, }, }, }) return err } // UpdatePATLastUsed updates the last_used_at timestamp of a PAT. func (s *Store) UpdatePATLastUsed(ctx context.Context, userID int32, tokenID string, lastUsed *timestamppb.Timestamp) error { tokens, err := s.GetUserPersonalAccessTokens(ctx, userID) if err != nil { return err } for _, token := range tokens { if token.TokenId == tokenID { token.LastUsedAt = lastUsed break } } _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: userID, Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS, Value: &storepb.UserSetting_PersonalAccessTokens{ PersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{ Tokens: tokens, }, }, }) 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_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_REFRESH_TOKENS: refreshTokensUserSetting := &storepb.RefreshTokensUserSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), refreshTokensUserSetting); err != nil { return nil, err } userSetting.Value = &storepb.UserSetting_RefreshTokens{RefreshTokens: refreshTokensUserSetting} case storepb.UserSetting_PERSONAL_ACCESS_TOKENS: patsUserSetting := &storepb.PersonalAccessTokensUserSetting{} if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), patsUserSetting); err != nil { return nil, err } userSetting.Value = &storepb.UserSetting_PersonalAccessTokens{PersonalAccessTokens: patsUserSetting} 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_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_REFRESH_TOKENS: refreshTokensUserSetting := userSetting.GetRefreshTokens() value, err := protojson.Marshal(refreshTokensUserSetting) if err != nil { return nil, err } raw.Value = string(value) case storepb.UserSetting_PERSONAL_ACCESS_TOKENS: patsUserSetting := userSetting.GetPersonalAccessTokens() value, err := protojson.Marshal(patsUserSetting) 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 } ================================================ FILE: web/.gitignore ================================================ node_modules .pnpm-store .DS_Store dist dist-ssr *.local src/types/proto/store ================================================ FILE: web/biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/2.3.5/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "includes": [ "**", "!!**/dist", "!src/types/proto" ], "ignoreUnknown": true }, "formatter": { "enabled": true, "formatWithErrors": false, "indentStyle": "space", "indentWidth": 2, "lineEnding": "lf", "lineWidth": 140, "attributePosition": "auto", "bracketSameLine": false, "bracketSpacing": true, "expand": "auto", "useEditorconfig": true }, "linter": { "enabled": true, "rules": { "recommended": false, "complexity": { "noAdjacentSpacesInRegex": "error", "noExtraBooleanCast": "error", "noUselessCatch": "error", "noUselessEscapeInRegex": "error", "noUselessTypeConstraint": "error" }, "correctness": { "noConstAssign": "error", "noConstantCondition": "error", "noEmptyCharacterClassInRegex": "error", "noEmptyPattern": "error", "noGlobalObjectCalls": "error", "noInvalidBuiltinInstantiation": "error", "noInvalidConstructorSuper": "error", "noNonoctalDecimalEscape": "error", "noPrecisionLoss": "error", "noSelfAssign": "error", "noSetterReturn": "error", "noSwitchDeclarations": "error", "noUndeclaredVariables": "error", "noUnreachable": "error", "noUnreachableSuper": "error", "noUnsafeFinally": "error", "noUnsafeOptionalChaining": "error", "noUnusedLabels": "error", "noUnusedPrivateClassMembers": "error", "noUnusedVariables": "error", "useIsNan": "error", "useValidForDirection": "error", "useValidTypeof": "error", "useYield": "error" }, "style": { "noCommonJs": "error", "noNamespace": "error", "useArrayLiterals": "error", "useAsConstAssertion": "error", "useBlockStatements": "off" }, "suspicious": { "noAsyncPromiseExecutor": "error", "noCatchAssign": "error", "noClassAssign": "error", "noCompareNegZero": "error", "noConstantBinaryExpressions": "error", "noControlCharactersInRegex": "error", "noDebugger": "error", "noDuplicateCase": "error", "noDuplicateClassMembers": "error", "noDuplicateElseIf": "error", "noDuplicateObjectKeys": "error", "noDuplicateParameters": "error", "noEmptyBlockStatements": "off", "noExplicitAny": "error", "noExtraNonNullAssertion": "error", "noFallthroughSwitchClause": "error", "noFunctionAssign": "error", "noGlobalAssign": "error", "noImportAssign": "error", "noIrregularWhitespace": "error", "noMisleadingCharacterClass": "error", "noMisleadingInstantiator": "error", "noNonNullAssertedOptionalChain": "error", "noPrototypeBuiltins": "error", "noRedeclare": "error", "noShadowRestrictedNames": "error", "noSparseArray": "error", "noUnsafeDeclarationMerging": "error", "noUnsafeNegation": "error", "noUselessRegexBackrefs": "error", "noWith": "error", "useGetterReturn": "error", "useNamespaceKeyword": "error" } }, "includes": [ "**", "!**/dist/**", "!**/node_modules/**", "!src/types/proto/**" ] }, "javascript": { "formatter": { "jsxQuoteStyle": "double", "quoteProperties": "asNeeded", "trailingCommas": "all", "semicolons": "always", "arrowParentheses": "always", "bracketSameLine": false, "quoteStyle": "double", "attributePosition": "auto", "bracketSpacing": true }, "globals": [] }, "css": { "parser": { "cssModules": false, "allowWrongLineComments": true, "tailwindDirectives": true } }, "html": { "formatter": { "indentScriptAndStyle": false, "selfCloseVoidElements": "always" } }, "overrides": [ { "includes": [ "**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts" ], "linter": { "rules": { "complexity": { "noArguments": "error" }, "correctness": { "noConstAssign": "off", "noGlobalObjectCalls": "off", "noInvalidBuiltinInstantiation": "off", "noInvalidConstructorSuper": "off", "noSetterReturn": "off", "noUndeclaredVariables": "off", "noUnreachable": "off", "noUnreachableSuper": "off" }, "style": { "useConst": "error" }, "suspicious": { "noClassAssign": "off", "noDuplicateClassMembers": "off", "noDuplicateObjectKeys": "off", "noDuplicateParameters": "off", "noFunctionAssign": "off", "noImportAssign": "off", "noRedeclare": "off", "noUnsafeNegation": "off", "noVar": "error", "noWith": "off", "useGetterReturn": "off" } } } }, { "includes": [ "src/utils/i18n.ts" ], "linter": { "rules": {} } } ], "assist": { "enabled": true, "actions": { "source": { "organizeImports": "on" } } } } ================================================ FILE: web/components.json ================================================ { "$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" } ================================================ FILE: web/docs/auth-architecture.md ================================================ # Authentication State Architecture ## Current Approach: AuthContext The application uses **AuthContext** for authentication state management, not React Query's `useCurrentUserQuery`. This is an intentional architectural decision. ### Why AuthContext Instead of React Query? #### 1. **Synchronous Initialization** - AuthContext fetches user data during app initialization (`main.tsx`) - Provides synchronous access to `currentUser` throughout the app - No need to handle loading states in every component #### 2. **Single Source of Truth** - User data fetched once on mount - All components get consistent, up-to-date user info - No race conditions from multiple query instances #### 3. **Integration with React Query** - AuthContext pre-populates React Query cache after fetch (line 81-82 in `AuthContext.tsx`) - Best of both worlds: synchronous access + cache consistency - React Query hooks like `useNotifications()` can still use the cached user data #### 4. **Simpler Component Code** ```typescript // With AuthContext (current) const user = useCurrentUser(); // Always returns User | undefined // With React Query (alternative) const { data: user, isLoading } = useCurrentUserQuery(); if (isLoading) return ; // Need loading handling everywhere ``` ### When to Use React Query for Auth? Consider migrating auth to React Query if: - App needs real-time user profile updates from external sources - Multiple tabs need instant sync - User data changes frequently during a session For Memos (a notes app where user profile rarely changes), AuthContext is the right choice. ### Future Considerations The unused `useCurrentUserQuery()` hook in `useUserQueries.ts` is kept for potential future use. If requirements change (e.g., real-time collaboration on user profiles), migration path is clear: 1. Remove AuthContext 2. Use `useCurrentUserQuery()` everywhere 3. Handle loading states in components 4. Add suspense boundaries if needed ## Recommendation **Keep the current AuthContext approach.** It provides better DX and performance for this use case. ================================================ FILE: web/index.html ================================================ Memos
================================================ FILE: web/package.json ================================================ { "name": "memos", "private": true, "engines": { "node": ">=24" }, "scripts": { "dev": "vite", "build": "vite build", "release": "vite build --mode release --outDir=../server/router/frontend/dist --emptyOutDir", "lint": "tsc --noEmit --skipLibCheck && biome check src", "lint:fix": "biome check --write src", "format": "biome format --write src" }, "dependencies": { "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-web": "^2.1.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@github/relative-time-element": "^4.5.0", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-query": "^5.90.21", "@tanstack/react-query-devtools": "^5.91.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.20", "fuse.js": "^7.1.0", "highlight.js": "^11.11.1", "i18next": "^25.8.18", "katex": "^0.16.38", "leaflet": "^1.9.4", "leaflet.markercluster": "^1.5.3", "lodash-es": "^4.17.23", "lucide-react": "^0.577.0", "mdast-util-from-markdown": "^2.0.3", "mdast-util-gfm": "^3.1.0", "mermaid": "^11.13.0", "micromark-extension-gfm": "^3.0.0", "mime": "^4.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-force-graph-2d": "^1.29.1", "react-hot-toast": "^2.6.0", "react-i18next": "^15.7.4", "react-leaflet": "^4.2.1", "react-leaflet-cluster": "^2.1.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.13.1", "react-use": "^17.6.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", "textarea-caret": "^3.1.0", "unist-util-visit": "^5.1.0", "uuid": "^11.1.0" }, "devDependencies": { "@biomejs/biome": "^2.4.7", "baseline-browser-mapping": "^2.10.8", "@bufbuild/protobuf": "^2.11.0", "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/katex": "^0.16.8", "@types/leaflet": "^1.9.21", "@types/lodash-es": "^4.17.12", "@types/mdast": "^4.0.4", "@types/node": "^24.10.1", "@types/qs": "^6.15.0", "@types/react": "^18.3.27", "@types/react-dom": "^18.3.7", "@types/textarea-caret": "^3.0.4", "@types/unist": "^3.0.3", "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.7.0", "long": "^5.3.2", "terser": "^5.46.1", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", "vite": "^7.2.4" }, "pnpm": { "onlyBuiltDependencies": [ "esbuild" ] } } ================================================ FILE: web/public/site.webmanifest ================================================ { "name": "Memos", "short_name": "Memos", "description": "An open-source, self-hosted note-taking tool. Capture thoughts instantly. Own them completely.", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "display": "standalone", "scope": "/", "start_url": "/", "theme_color": "#faf9f5", "background_color": "#faf9f5" } ================================================ FILE: web/src/App.tsx ================================================ import { useEffect } from "react"; import { Outlet } from "react-router-dom"; import { useInstance } from "./contexts/InstanceContext"; import { MemoFilterProvider } from "./contexts/MemoFilterContext"; import useNavigateTo from "./hooks/useNavigateTo"; import { useUserLocale } from "./hooks/useUserLocale"; import { useUserTheme } from "./hooks/useUserTheme"; import { cleanupExpiredOAuthState } from "./utils/oauth"; const App = () => { const navigateTo = useNavigateTo(); const { profile: instanceProfile, profileLoaded, generalSetting: instanceGeneralSetting } = useInstance(); // Apply user preferences reactively useUserLocale(); useUserTheme(); // Clean up expired OAuth states on app initialization useEffect(() => { cleanupExpiredOAuthState(); }, []); // Redirect to sign up page if instance not initialized (no admin account exists yet). // Guard with profileLoaded so a fetch failure doesn't incorrectly trigger the redirect. useEffect(() => { if (profileLoaded && !instanceProfile.admin) { navigateTo("/auth/signup"); } }, [profileLoaded, instanceProfile.admin, navigateTo]); useEffect(() => { if (instanceGeneralSetting.additionalStyle) { const styleEl = document.createElement("style"); styleEl.innerHTML = instanceGeneralSetting.additionalStyle; styleEl.setAttribute("type", "text/css"); document.body.insertAdjacentElement("beforeend", styleEl); } }, [instanceGeneralSetting.additionalStyle]); useEffect(() => { if (instanceGeneralSetting.additionalScript) { const scriptEl = document.createElement("script"); scriptEl.innerHTML = instanceGeneralSetting.additionalScript; document.head.appendChild(scriptEl); } }, [instanceGeneralSetting.additionalScript]); // Dynamic update metadata with customized profile useEffect(() => { if (!instanceGeneralSetting.customProfile) { return; } document.title = instanceGeneralSetting.customProfile.title; const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement; link.href = instanceGeneralSetting.customProfile.logoUrl || "/logo.webp"; }, [instanceGeneralSetting.customProfile]); return ( ); }; export default App; ================================================ FILE: web/src/auth-state.ts ================================================ // Access token storage using localStorage for persistence across tabs and sessions. // Tokens are cleared on logout or expiry. let accessToken: string | null = null; let tokenExpiresAt: Date | null = null; const TOKEN_KEY = "memos_access_token"; const EXPIRES_KEY = "memos_token_expires_at"; // BroadcastChannel lets tabs share freshly-refreshed tokens so that only one // tab needs to hit the refresh endpoint. When another tab successfully refreshes // we adopt the new token immediately, avoiding a redundant (and potentially // conflicting) refresh request of our own. const TOKEN_CHANNEL_NAME = "memos_token_sync"; // Token refresh policy: // - REQUEST_TOKEN_EXPIRY_BUFFER_MS: used for normal API requests. // - FOCUS_TOKEN_EXPIRY_BUFFER_MS: used on tab visibility restore to refresh earlier. export const REQUEST_TOKEN_EXPIRY_BUFFER_MS = 30 * 1000; export const FOCUS_TOKEN_EXPIRY_BUFFER_MS = 2 * 60 * 1000; interface TokenBroadcastMessage { token: string; expiresAt: string; // ISO string } let tokenChannel: BroadcastChannel | null = null; function getTokenChannel(): BroadcastChannel | null { if (tokenChannel) return tokenChannel; try { tokenChannel = new BroadcastChannel(TOKEN_CHANNEL_NAME); tokenChannel.onmessage = (event: MessageEvent) => { const { token, expiresAt } = event.data ?? {}; if (token && expiresAt) { // Another tab refreshed — adopt the token in-memory so we don't // fire our own refresh request. accessToken = token; tokenExpiresAt = new Date(expiresAt); } }; } catch { // BroadcastChannel not available (e.g. some privacy modes) tokenChannel = null; } return tokenChannel; } // Initialize the channel at module load so the listener is registered // before any token refresh can occur in any tab. getTokenChannel(); export const getAccessToken = (): string | null => { if (!accessToken) { try { const storedToken = localStorage.getItem(TOKEN_KEY); const storedExpires = localStorage.getItem(EXPIRES_KEY); if (storedToken && storedExpires) { const expiresAt = new Date(storedExpires); if (expiresAt > new Date()) { accessToken = storedToken; tokenExpiresAt = expiresAt; } // Do NOT remove expired tokens here. Callers such as InstanceContext.initialize() // run concurrently with AuthContext.initialize() via Promise.all. If we eagerly // delete the expired token from localStorage, hasStoredToken() (called synchronously // inside AuthContext.initialize()) finds nothing and skips the refresh attempt, // logging the user out even when the refresh-token cookie is still valid. // clearAccessToken() handles proper cleanup after a confirmed auth failure or logout. } } catch (e) { // localStorage might not be available (e.g., in some privacy modes) console.warn("Failed to access localStorage:", e); } } return accessToken; }; export const setAccessToken = (token: string | null, expiresAt?: Date): void => { accessToken = token; tokenExpiresAt = expiresAt || null; try { if (token && expiresAt) { localStorage.setItem(TOKEN_KEY, token); localStorage.setItem(EXPIRES_KEY, expiresAt.toISOString()); // Broadcast to other tabs so they adopt the new token without refreshing. const msg: TokenBroadcastMessage = { token, expiresAt: expiresAt.toISOString() }; getTokenChannel()?.postMessage(msg); } else { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(EXPIRES_KEY); } } catch (e) { // localStorage might not be available (e.g., in some privacy modes) console.warn("Failed to write to localStorage:", e); } }; export const isTokenExpired = (bufferMs: number = REQUEST_TOKEN_EXPIRY_BUFFER_MS): boolean => { if (!tokenExpiresAt) return true; // Consider expired with a safety buffer before actual expiry. return new Date() >= new Date(tokenExpiresAt.getTime() - bufferMs); }; // Returns true if a token exists in localStorage, even if it is expired. // Used to decide whether to attempt GetCurrentUser on app init — if no token // was ever stored, the user is definitively not logged in and there is nothing // to refresh, so we can skip the network round-trip entirely. export const hasStoredToken = (): boolean => { if (accessToken) return true; try { return !!localStorage.getItem(TOKEN_KEY); } catch { return false; } }; export const clearAccessToken = (): void => { accessToken = null; tokenExpiresAt = null; try { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(EXPIRES_KEY); } catch (e) { console.warn("Failed to clear localStorage:", e); } }; ================================================ FILE: web/src/components/ActivityCalendar/CalendarCell.tsx ================================================ import { memo } from "react"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { DEFAULT_CELL_SIZE, SMALL_CELL_SIZE } from "./constants"; import type { CalendarDayCell, CalendarSize } from "./types"; import { getCellIntensityClass } from "./utils"; export interface CalendarCellProps { day: CalendarDayCell; maxCount: number; tooltipText: string; onClick?: (date: string) => void; size?: CalendarSize; disableTooltip?: boolean; } export const CalendarCell = memo((props: CalendarCellProps) => { const { day, maxCount, tooltipText, onClick, size = "default", disableTooltip = false } = props; const handleClick = () => { if (day.count > 0 && onClick) { onClick(day.date); } }; const sizeConfig = size === "small" ? SMALL_CELL_SIZE : DEFAULT_CELL_SIZE; const smallExtraClasses = size === "small" ? `${SMALL_CELL_SIZE.dimensions} min-h-0` : ""; const baseClasses = cn( "aspect-square w-full flex items-center justify-center text-center transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-2 select-none border border-border/10 bg-muted/20", sizeConfig.font, sizeConfig.borderRadius, smallExtraClasses, ); const isInteractive = Boolean(onClick && day.count > 0); const ariaLabel = day.isSelected ? `${tooltipText} (selected)` : tooltipText; if (!day.isCurrentMonth) { return
{day.label}
; } const intensityClass = getCellIntensityClass(day, maxCount); const buttonClasses = cn( baseClasses, intensityClass, day.isToday && "ring-2 ring-primary/30 ring-offset-1 font-semibold z-10", day.isSelected && "ring-2 ring-primary ring-offset-1 font-bold z-10", isInteractive ? "cursor-pointer hover:bg-muted/40 hover:border-border/30" : "cursor-default", ); const button = ( ); const shouldShowTooltip = tooltipText && day.count > 0 && !disableTooltip; if (!shouldShowTooltip) { return button; } return ( {button}

{tooltipText}

); }); CalendarCell.displayName = "CalendarCell"; ================================================ FILE: web/src/components/ActivityCalendar/MonthCalendar.tsx ================================================ import { memo, useMemo } from "react"; import { useInstance } from "@/contexts/InstanceContext"; import { cn } from "@/lib/utils"; import { useTranslate } from "@/utils/i18n"; import { CalendarCell } from "./CalendarCell"; import { useTodayDate, useWeekdayLabels } from "./hooks"; import type { CalendarSize, MonthCalendarProps } from "./types"; import { useCalendarMatrix } from "./useCalendar"; import { getTooltipText } from "./utils"; const GRID_STYLES: Record = { small: { gap: "gap-1.5", headerText: "text-[10px]" }, default: { gap: "gap-2", headerText: "text-xs" }, }; interface WeekdayHeaderProps { weekDays: string[]; size: CalendarSize; } const WeekdayHeader = memo(({ weekDays, size }: WeekdayHeaderProps) => (
{weekDays.map((label, index) => (
{label}
))}
)); WeekdayHeader.displayName = "WeekdayHeader"; export const MonthCalendar = memo((props: MonthCalendarProps) => { const { month, data, maxCount, size = "default", onClick, className, disableTooltips = false } = props; const t = useTranslate(); const { generalSetting } = useInstance(); const today = useTodayDate(); const weekDays = useWeekdayLabels(); const { weeks, weekDays: rotatedWeekDays } = useCalendarMatrix({ month, data, weekDays, weekStartDayOffset: generalSetting.weekStartDayOffset, today, selectedDate: "", }); const flatDays = useMemo(() => weeks.flatMap((week) => week.days), [weeks]); return (
{flatDays.map((day) => ( ))}
); }); MonthCalendar.displayName = "MonthCalendar"; ================================================ FILE: web/src/components/ActivityCalendar/YearCalendar.tsx ================================================ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import { memo, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { useTranslate } from "@/utils/i18n"; import { getMaxYear, MIN_YEAR } from "./constants"; import { MonthCalendar } from "./MonthCalendar"; import type { YearCalendarProps } from "./types"; import { calculateYearMaxCount, filterDataByYear, generateMonthsForYear, getMonthLabel } from "./utils"; interface YearNavigationProps { selectedYear: number; currentYear: number; onPrev: () => void; onNext: () => void; onToday: () => void; canGoPrev: boolean; canGoNext: boolean; } const YearNavigation = memo(({ selectedYear, currentYear, onPrev, onNext, onToday, canGoPrev, canGoNext }: YearNavigationProps) => { const t = useTranslate(); const isCurrentYear = selectedYear === currentYear; return (

{selectedYear}

); }); YearNavigation.displayName = "YearNavigation"; interface MonthCardProps { month: string; data: Record; maxCount: number; onDateClick: (date: string) => void; } const MonthCard = memo(({ month, data, maxCount, onDateClick }: MonthCardProps) => (
{getMonthLabel(month)}
)); MonthCard.displayName = "MonthCard"; export const YearCalendar = memo(({ selectedYear, data, onYearChange, onDateClick, className }: YearCalendarProps) => { const currentYear = useMemo(() => new Date().getFullYear(), []); const yearData = useMemo(() => filterDataByYear(data, selectedYear), [data, selectedYear]); const months = useMemo(() => generateMonthsForYear(selectedYear), [selectedYear]); const yearMaxCount = useMemo(() => calculateYearMaxCount(yearData), [yearData]); const canGoPrev = selectedYear > MIN_YEAR; const canGoNext = selectedYear < getMaxYear(); return (
canGoPrev && onYearChange(selectedYear - 1)} onNext={() => canGoNext && onYearChange(selectedYear + 1)} onToday={() => onYearChange(currentYear)} canGoPrev={canGoPrev} canGoNext={canGoNext} />
{months.map((month) => ( ))}
); }); YearCalendar.displayName = "YearCalendar"; ================================================ FILE: web/src/components/ActivityCalendar/constants.ts ================================================ export const DAYS_IN_WEEK = 7; export const MONTHS_IN_YEAR = 12; export const WEEKEND_DAYS = [0, 6] as const; export const MIN_COUNT = 1; export const MIN_YEAR = 1970; export const getMaxYear = () => new Date().getFullYear() + 1; export const INTENSITY_THRESHOLDS = { HIGH: 0.75, MEDIUM: 0.5, LOW: 0.25, MINIMAL: 0, } as const; export const CELL_STYLES = { HIGH: "bg-primary text-primary-foreground shadow-sm border-transparent", MEDIUM: "bg-primary/85 text-primary-foreground shadow-sm border-transparent", LOW: "bg-primary/70 text-primary-foreground border-transparent", MINIMAL: "bg-primary/50 text-foreground border-transparent", EMPTY: "bg-muted/20 text-muted-foreground hover:bg-muted/30 border-border/10", } as const; export const SMALL_CELL_SIZE = { font: "text-[11px]", dimensions: "w-full h-full", borderRadius: "rounded-lg", gap: "gap-1.5", } as const; export const DEFAULT_CELL_SIZE = { font: "text-xs", borderRadius: "rounded-lg", gap: "gap-2", } as const; ================================================ FILE: web/src/components/ActivityCalendar/hooks.ts ================================================ import dayjs from "dayjs"; import { useMemo } from "react"; import { useTranslate } from "@/utils/i18n"; export const useWeekdayLabels = () => { const t = useTranslate(); return useMemo( () => [ t("common.days.sun"), t("common.days.mon"), t("common.days.tue"), t("common.days.wed"), t("common.days.thu"), t("common.days.fri"), t("common.days.sat"), ], [t], ); }; export const useTodayDate = () => { return dayjs().format("YYYY-MM-DD"); }; ================================================ FILE: web/src/components/ActivityCalendar/index.ts ================================================ export * from "./MonthCalendar"; export * from "./types"; export * from "./utils"; export * from "./YearCalendar"; ================================================ FILE: web/src/components/ActivityCalendar/types.ts ================================================ export type CalendarSize = "default" | "small"; export interface CalendarDayCell { date: string; label: number; count: number; isCurrentMonth: boolean; isToday: boolean; isSelected: boolean; isWeekend: boolean; } export interface CalendarDayRow { days: CalendarDayCell[]; } export interface CalendarMatrixResult { weeks: CalendarDayRow[]; weekDays: string[]; maxCount: number; } export interface MonthCalendarProps { month: string; data: Record; maxCount: number; size?: CalendarSize; onClick?: (date: string) => void; className?: string; disableTooltips?: boolean; } export interface YearCalendarProps { selectedYear: number; data: Record; onYearChange: (year: number) => void; onDateClick: (date: string) => void; className?: string; } ================================================ FILE: web/src/components/ActivityCalendar/useCalendar.ts ================================================ import dayjs from "dayjs"; import { useMemo } from "react"; import { DAYS_IN_WEEK, MIN_COUNT, WEEKEND_DAYS } from "./constants"; import type { CalendarDayCell, CalendarMatrixResult } from "./types"; export interface UseCalendarMatrixParams { month: string; data: Record; weekDays: string[]; weekStartDayOffset: number; today: string; selectedDate: string; } const createCalendarDayCell = ( current: dayjs.Dayjs, monthKey: string, data: Record, today: string, selectedDate: string, ): CalendarDayCell => { const isoDate = current.format("YYYY-MM-DD"); const isCurrentMonth = current.format("YYYY-MM") === monthKey; const count = data[isoDate] ?? 0; return { date: isoDate, label: current.date(), count, isCurrentMonth, isToday: isoDate === today, isSelected: isoDate === selectedDate, isWeekend: WEEKEND_DAYS.includes(current.day() as 0 | 6), }; }; const calculateCalendarBoundaries = (monthStart: dayjs.Dayjs, weekStartDayOffset: number) => { const monthEnd = monthStart.endOf("month"); const startOffset = (monthStart.day() - weekStartDayOffset + DAYS_IN_WEEK) % DAYS_IN_WEEK; const endOffset = (weekStartDayOffset + (DAYS_IN_WEEK - 1) - monthEnd.day() + DAYS_IN_WEEK) % DAYS_IN_WEEK; const calendarStart = monthStart.subtract(startOffset, "day"); const calendarEnd = monthEnd.add(endOffset, "day"); const dayCount = calendarEnd.diff(calendarStart, "day") + 1; return { calendarStart, dayCount }; }; /** * Generates a matrix of calendar days for a given month, handling week alignment and data mapping. */ export const useCalendarMatrix = ({ month, data, weekDays, weekStartDayOffset, today, selectedDate, }: UseCalendarMatrixParams): CalendarMatrixResult => { return useMemo(() => { // Determine the start of the month and its formatted key (YYYY-MM) const monthStart = dayjs(month).startOf("month"); const monthKey = monthStart.format("YYYY-MM"); // Rotate week labels based on the user's preferred start of the week const rotatedWeekDays = weekDays.slice(weekStartDayOffset).concat(weekDays.slice(0, weekStartDayOffset)); // Calculate the start and end dates for the calendar grid to ensure full weeks const { calendarStart, dayCount } = calculateCalendarBoundaries(monthStart, weekStartDayOffset); const weeks: CalendarMatrixResult["weeks"] = []; let maxCount = 0; // Iterate through each day in the calendar grid for (let index = 0; index < dayCount; index += 1) { const current = calendarStart.add(index, "day"); const weekIndex = Math.floor(index / DAYS_IN_WEEK); if (!weeks[weekIndex]) { weeks[weekIndex] = { days: [] }; } // Create the day cell object with data and status flags const dayCell = createCalendarDayCell(current, monthKey, data, today, selectedDate); weeks[weekIndex].days.push(dayCell); maxCount = Math.max(maxCount, dayCell.count); } return { weeks, weekDays: rotatedWeekDays, maxCount: Math.max(maxCount, MIN_COUNT), }; }, [month, data, weekDays, weekStartDayOffset, today, selectedDate]); }; ================================================ FILE: web/src/components/ActivityCalendar/utils.ts ================================================ import dayjs from "dayjs"; import isSameOrAfter from "dayjs/plugin/isSameOrAfter"; import isSameOrBefore from "dayjs/plugin/isSameOrBefore"; import { useTranslate } from "@/utils/i18n"; import { CELL_STYLES, INTENSITY_THRESHOLDS, MIN_COUNT, MONTHS_IN_YEAR } from "./constants"; import type { CalendarDayCell } from "./types"; dayjs.extend(isSameOrAfter); dayjs.extend(isSameOrBefore); export type TranslateFunction = ReturnType; export const getCellIntensityClass = (day: CalendarDayCell, maxCount: number): string => { if (!day.isCurrentMonth || day.count === 0) { return CELL_STYLES.EMPTY; } const ratio = day.count / maxCount; if (ratio > INTENSITY_THRESHOLDS.HIGH) return CELL_STYLES.HIGH; if (ratio > INTENSITY_THRESHOLDS.MEDIUM) return CELL_STYLES.MEDIUM; if (ratio > INTENSITY_THRESHOLDS.LOW) return CELL_STYLES.LOW; return CELL_STYLES.MINIMAL; }; export const generateMonthsForYear = (year: number): string[] => { return Array.from({ length: MONTHS_IN_YEAR }, (_, i) => dayjs(`${year}-01-01`).add(i, "month").format("YYYY-MM")); }; export const calculateYearMaxCount = (data: Record): number => { let max = 0; for (const count of Object.values(data)) { max = Math.max(max, count); } return Math.max(max, MIN_COUNT); }; export const getMonthLabel = (month: string): string => { return dayjs(month).format("MMM"); }; export const filterDataByYear = (data: Record, year: number): Record => { if (!data) return {}; const filtered: Record = {}; const yearStart = dayjs(`${year}-01-01`); const yearEnd = dayjs(`${year}-12-31`); for (const [dateStr, count] of Object.entries(data)) { const date = dayjs(dateStr); if (date.isSameOrAfter(yearStart, "day") && date.isSameOrBefore(yearEnd, "day")) { filtered[dateStr] = count; } } return filtered; }; export const hasActivityData = (data: Record): boolean => { return Object.values(data).some((count) => count > 0); }; export const getTooltipText = (count: number, date: string, t: TranslateFunction): string => { if (count === 0) { return date; } return t("memo.count-memos-in-date", { count, memos: count === 1 ? t("common.memo") : t("common.memos"), date, }).toLowerCase(); }; ================================================ FILE: web/src/components/AttachmentIcon.tsx ================================================ 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_pb"; import { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; import SquareDiv from "./kit/SquareDiv"; import PreviewImageDialog from "./PreviewImageDialog"; 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); ================================================ FILE: web/src/components/AuthFooter.tsx ================================================ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; import { loadLocale } from "@/utils/i18n"; import { getInitialTheme, loadTheme, Theme } from "@/utils/theme"; import LocaleSelect from "./LocaleSelect"; import ThemeSelect from "./ThemeSelect"; interface Props { className?: string; } const AuthFooter = ({ className }: Props) => { const { i18n: i18nInstance } = useTranslation(); const currentLocale = i18nInstance.language as Locale; const [currentTheme, setCurrentTheme] = useState(getInitialTheme()); const handleLocaleChange = (locale: Locale) => { loadLocale(locale); }; const handleThemeChange = (theme: string) => { loadTheme(theme); setCurrentTheme(theme as Theme); }; return (
); }; export default AuthFooter; ================================================ FILE: web/src/components/ChangeMemberPasswordDialog.tsx ================================================ import { 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 { useUpdateUser } from "@/hooks/useUserQueries"; import { handleError } from "@/lib/error"; import { User } from "@/types/proto/api/v1/user_service_pb"; import { useTranslate } from "@/utils/i18n"; interface Props { open: boolean; onOpenChange: (open: boolean) => void; user?: User; onSuccess?: () => void; } function ChangeMemberPasswordDialog({ open, onOpenChange, user, onSuccess }: Props) { const t = useTranslate(); const { mutateAsync: updateUser } = useUpdateUser(); const [newPassword, setNewPassword] = useState(""); const [newPasswordAgain, setNewPasswordAgain] = useState(""); 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 updateUser({ user: { name: user.name, password: newPassword, }, updateMask: ["password"], }); toast(t("message.password-changed")); onSuccess?.(); onOpenChange(false); } catch (error: unknown) { await handleError(error, toast.error, { context: "Change member password", }); } }; if (!user) return null; return ( {t("setting.account.change-password")} ({user.displayName})
); } export default ChangeMemberPasswordDialog; ================================================ FILE: web/src/components/ConfirmDialog/README.md ================================================ # ConfirmDialog - Accessible Confirmation Dialog ## Overview `ConfirmDialog` standardizes confirmation flows across the app. It replaces ad‑hoc `window.confirm` usage with an accessible, themeable dialog that supports asynchronous operations. ## Key Features ### 1. Accessibility & UX - Uses shared `Dialog` primitives (focus trap, ARIA roles) - Blocks dismissal while async confirm is pending - Clear separation of title (action) vs description (context) ### 2. Async-Aware - Accepts sync or async `onConfirm` - Auto-closes on resolve; remains open on error for retry / toast ### 3. Internationalization Ready - All labels / text provided by caller through i18n hook - Supports interpolation for dynamic context ### 4. Minimal Surface, Easy Extension - Lightweight API (few required props) - Style hook via `.container` class (SCSS module) ## Architecture ``` ConfirmDialog ├── State: loading (tracks pending confirm action) ├── Dialog primitives: Header (title + description), Footer (buttons) └── External control: parent owns open state via onOpenChange ``` ## Usage ```tsx import { useTranslate } from "@/utils/i18n"; import ConfirmDialog from "@/components/ConfirmDialog"; const t = useTranslate(); ; ``` ## Props | Prop | Type | Required | Acceptable Values | |------|------|----------|------------------| | `open` | `boolean` | Yes | `true` (visible) / `false` (hidden) | | `onOpenChange` | `(open: boolean) => void` | Yes | Callback receiving next state; should update parent state | | `title` | `React.ReactNode` | Yes | Short localized action summary (text / node) | | `description` | `React.ReactNode` | No | Optional contextual message | | `confirmLabel` | `string` | Yes | Non-empty localized action text (1–2 words) | | `cancelLabel` | `string` | Yes | Localized cancel label | | `onConfirm` | `() => void | Promise` | Yes | Sync or async handler; resolve = close, reject = stay open | | `confirmVariant` | `"default" | "destructive"` | No | Defaults to `"default"`; use `"destructive"` for irreversible actions | ## Benefits vs Previous Implementation ### Before (window.confirm / ad‑hoc dialogs) - Blocking native prompt, inconsistent styling - No async progress handling - No rich formatting - Hard to localize consistently ### After (ConfirmDialog) - Unified styling + accessibility semantics - Async-safe with loading state shielding - Plain description flexibility - i18n-first via externalized labels ## Technical Implementation Details ### Async Handling ```tsx const handleConfirm = async () => { setLoading(true); try { await onConfirm(); // resolve -> close onOpenChange(false); } catch (e) { console.error(e); // remain open for retry } finally { setLoading(false); } }; ``` ### Close Guard ```tsx !loading && onOpenChange(next)} /> ``` ## Browser / Environment Support - Works anywhere the existing `Dialog` primitives work (modern browsers) - No ResizeObserver / layout dependencies ## Performance Considerations 1. Minimal renders: loading state toggles once per confirm attempt 2. No portal churn—relies on underlying dialog infra ## Future Enhancements 1. Severity icon / header accent 2. Auto-focus destructive button toggle 3. Secondary action (e.g. "Archive" vs "Delete") 4. Built-in retry / error slot 5. Optional checkbox confirmation ("I understand the consequences") 6. Motion/animation tokens integration ## Styling The `ConfirmDialog.module.scss` file provides a `.container` hook. It currently only hosts a harmless custom property so the stylesheet is non-empty. Add real layout or variant tokens there instead of inline styles. ## Internationalization All visible strings must come from the translation system. Use `useTranslate()` and pass localized values into props. Separate keys for title/description. ## Error Handling Errors thrown in `onConfirm` are caught and logged. The dialog stays open so the caller can surface a toast or inline message and allow retry. (Consider routing serious errors to a higher-level handler.) --- If you extend this component, update this README to keep usage discoverable. ================================================ FILE: web/src/components/ConfirmDialog/index.tsx ================================================ import * as React from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; export interface ConfirmDialogProps { open: boolean; onOpenChange: (open: boolean) => void; title: React.ReactNode; description?: React.ReactNode; confirmLabel: string; cancelLabel: string; onConfirm: () => void | Promise; confirmVariant?: "default" | "destructive"; } export default function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, cancelLabel, onConfirm, confirmVariant = "default", }: ConfirmDialogProps) { const [loading, setLoading] = React.useState(false); const handleConfirm = async () => { try { setLoading(true); await onConfirm(); onOpenChange(false); } catch (e) { // Intentionally swallow errors so user can retry; surface via caller's toast/logging console.error("ConfirmDialog error for action:", title, e); } finally { setLoading(false); } }; return ( !loading && onOpenChange(o)}> {title} {description ? {description} : null} ); } ================================================ FILE: web/src/components/CreateAccessTokenDialog.tsx ================================================ import copy from "copy-to-clipboard"; import React, { 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 { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Textarea } from "@/components/ui/textarea"; import { userServiceClient } from "@/connect"; import useCurrentUser from "@/hooks/useCurrentUser"; import useLoading from "@/hooks/useLoading"; import { handleError } from "@/lib/error"; import { CreatePersonalAccessTokenResponse } from "@/types/proto/api/v1/user_service_pb"; import { useTranslate } from "@/utils/i18n"; interface Props { open: boolean; onOpenChange: (open: boolean) => void; onSuccess: (response: CreatePersonalAccessTokenResponse) => void; } interface State { description: string; expiration: number; } function CreateAccessTokenDialog({ open, onOpenChange, onSuccess }: Props) { const t = useTranslate(); const currentUser = useCurrentUser(); const [state, setState] = useState({ description: "", expiration: 30, // Default: 30 days }); const [createdToken, setCreatedToken] = useState(null); const requestState = useLoading(false); // Expiration options in days (0 = never expires) const expirationOptions = [ { label: t("setting.access-token.create-dialog.duration-1m"), value: 30, }, { label: "90 Days", value: 90, }, { label: t("setting.access-token.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(); const response = await userServiceClient.createPersonalAccessToken({ parent: currentUser?.name, description: state.description, expiresInDays: state.expiration, }); requestState.setFinish(); onSuccess(response); if (response.token) { setCreatedToken(response.token); } else { onOpenChange(false); } } catch (error: unknown) { handleError(error, toast.error, { context: "Create access token", onError: () => requestState.setError(), }); } }; const handleCopyToken = () => { if (!createdToken) return; copy(createdToken); toast.success(t("message.copied")); }; useEffect(() => { if (!open) return; setState({ description: "", expiration: 30, }); setCreatedToken(null); }, [open]); return ( {t("setting.access-token.create-dialog.create-access-token")} {createdToken ? (
); }); export default Editor; ================================================ FILE: web/src/components/MemoEditor/Editor/shortcuts.ts ================================================ import type { EditorRefActions } from "./index"; const SHORTCUTS = { BOLD: { key: "b", delimiter: "**" }, ITALIC: { key: "i", delimiter: "*" }, LINK: { key: "k" }, } as const; const URL_PLACEHOLDER = "url"; const URL_REGEX = /^https?:\/\/[^\s]+$/; const LINK_OFFSET = 3; // Length of "]()" export function handleMarkdownShortcuts(event: React.KeyboardEvent, editor: EditorRefActions): void { const key = event.key.toLowerCase(); if (key === SHORTCUTS.BOLD.key) { event.preventDefault(); toggleTextStyle(editor, SHORTCUTS.BOLD.delimiter); } else if (key === SHORTCUTS.ITALIC.key) { event.preventDefault(); toggleTextStyle(editor, SHORTCUTS.ITALIC.delimiter); } else if (key === SHORTCUTS.LINK.key) { event.preventDefault(); insertHyperlink(editor); } } export function insertHyperlink(editor: EditorRefActions, url?: string): void { const cursorPosition = editor.getCursorPosition(); const selectedContent = editor.getSelectedContent(); const isUrlSelected = !url && URL_REGEX.test(selectedContent.trim()); if (isUrlSelected) { editor.insertText(`[](${selectedContent})`); editor.setCursorPosition(cursorPosition + 1, cursorPosition + 1); return; } const href = url ?? URL_PLACEHOLDER; editor.insertText(`[${selectedContent}](${href})`); if (href === URL_PLACEHOLDER) { const urlStart = cursorPosition + selectedContent.length + LINK_OFFSET; editor.setCursorPosition(urlStart, urlStart + href.length); } } function toggleTextStyle(editor: EditorRefActions, delimiter: string): void { const cursorPosition = editor.getCursorPosition(); const selectedContent = editor.getSelectedContent(); const isStyled = selectedContent.startsWith(delimiter) && selectedContent.endsWith(delimiter); if (isStyled) { const unstyled = selectedContent.slice(delimiter.length, -delimiter.length); editor.insertText(unstyled); editor.setCursorPosition(cursorPosition, cursorPosition + unstyled.length); } else { editor.insertText(`${delimiter}${selectedContent}${delimiter}`); editor.setCursorPosition(cursorPosition + delimiter.length, cursorPosition + delimiter.length + selectedContent.length); } } export function hyperlinkHighlightedText(editor: EditorRefActions, url: string): void { const selectedContent = editor.getSelectedContent(); const cursorPosition = editor.getCursorPosition(); editor.insertText(`[${selectedContent}](${url})`); const newPosition = cursorPosition + selectedContent.length + url.length + 4; editor.setCursorPosition(newPosition, newPosition); } ================================================ FILE: web/src/components/MemoEditor/Editor/useListCompletion.ts ================================================ import { useEffect, useRef } from "react"; import { detectLastListItem, generateListContinuation } from "@/utils/markdown-list-detection"; import { EditorRefActions } from "."; interface UseListCompletionOptions { editorRef: React.RefObject; editorActions: EditorRefActions; isInIME: boolean; } // Patterns to detect empty list items const EMPTY_LIST_PATTERNS = [ /^(\s*)([-*+])\s*$/, // Empty unordered list /^(\s*)([-*+])\s+\[([ xX])\]\s*$/, // Empty task list /^(\s*)(\d+)[.)]\s*$/, // Empty ordered list ]; const isEmptyListItem = (line: string) => EMPTY_LIST_PATTERNS.some((pattern) => pattern.test(line)); export function useListCompletion({ editorRef, editorActions, isInIME }: UseListCompletionOptions) { const isInIMERef = useRef(isInIME); isInIMERef.current = isInIME; const editorActionsRef = useRef(editorActions); editorActionsRef.current = editorActions; // Track when composition ends to handle Safari race condition // Safari fires keydown(Enter) immediately after compositionend, while Chrome doesn't // See: https://github.com/usememos/memos/issues/5469 const lastCompositionEndRef = useRef(0); useEffect(() => { const editor = editorRef.current; if (!editor) return; const handleCompositionEnd = () => { lastCompositionEndRef.current = Date.now(); }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key !== "Enter" || isInIMERef.current || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) { return; } // Safari fix: Ignore Enter key within 100ms of composition end // This prevents double-enter behavior when confirming IME input in lists if (Date.now() - lastCompositionEndRef.current < 100) { return; } const actions = editorActionsRef.current; const cursorPosition = actions.getCursorPosition(); const contentBeforeCursor = actions.getContent().substring(0, cursorPosition); const listInfo = detectLastListItem(contentBeforeCursor); if (!listInfo.type) return; event.preventDefault(); const lines = contentBeforeCursor.split("\n"); const currentLine = lines[lines.length - 1]; if (isEmptyListItem(currentLine)) { const lineStartPos = cursorPosition - currentLine.length; actions.removeText(lineStartPos, currentLine.length); } else { const continuation = generateListContinuation(listInfo); actions.insertText("\n" + continuation); // Auto-scroll to keep cursor visible after inserting list item setTimeout(() => actions.scrollToCursor(), 0); } }; editor.addEventListener("compositionend", handleCompositionEnd); editor.addEventListener("keydown", handleKeyDown); return () => { editor.removeEventListener("compositionend", handleCompositionEnd); editor.removeEventListener("keydown", handleKeyDown); }; }, []); } ================================================ FILE: web/src/components/MemoEditor/Editor/useSuggestions.ts ================================================ import { useEffect, useRef, useState } from "react"; import getCaretCoordinates from "textarea-caret"; import { EditorRefActions } from "."; export interface Position { left: number; top: number; height: number; } export interface UseSuggestionsOptions { editorRef: React.RefObject; editorActions: React.ForwardedRef; triggerChar: string; items: T[]; filterItems: (items: T[], searchQuery: string) => T[]; onAutocomplete: (item: T, word: string, startIndex: number, actions: EditorRefActions) => void; } export interface UseSuggestionsReturn { position: Position | null; suggestions: T[]; selectedIndex: number; isVisible: boolean; handleItemSelect: (item: T) => void; } export function useSuggestions({ editorRef, editorActions, triggerChar, items, filterItems, onAutocomplete, }: UseSuggestionsOptions): UseSuggestionsReturn { const [position, setPosition] = useState(null); const [selectedIndex, setSelectedIndex] = useState(0); const isProcessingRef = useRef(false); const selectedRef = useRef(selectedIndex); selectedRef.current = selectedIndex; const getCurrentWord = (): [word: string, startIndex: number] => { const editor = editorRef.current; if (!editor) return ["", 0]; const cursorPos = editor.selectionEnd; const before = editor.value.slice(0, cursorPos).match(/\S*$/) || { 0: "", index: cursorPos }; const after = editor.value.slice(cursorPos).match(/^\S*/) || { 0: "" }; return [before[0] + after[0], before.index ?? cursorPos]; }; const hide = () => setPosition(null); const suggestionsRef = useRef([]); suggestionsRef.current = (() => { const [word] = getCurrentWord(); if (!word.startsWith(triggerChar)) return []; const searchQuery = word.slice(triggerChar.length).toLowerCase(); return filterItems(items, searchQuery); })(); const isVisibleRef = useRef(false); isVisibleRef.current = !!(position && suggestionsRef.current.length > 0); const handleAutocomplete = (item: T) => { if (!editorActions || !("current" in editorActions) || !editorActions.current) { console.warn("useSuggestions: editorActions not available"); return; } isProcessingRef.current = true; const [word, index] = getCurrentWord(); onAutocomplete(item, word, index, editorActions.current); hide(); // Re-enable input handling after all DOM operations complete queueMicrotask(() => { isProcessingRef.current = false; }); }; const handleNavigation = (e: KeyboardEvent, selected: number, suggestionsCount: number) => { if (e.code === "ArrowDown") { setSelectedIndex((selected + 1) % suggestionsCount); e.preventDefault(); e.stopPropagation(); } else if (e.code === "ArrowUp") { setSelectedIndex((selected - 1 + suggestionsCount) % suggestionsCount); e.preventDefault(); e.stopPropagation(); } }; const handleKeyDown = (e: KeyboardEvent) => { if (!isVisibleRef.current) return; const suggestions = suggestionsRef.current; const selected = selectedRef.current; if (["Escape", "ArrowLeft", "ArrowRight"].includes(e.code)) { hide(); return; } if (["ArrowDown", "ArrowUp"].includes(e.code)) { handleNavigation(e, selected, suggestions.length); return; } if (["Enter", "Tab"].includes(e.code)) { handleAutocomplete(suggestions[selected]); e.preventDefault(); e.stopImmediatePropagation(); } }; const handleInput = () => { if (isProcessingRef.current) return; const editor = editorRef.current; if (!editor) return; setSelectedIndex(0); const [word, index] = getCurrentWord(); const currentChar = editor.value[editor.selectionEnd]; const isActive = word.startsWith(triggerChar) && currentChar !== triggerChar; if (isActive) { const coords = getCaretCoordinates(editor, index); coords.top -= editor.scrollTop; setPosition(coords); } else { hide(); } }; useEffect(() => { const editor = editorRef.current; if (!editor) return; const handlers = { click: hide, blur: hide, keydown: handleKeyDown, input: handleInput }; Object.entries(handlers).forEach(([event, handler]) => { editor.addEventListener(event, handler as EventListener); }); return () => { Object.entries(handlers).forEach(([event, handler]) => { editor.removeEventListener(event, handler as EventListener); }); }; }, []); return { position, suggestions: suggestionsRef.current, selectedIndex, isVisible: isVisibleRef.current, handleItemSelect: handleAutocomplete, }; } ================================================ FILE: web/src/components/MemoEditor/README.md ================================================ # MemoEditor Architecture ## Overview MemoEditor uses a three-layer architecture for better separation of concerns and testability. ## Architecture ``` ┌─────────────────────────────────────────┐ │ Presentation Layer (Components) │ │ - EditorToolbar, EditorContent, etc. │ └─────────────────┬───────────────────────┘ │ ┌─────────────────▼───────────────────────┐ │ State Layer (Reducer + Context) │ │ - state/, useEditorContext() │ └─────────────────┬───────────────────────┘ │ ┌─────────────────▼───────────────────────┐ │ Service Layer (Business Logic) │ │ - services/ (pure functions) │ └─────────────────────────────────────────┘ ``` ## Directory Structure ``` MemoEditor/ ├── state/ # State management (reducer, actions, context) ├── services/ # Business logic (pure functions) ├── components/ # UI components ├── hooks/ # React hooks (utilities) ├── Editor/ # Core editor component ├── Toolbar/ # Toolbar components ├── constants.ts └── types/ ``` ## Key Concepts ### State Management Uses `useReducer` + Context for predictable state transitions. All state changes go through action creators. ### Services Pure TypeScript functions containing business logic. No React hooks, easy to test. ### Components Thin presentation components that dispatch actions and render UI. ## Usage ```typescript import MemoEditor from "@/components/MemoEditor"; console.log('Saved:', name)} onCancel={() => console.log('Cancelled')} /> ``` ## Testing Services are pure functions - easy to unit test without React. ```typescript const state = mockEditorState(); const result = await memoService.save(state, { memoName: 'memos/123' }); ``` ================================================ FILE: web/src/components/MemoEditor/Toolbar/InsertMenu.tsx ================================================ import { LatLng } from "leaflet"; import { uniqBy } from "lodash-es"; import { FileIcon, LinkIcon, LoaderIcon, type LucideIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useDebounce } from "react-use"; import { useReverseGeocoding } from "@/components/map"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, useDropdownMenuSubHoverDelay, } from "@/components/ui/dropdown-menu"; import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb"; import { useTranslate } from "@/utils/i18n"; import { LinkMemoDialog, LocationDialog } from "../components"; import { useFileUpload, useLinkMemo, useLocation } from "../hooks"; import { useEditorContext } from "../state"; import type { InsertMenuProps } from "../types"; import type { LocalFile } from "../types/attachment"; const InsertMenu = (props: InsertMenuProps) => { const t = useTranslate(); const { state, actions, dispatch } = useEditorContext(); const { location: initialLocation, onLocationChange, onToggleFocusMode, isUploading: isUploadingProp } = props; const [linkDialogOpen, setLinkDialogOpen] = useState(false); const [locationDialogOpen, setLocationDialogOpen] = useState(false); const [moreSubmenuOpen, setMoreSubmenuOpen] = useState(false); const { handleTriggerEnter, handleTriggerLeave, handleContentEnter, handleContentLeave } = useDropdownMenuSubHoverDelay( 150, setMoreSubmenuOpen, ); const { fileInputRef, selectingFlag, handleFileInputChange, handleUploadClick } = useFileUpload((newFiles: LocalFile[]) => { newFiles.forEach((file) => dispatch(actions.addLocalFile(file))); }); const linkMemo = useLinkMemo({ isOpen: linkDialogOpen, currentMemoName: props.memoName, existingRelations: state.metadata.relations, onAddRelation: (relation: MemoRelation) => { dispatch(actions.setMetadata({ relations: uniqBy([...state.metadata.relations, relation], (r) => r.relatedMemo?.name) })); setLinkDialogOpen(false); }, }); const location = useLocation(props.location); const [debouncedPosition, setDebouncedPosition] = useState(undefined); useDebounce( () => { setDebouncedPosition(location.state.position); }, 1000, [location.state.position], ); const { data: displayName } = useReverseGeocoding(debouncedPosition?.lat, debouncedPosition?.lng); useEffect(() => { if (displayName) { location.setPlaceholder(displayName); } }, [displayName]); const isUploading = selectingFlag || isUploadingProp; const handleOpenLinkDialog = useCallback(() => { setLinkDialogOpen(true); }, []); const handleLocationClick = useCallback(() => { setLocationDialogOpen(true); if (!initialLocation && !location.locationInitialized) { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { location.handlePositionChange(new LatLng(position.coords.latitude, position.coords.longitude)); }, (error) => { console.error("Geolocation error:", error); }, ); } } }, [initialLocation, location]); const handleLocationConfirm = useCallback(() => { const newLocation = location.getLocation(); if (newLocation) { onLocationChange(newLocation); setLocationDialogOpen(false); } }, [location, onLocationChange]); const handleLocationCancel = useCallback(() => { location.reset(); setLocationDialogOpen(false); }, [location]); const handlePositionChange = useCallback( (position: LatLng) => { location.handlePositionChange(position); }, [location], ); const handleToggleFocusMode = useCallback(() => { onToggleFocusMode?.(); setMoreSubmenuOpen(false); }, [onToggleFocusMode]); const menuItems = useMemo( () => [ { key: "upload", label: t("common.upload"), icon: FileIcon, onClick: handleUploadClick, }, { key: "link", label: t("tooltip.link-memo"), icon: LinkIcon, onClick: handleOpenLinkDialog, }, { key: "location", label: t("tooltip.select-location"), icon: MapPinIcon, onClick: handleLocationClick, }, ] satisfies Array<{ key: string; label: string; icon: LucideIcon; onClick: () => void }>, [handleLocationClick, handleOpenLinkDialog, handleUploadClick, t], ); return ( <> {menuItems.map((item) => ( {item.label} ))} {/* View submenu with Focus Mode */} {t("common.more")} {t("editor.focus-mode")}
{t("editor.slash-commands")}
{/* Hidden file input */} ); }; export default InsertMenu; ================================================ FILE: web/src/components/MemoEditor/Toolbar/VisibilitySelector.tsx ================================================ import { CheckIcon, ChevronDownIcon } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import VisibilityIcon from "@/components/VisibilityIcon"; import { Visibility } from "@/types/proto/api/v1/memo_service_pb"; import { useTranslate } from "@/utils/i18n"; import type { VisibilitySelectorProps } from "../types"; const VisibilitySelector = (props: VisibilitySelectorProps) => { const { value, onChange } = props; const t = useTranslate(); const visibilityOptions = [ { value: Visibility.PRIVATE, label: t("memo.visibility.private") }, { value: Visibility.PROTECTED, label: t("memo.visibility.protected") }, { value: Visibility.PUBLIC, label: t("memo.visibility.public") }, ] as const; const currentLabel = visibilityOptions.find((option) => option.value === value)?.label || ""; return ( {visibilityOptions.map((option) => ( onChange(option.value)}> {option.label} {value === option.value && } ))} ); }; export default VisibilitySelector; ================================================ FILE: web/src/components/MemoEditor/Toolbar/index.ts ================================================ // Toolbar components for MemoEditor export { default as InsertMenu } from "./InsertMenu"; export { default as VisibilitySelector } from "./VisibilitySelector"; ================================================ FILE: web/src/components/MemoEditor/components/AttachmentList.tsx ================================================ import { ChevronDownIcon, ChevronUpIcon, FileIcon, PaperclipIcon, XIcon } from "lucide-react"; import type { FC } from "react"; import { cn } from "@/lib/utils"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import { formatFileSize, getFileTypeLabel } from "@/utils/format"; import type { LocalFile } from "../types/attachment"; import { toAttachmentItems } from "../types/attachment"; interface AttachmentListProps { attachments: Attachment[]; localFiles?: LocalFile[]; onAttachmentsChange?: (attachments: Attachment[]) => void; onRemoveLocalFile?: (previewUrl: string) => void; } const AttachmentItemCard: FC<{ item: ReturnType[0]; onRemove?: () => void; onMoveUp?: () => void; onMoveDown?: () => void; canMoveUp?: boolean; canMoveDown?: boolean; }> = ({ item, onRemove, onMoveUp, onMoveDown, canMoveUp = true, canMoveDown = true }) => { const { category, filename, thumbnailUrl, mimeType, size } = item; const fileTypeLabel = getFileTypeLabel(mimeType); const fileSizeLabel = size ? formatFileSize(size) : undefined; return (
{category === "image" && thumbnailUrl ? ( ) : ( )}
{filename}
{fileTypeLabel} {fileSizeLabel && ( <> {fileSizeLabel} )}
{onMoveUp && ( )} {onMoveDown && ( )} {onRemove && ( )}
); }; const AttachmentList: FC = ({ attachments, localFiles = [], onAttachmentsChange, onRemoveLocalFile }) => { if (attachments.length === 0 && localFiles.length === 0) { return null; } const items = toAttachmentItems(attachments, localFiles); const handleMoveUp = (index: number) => { if (index === 0 || !onAttachmentsChange) return; const newAttachments = [...attachments]; [newAttachments[index - 1], newAttachments[index]] = [newAttachments[index], newAttachments[index - 1]]; onAttachmentsChange(newAttachments); }; const handleMoveDown = (index: number) => { if (index === attachments.length - 1 || !onAttachmentsChange) return; const newAttachments = [...attachments]; [newAttachments[index], newAttachments[index + 1]] = [newAttachments[index + 1], newAttachments[index]]; onAttachmentsChange(newAttachments); }; const handleRemoveAttachment = (name: string) => { if (onAttachmentsChange) { onAttachmentsChange(attachments.filter((attachment) => attachment.name !== name)); } }; const handleRemoveItem = (item: (typeof items)[0]) => { if (item.isLocal) { onRemoveLocalFile?.(item.id); } else { handleRemoveAttachment(item.id); } }; return (
Attachments ({items.length})
{items.map((item) => { const isLocalFile = item.isLocal; const attachmentIndex = isLocalFile ? -1 : attachments.findIndex((a) => a.name === item.id); return ( handleRemoveItem(item)} onMoveUp={!isLocalFile ? () => handleMoveUp(attachmentIndex) : undefined} onMoveDown={!isLocalFile ? () => handleMoveDown(attachmentIndex) : undefined} canMoveUp={!isLocalFile && attachmentIndex > 0} canMoveDown={!isLocalFile && attachmentIndex < attachments.length - 1} /> ); })}
); }; export default AttachmentList; ================================================ FILE: web/src/components/MemoEditor/components/EditorContent.tsx ================================================ import { forwardRef } from "react"; import Editor, { type EditorRefActions } from "../Editor"; import { useBlobUrls, useDragAndDrop } from "../hooks"; import { useEditorContext } from "../state"; import type { EditorContentProps } from "../types"; import type { LocalFile } from "../types/attachment"; export const EditorContent = forwardRef(({ placeholder }, ref) => { const { state, actions, dispatch } = useEditorContext(); const { createBlobUrl } = useBlobUrls(); const { dragHandlers } = useDragAndDrop((files: FileList) => { const localFiles: LocalFile[] = Array.from(files).map((file) => ({ file, previewUrl: createBlobUrl(file), })); localFiles.forEach((localFile) => dispatch(actions.addLocalFile(localFile))); }); const handleCompositionStart = () => { dispatch(actions.setComposing(true)); }; const handleCompositionEnd = () => { dispatch(actions.setComposing(false)); }; const handleContentChange = (content: string) => { dispatch(actions.updateContent(content)); }; const handlePaste = (event: React.ClipboardEvent) => { const clipboard = event.clipboardData; if (!clipboard) return; const files: File[] = []; if (clipboard.items && clipboard.items.length > 0) { for (const item of Array.from(clipboard.items)) { if (item.kind !== "file") continue; const file = item.getAsFile(); if (file) files.push(file); } } else if (clipboard.files && clipboard.files.length > 0) { files.push(...Array.from(clipboard.files)); } if (files.length === 0) return; const localFiles: LocalFile[] = files.map((file) => ({ file, previewUrl: createBlobUrl(file), })); localFiles.forEach((localFile) => dispatch(actions.addLocalFile(localFile))); event.preventDefault(); }; return (
); }); EditorContent.displayName = "EditorContent"; ================================================ FILE: web/src/components/MemoEditor/components/EditorMetadata.tsx ================================================ import type { FC } from "react"; import { useEditorContext } from "../state"; import type { EditorMetadataProps } from "../types"; import AttachmentList from "./AttachmentList"; import LocationDisplay from "./LocationDisplay"; import RelationList from "./RelationList"; export const EditorMetadata: FC = ({ memoName }) => { const { state, actions, dispatch } = useEditorContext(); return (
dispatch(actions.setMetadata({ attachments }))} onRemoveLocalFile={(previewUrl) => dispatch(actions.removeLocalFile(previewUrl))} /> dispatch(actions.setMetadata({ relations }))} memoName={memoName} /> {state.metadata.location && ( dispatch(actions.setMetadata({ location: undefined }))} /> )}
); }; ================================================ FILE: web/src/components/MemoEditor/components/EditorToolbar.tsx ================================================ import type { FC } from "react"; import { Button } from "@/components/ui/button"; import { useTranslate } from "@/utils/i18n"; import { validationService } from "../services"; import { useEditorContext } from "../state"; import InsertMenu from "../Toolbar/InsertMenu"; import VisibilitySelector from "../Toolbar/VisibilitySelector"; import type { EditorToolbarProps } from "../types"; export const EditorToolbar: FC = ({ onSave, onCancel, memoName }) => { const t = useTranslate(); const { state, actions, dispatch } = useEditorContext(); const { valid } = validationService.canSave(state); const isSaving = state.ui.isLoading.saving; const handleLocationChange = (location: typeof state.metadata.location) => { dispatch(actions.setMetadata({ location })); }; const handleToggleFocusMode = () => { dispatch(actions.toggleFocusMode()); }; const handleVisibilityChange = (visibility: typeof state.metadata.visibility) => { dispatch(actions.setMetadata({ visibility })); }; return (
{onCancel && ( )}
); }; ================================================ FILE: web/src/components/MemoEditor/components/FocusModeOverlay.tsx ================================================ import { Minimize2Icon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { FOCUS_MODE_STYLES } from "../constants"; import type { FocusModeExitButtonProps, FocusModeOverlayProps } from "../types"; export function FocusModeOverlay({ isActive, onToggle }: FocusModeOverlayProps) { if (!isActive) return null; return ); } ================================================ FILE: web/src/components/MemoEditor/components/LinkMemoDialog.tsx ================================================ import { timestampDate } from "@bufbuild/protobuf/wkt"; import { LinkIcon } from "lucide-react"; import { MemoPreview } from "@/components/MemoPreview"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { VisuallyHidden } from "@/components/ui/visually-hidden"; import { extractMemoIdFromName } from "@/helpers/resource-names"; import { cn } from "@/lib/utils"; import { useTranslate } from "@/utils/i18n"; import type { LinkMemoDialogProps } from "../types"; export const LinkMemoDialog = ({ open, onOpenChange, searchText, onSearchChange, filteredMemos, isFetching, onSelectMemo, isAlreadyLinked, }: LinkMemoDialogProps) => { const t = useTranslate(); return ( {t("tooltip.link-memo")} Search and select a memo to link
onSearchChange(e.target.value)} className="!text-sm h-9" autoFocus />
{filteredMemos.length === 0 ? (
{isFetching ? "Loading..." : t("reference.no-memos-found")}
) : ( filteredMemos.map((memo) => { const alreadyLinked = isAlreadyLinked(memo.name); return (
!alreadyLinked && onSelectMemo(memo)} >
{alreadyLinked && } {extractMemoIdFromName(memo.name).slice(0, 6)} {memo.displayTime && timestampDate(memo.displayTime).toLocaleString()}
); }) )}
); }; ================================================ FILE: web/src/components/MemoEditor/components/LocationDialog.tsx ================================================ import { LocationPicker } from "@/components/map"; import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { VisuallyHidden } from "@/components/ui/visually-hidden"; import { useTranslate } from "@/utils/i18n"; import type { LocationDialogProps } from "../types"; export const LocationDialog = ({ open, onOpenChange, state, locationInitialized: _locationInitialized, onPositionChange, onUpdateCoordinate, onPlaceholderChange, onCancel, onConfirm, }: LocationDialogProps) => { const t = useTranslate(); const { placeholder, position, latInput, lngInput } = state; return ( {t("tooltip.select-location")} Select a location on the map or enter coordinates manually
onUpdateCoordinate("lat", e.target.value)} className="h-9" />
onUpdateCoordinate("lng", e.target.value)} className="h-9" />