Repository: apache/answer Branch: main Commit: fca80abbaf38 Files: 1094 Total size: 7.8 MB Directory structure: gitextract_8j8m3hg4/ ├── .asf.yaml ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ ├── enhancement_request.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE/ │ │ └── pull_request_template.md │ └── workflows/ │ ├── build-binary-for-release.yml │ ├── build-image-for-latest-release.yml │ ├── build-image-for-manual.yml │ ├── build-image-for-release.yml │ ├── build-image-for-test.yml │ ├── check-asf-header.yml │ └── lint.yml ├── .gitignore ├── .gitlab-ci.yml ├── .golangci.yaml ├── .goreleaser.yaml ├── .vaunt/ │ └── config.yaml ├── .vscode/ │ └── settings.json ├── Dockerfile ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── build/ │ └── README.md ├── charts/ │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates/ │ │ ├── _helpers.tpl │ │ ├── deployment.yaml │ │ ├── hpa.yaml │ │ ├── ingress.yaml │ │ ├── pvc.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ └── values.yaml ├── cmd/ │ ├── answer/ │ │ └── main.go │ ├── command.go │ ├── main.go │ ├── wire.go │ └── wire_gen.go ├── configs/ │ ├── config.go │ ├── config.yaml │ ├── path_ignore.yaml │ └── reserved-usernames.json ├── crowdin.yml ├── docker-compose.yaml ├── docs/ │ ├── docs.go │ ├── release/ │ │ ├── LICENSE │ │ ├── NOTICE │ │ └── licenses/ │ │ ├── LICENSE-JedWatson-classnames.txt │ │ ├── LICENSE-Machiel-slugify.txt │ │ ├── LICENSE-Masterminds-semver.txt │ │ ├── LICENSE-Qix--color.txt │ │ ├── LICENSE-anargu-gin-brotli.txt │ │ ├── LICENSE-asaskevich-govalidator.txt │ │ ├── LICENSE-axios-axios.txt │ │ ├── LICENSE-bwmarrin-snowflake.txt │ │ ├── LICENSE-codemirror-basic-setup.txt │ │ ├── LICENSE-codemirror-lang-markdown.txt │ │ ├── LICENSE-codemirror-language-data.txt │ │ ├── LICENSE-codemirror-state.txt │ │ ├── LICENSE-codemirror-view.txt │ │ ├── LICENSE-cznic-sqlite.txt │ │ ├── LICENSE-disintegration-imaging.txt │ │ ├── LICENSE-emn178-js-sha256.txt │ │ ├── LICENSE-facebook-react.txt │ │ ├── LICENSE-gin-gonic-gin.txt │ │ ├── LICENSE-go-gomail-gomail.txt │ │ ├── LICENSE-go-playground-locales.txt │ │ ├── LICENSE-go-playground-universal-translator.txt │ │ ├── LICENSE-go-playground-validator.txt │ │ ├── LICENSE-go-resty-resty.txt │ │ ├── LICENSE-go-sql-driver-mysql.txt │ │ ├── LICENSE-go-yaml-yaml.txt │ │ ├── LICENSE-goccy-go-json.txt │ │ ├── LICENSE-golang-org-x.txt │ │ ├── LICENSE-google-uuid.txt │ │ ├── LICENSE-google-wire.txt │ │ ├── LICENSE-grokify-html-strip-tags-go.txt │ │ ├── LICENSE-i18next-i18next.txt │ │ ├── LICENSE-i18next-react-i18next.txt │ │ ├── LICENSE-iamkun-dayjs.txt │ │ ├── LICENSE-jinzhu-copier.txt │ │ ├── LICENSE-jinzhu-now.txt │ │ ├── LICENSE-joho-godotenv.txt │ │ ├── LICENSE-jordan-wright-email.txt │ │ ├── LICENSE-jxson-front-matter.txt │ │ ├── LICENSE-kpdecker-jsdiff.txt │ │ ├── LICENSE-lib-pq.txt │ │ ├── LICENSE-ljharb-qs.txt │ │ ├── LICENSE-lodash-lodash.txt │ │ ├── LICENSE-mark3labs-mcp-go.txt │ │ ├── LICENSE-markedjs-marked.txt │ │ ├── LICENSE-mattn-go-sqlite3.txt │ │ ├── LICENSE-microcosm-cc-bluemonday.txt │ │ ├── LICENSE-mojocn-base64Captcha.txt │ │ ├── LICENSE-mozillazg-go-pinyin.txt │ │ ├── LICENSE-npm-node-semver.txt │ │ ├── LICENSE-ory-dockertest.txt │ │ ├── LICENSE-pmndrs-zustand.txt │ │ ├── LICENSE-react-bootstrap-react-bootstrap.txt │ │ ├── LICENSE-remix-run-react-router.txt │ │ ├── LICENSE-robfig-cron.txt │ │ ├── LICENSE-sashabaranov-go-openai.txt │ │ ├── LICENSE-scottleedavis-go-exif-remove.txt │ │ ├── LICENSE-segmentfault-pacman.txt │ │ ├── LICENSE-soldair-qrcode.txt │ │ ├── LICENSE-spf13-cobra.txt │ │ ├── LICENSE-staylor-react-helmet-async.txt │ │ ├── LICENSE-stretchr-testify.txt │ │ ├── LICENSE-sudodoki-copy-to-clipboard.txt │ │ ├── LICENSE-swaggo-files.txt │ │ ├── LICENSE-swaggo-gin-swagger.txt │ │ ├── LICENSE-swaggo-swag.txt │ │ ├── LICENSE-tidwall-gjson.txt │ │ ├── LICENSE-twbs-bootstrap.txt │ │ ├── LICENSE-twbs-icons.txt │ │ ├── LICENSE-uber-go-mock.txt │ │ ├── LICENSE-vercel-swr.txt │ │ ├── LICENSE-xorm.txt │ │ ├── LICENSE-yuin-goldmark.txt │ │ └── LIcENSE-Bunlong-next-share.txt │ ├── swagger.json │ └── swagger.yaml ├── go.mod ├── go.sum ├── i18n/ │ ├── af_ZA.yaml │ ├── ar_SA.yaml │ ├── az_AZ.yaml │ ├── bal_BA.yaml │ ├── ban_ID.yaml │ ├── bn_BD.yaml │ ├── bs_BA.yaml │ ├── ca_ES.yaml │ ├── cs_CZ.yaml │ ├── cy_GB.yaml │ ├── da_DK.yaml │ ├── de_DE.yaml │ ├── el_GR.yaml │ ├── en_US.yaml │ ├── es_ES.yaml │ ├── fa_IR.yaml │ ├── fi_FI.yaml │ ├── fr_FR.yaml │ ├── he_IL.yaml │ ├── hi_IN.yaml │ ├── hu_HU.yaml │ ├── hy_AM.yaml │ ├── i18n.go │ ├── i18n.yaml │ ├── id_ID.yaml │ ├── it_IT.yaml │ ├── ja_JP.yaml │ ├── ko_KR.yaml │ ├── ml_IN.yaml │ ├── nl_NL.yaml │ ├── no_NO.yaml │ ├── pl_PL.yaml │ ├── pt_BR.yaml │ ├── pt_PT.yaml │ ├── ro_RO.yaml │ ├── ru_RU.yaml │ ├── sk_SK.yaml │ ├── sq_AL.yaml │ ├── sr_SP.yaml │ ├── sv_SE.yaml │ ├── te_IN.yaml │ ├── tr_TR.yaml │ ├── uk_UA.yaml │ ├── vi_VN.yaml │ ├── zh_CN.yaml │ └── zh_TW.yaml ├── internal/ │ ├── base/ │ │ ├── conf/ │ │ │ └── conf.go │ │ ├── constant/ │ │ │ ├── acticity.go │ │ │ ├── ai_config.go │ │ │ ├── cache_key.go │ │ │ ├── comment.go │ │ │ ├── constant.go │ │ │ ├── ctx_flag.go │ │ │ ├── email_tpl_key.go │ │ │ ├── event.go │ │ │ ├── meta.go │ │ │ ├── notification.go │ │ │ ├── object_type.go │ │ │ ├── plugin_config_key.go │ │ │ ├── privilege.go │ │ │ ├── question.go │ │ │ ├── reason.go │ │ │ ├── revision.go │ │ │ ├── site_info.go │ │ │ ├── site_type.go │ │ │ ├── upload.go │ │ │ └── user.go │ │ ├── cron/ │ │ │ ├── cron.go │ │ │ └── provider.go │ │ ├── data/ │ │ │ ├── config.go │ │ │ └── data.go │ │ ├── handler/ │ │ │ ├── handler.go │ │ │ ├── lang.go │ │ │ ├── response.go │ │ │ └── short_id.go │ │ ├── middleware/ │ │ │ ├── accept_language.go │ │ │ ├── api_key_auth.go │ │ │ ├── auth.go │ │ │ ├── avatar.go │ │ │ ├── header.go │ │ │ ├── mcp_auth.go │ │ │ ├── provider.go │ │ │ ├── rate_limit.go │ │ │ ├── short_id.go │ │ │ ├── user_center_plugin_auth.go │ │ │ └── visit_img_auth.go │ │ ├── pager/ │ │ │ ├── pager.go │ │ │ └── pagination.go │ │ ├── path/ │ │ │ └── path.go │ │ ├── queue/ │ │ │ ├── queue.go │ │ │ └── queue_test.go │ │ ├── reason/ │ │ │ ├── privilege.go │ │ │ └── reason.go │ │ ├── server/ │ │ │ ├── config.go │ │ │ ├── http.go │ │ │ ├── http_funcmap.go │ │ │ └── provider.go │ │ ├── translator/ │ │ │ ├── config.go │ │ │ └── provider.go │ │ └── validator/ │ │ └── validator.go │ ├── cli/ │ │ ├── build.go │ │ ├── config.go │ │ ├── dump.go │ │ ├── i18n.go │ │ ├── install.go │ │ ├── install_check.go │ │ └── reset_password.go │ ├── controller/ │ │ ├── activity_controller.go │ │ ├── ai_controller.go │ │ ├── ai_conversation_controller.go │ │ ├── answer_controller.go │ │ ├── badge_controller.go │ │ ├── collection_controller.go │ │ ├── comment_controller.go │ │ ├── connector_controller.go │ │ ├── controller.go │ │ ├── dashboard_controller.go │ │ ├── embed_controller.go │ │ ├── follow_controller.go │ │ ├── lang_controller.go │ │ ├── mcp_controller.go │ │ ├── meta_controller.go │ │ ├── notification_controller.go │ │ ├── permission_controller.go │ │ ├── plugin_captcha_controller.go │ │ ├── plugin_sidebar_controller.go │ │ ├── plugin_user_center_controller.go │ │ ├── question_controller.go │ │ ├── rank_controller.go │ │ ├── reason_controller.go │ │ ├── render_controller.go │ │ ├── report_controller.go │ │ ├── review_controller.go │ │ ├── revision_controller.go │ │ ├── search_controller.go │ │ ├── siteinfo_controller.go │ │ ├── tag_controller.go │ │ ├── template_controller.go │ │ ├── template_render/ │ │ │ ├── answer.go │ │ │ ├── comment.go │ │ │ ├── controller.go │ │ │ ├── question.go │ │ │ ├── tags.go │ │ │ └── userinfo.go │ │ ├── upload_controller.go │ │ ├── user_controller.go │ │ ├── user_plugin_controller.go │ │ └── vote_controller.go │ ├── controller_admin/ │ │ ├── ai_conversation_admin_controller.go │ │ ├── badge_controller.go │ │ ├── controller.go │ │ ├── e_api_key_controller.go │ │ ├── plugin_controller.go │ │ ├── role_controller.go │ │ ├── siteinfo_controller.go │ │ ├── theme_controller.go │ │ └── user_backyard_controller.go │ ├── entity/ │ │ ├── activity_entity.go │ │ ├── ai_conversation.go │ │ ├── ai_conversation_record.go │ │ ├── answer_entity.go │ │ ├── api_key_entity.go │ │ ├── auth_user_entity.go │ │ ├── badge_award_entity.go │ │ ├── badge_entity.go │ │ ├── badge_group_entity.go │ │ ├── captcha_entity.go │ │ ├── collection_entity.go │ │ ├── collection_group_entity.go │ │ ├── comment_entity.go │ │ ├── config_entity.go │ │ ├── file_record_entity.go │ │ ├── meta_entity.go │ │ ├── notification_entity.go │ │ ├── plugin_config_entity.go │ │ ├── plugin_kv_storage_entity.go │ │ ├── plugin_user_config_entity.go │ │ ├── power_entity.go │ │ ├── question_entity.go │ │ ├── question_link_entity.go │ │ ├── report_entity.go │ │ ├── review_entity.go │ │ ├── revision_entity.go │ │ ├── role_entity.go │ │ ├── role_power_rel_entity.go │ │ ├── site_info.go │ │ ├── tag_entity.go │ │ ├── tag_rel_entity.go │ │ ├── uniqid_entity.go │ │ ├── user_entity.go │ │ ├── user_external_login_entity.go │ │ ├── user_notification_config_entity.go │ │ ├── user_role_rel_entity.go │ │ └── version_entity.go │ ├── install/ │ │ ├── install_controller.go │ │ ├── install_from_env.go │ │ ├── install_main.go │ │ ├── install_req.go │ │ └── install_server.go │ ├── migrations/ │ │ ├── init.go │ │ ├── init_data.go │ │ ├── migrations.go │ │ ├── v1.go │ │ ├── v10.go │ │ ├── v11.go │ │ ├── v12.go │ │ ├── v13.go │ │ ├── v14.go │ │ ├── v15.go │ │ ├── v16.go │ │ ├── v17.go │ │ ├── v18.go │ │ ├── v19.go │ │ ├── v2.go │ │ ├── v20.go │ │ ├── v21.go │ │ ├── v22.go │ │ ├── v23.go │ │ ├── v24.go │ │ ├── v25.go │ │ ├── v26.go │ │ ├── v27.go │ │ ├── v28.go │ │ ├── v29.go │ │ ├── v3.go │ │ ├── v30.go │ │ ├── v31.go │ │ ├── v4.go │ │ ├── v5.go │ │ ├── v6.go │ │ ├── v7.go │ │ ├── v8.go │ │ └── v9.go │ ├── repo/ │ │ ├── activity/ │ │ │ ├── activity_repo.go │ │ │ ├── answer_repo.go │ │ │ ├── follow_repo.go │ │ │ ├── review_repo.go │ │ │ ├── user_active_repo.go │ │ │ └── vote_repo.go │ │ ├── activity_common/ │ │ │ ├── activity_repo.go │ │ │ ├── follow.go │ │ │ └── vote.go │ │ ├── ai_conversation/ │ │ │ └── ai_conversation_repo.go │ │ ├── answer/ │ │ │ └── answer_repo.go │ │ ├── api_key/ │ │ │ └── api_key_repo.go │ │ ├── auth/ │ │ │ └── auth.go │ │ ├── badge/ │ │ │ ├── badge_event_rule.go │ │ │ └── badge_repo.go │ │ ├── badge_award/ │ │ │ └── badge_award_repo.go │ │ ├── badge_group/ │ │ │ └── badge_group_repo.go │ │ ├── captcha/ │ │ │ └── captcha.go │ │ ├── collection/ │ │ │ ├── collection_group_repo.go │ │ │ └── collection_repo.go │ │ ├── comment/ │ │ │ └── comment_repo.go │ │ ├── config/ │ │ │ └── config_repo.go │ │ ├── export/ │ │ │ └── email_repo.go │ │ ├── file_record/ │ │ │ └── file_record_repo.go │ │ ├── limit/ │ │ │ └── limit.go │ │ ├── meta/ │ │ │ └── meta_repo.go │ │ ├── notification/ │ │ │ └── notification_repo.go │ │ ├── plugin_config/ │ │ │ ├── plugin_config_repo.go │ │ │ └── plugin_user_config_repo.go │ │ ├── provider.go │ │ ├── question/ │ │ │ └── question_repo.go │ │ ├── rank/ │ │ │ └── user_rank_repo.go │ │ ├── reason/ │ │ │ └── reason_repo.go │ │ ├── repo_test/ │ │ │ ├── auth_test.go │ │ │ ├── captcha_test.go │ │ │ ├── comment_repo_test.go │ │ │ ├── email_repo_test.go │ │ │ ├── meta_repo_test.go │ │ │ ├── notification_repo_test.go │ │ │ ├── reason_repo_test.go │ │ │ ├── recommend_test.go │ │ │ ├── repo_main_test.go │ │ │ ├── revision_repo_test.go │ │ │ ├── siteinfo_repo_test.go │ │ │ ├── tag_rel_repo_test.go │ │ │ ├── tag_repo_test.go │ │ │ ├── user_backyard_repo_test.go │ │ │ └── user_repo_test.go │ │ ├── report/ │ │ │ └── report_repo.go │ │ ├── review/ │ │ │ └── review_repo.go │ │ ├── revision/ │ │ │ └── revision_repo.go │ │ ├── role/ │ │ │ ├── power_repo.go │ │ │ ├── role_power_rel_repo.go │ │ │ ├── role_repo.go │ │ │ └── user_role_rel_repo.go │ │ ├── search_common/ │ │ │ └── search_repo.go │ │ ├── search_sync/ │ │ │ └── search_sync.go │ │ ├── site_info/ │ │ │ └── siteinfo_repo.go │ │ ├── tag/ │ │ │ ├── tag_rel_repo.go │ │ │ └── tag_repo.go │ │ ├── tag_common/ │ │ │ └── tag_common_repo.go │ │ ├── unique/ │ │ │ └── uniqid_repo.go │ │ ├── user/ │ │ │ ├── user_backyard_repo.go │ │ │ └── user_repo.go │ │ ├── user_external_login/ │ │ │ └── user_external_login_repo.go │ │ └── user_notification_config/ │ │ └── user_notification_config_repo.go │ ├── router/ │ │ ├── answer_api_router.go │ │ ├── config.go │ │ ├── mcp_router.go │ │ ├── plugin_api_router.go │ │ ├── provider.go │ │ ├── static_router.go │ │ ├── swagger_router.go │ │ ├── template_router.go │ │ ├── ui.go │ │ └── ui_test.go │ ├── schema/ │ │ ├── activity.go │ │ ├── ai_config_schema.go │ │ ├── ai_conversation_schema.go │ │ ├── answer_activity_schema.go │ │ ├── answer_schema.go │ │ ├── api_key_schema.go │ │ ├── backyard_user_schema.go │ │ ├── badge_schema.go │ │ ├── collection_group_schema.go │ │ ├── comment_schema.go │ │ ├── config_schema.go │ │ ├── connector_schema.go │ │ ├── dashboard_schema.go │ │ ├── email_template.go │ │ ├── err_schema.go │ │ ├── event_schema.go │ │ ├── follow_schema.go │ │ ├── forbidden_schema.go │ │ ├── mcp_schema.go │ │ ├── mcp_tools/ │ │ │ └── mcp_tools.go │ │ ├── meta_schema.go │ │ ├── new_question_queue_schema.go │ │ ├── notification_schema.go │ │ ├── permission.go │ │ ├── plugin_admin_schema.go │ │ ├── plugin_user_center.go │ │ ├── plugin_user_schema.go │ │ ├── question_schema.go │ │ ├── rank_schema.go │ │ ├── reason_schema.go │ │ ├── render_schema.go │ │ ├── report_schema.go │ │ ├── review_schema.go │ │ ├── revision_schema.go │ │ ├── role_schema.go │ │ ├── search_schema.go │ │ ├── search_schema_test.go │ │ ├── simple_obj_info_schema.go │ │ ├── siteinfo_schema.go │ │ ├── sitemap_schema.go │ │ ├── tag_list_schema.go │ │ ├── tag_schema.go │ │ ├── template_schema.go │ │ ├── theme_schema.go │ │ ├── user_external_login_schema.go │ │ ├── user_notification_schema.go │ │ ├── user_schema.go │ │ └── vote_schema.go │ └── service/ │ ├── action/ │ │ ├── captcha_service.go │ │ └── captcha_strategy.go │ ├── activity/ │ │ ├── activity.go │ │ ├── answer_activity_service.go │ │ ├── review_active.go │ │ └── user_active.go │ ├── activity_common/ │ │ ├── activity.go │ │ ├── follow.go │ │ └── vote.go │ ├── activity_type/ │ │ └── activity_type.go │ ├── activityqueue/ │ │ └── activity_queue.go │ ├── ai_conversation/ │ │ └── ai_conversation_service.go │ ├── answer_common/ │ │ └── answer.go │ ├── apikey/ │ │ └── apikey_service.go │ ├── auth/ │ │ └── auth.go │ ├── badge/ │ │ ├── badge_award_service.go │ │ ├── badge_event_handler.go │ │ ├── badge_group_service.go │ │ └── badge_service.go │ ├── collection/ │ │ ├── collection_group_service.go │ │ └── collection_service.go │ ├── collection_common/ │ │ └── collection.go │ ├── comment/ │ │ └── comment_service.go │ ├── comment_common/ │ │ └── comment_service.go │ ├── config/ │ │ └── config_service.go │ ├── content/ │ │ ├── answer_service.go │ │ ├── question_hottest_service.go │ │ ├── question_service.go │ │ ├── revision_service.go │ │ ├── search_service.go │ │ ├── user_service.go │ │ └── vote_service.go │ ├── dashboard/ │ │ ├── dashboard_service.go │ │ └── dashboard_test.go │ ├── eventqueue/ │ │ └── event_queue.go │ ├── export/ │ │ └── email_service.go │ ├── feature_toggle/ │ │ └── feature_toggle_service.go │ ├── file_record/ │ │ └── file_record_service.go │ ├── follow/ │ │ └── follow_service.go │ ├── importer/ │ │ └── importer_service.go │ ├── meta/ │ │ └── meta_service.go │ ├── meta_common/ │ │ └── meta_common_service.go │ ├── mock/ │ │ └── siteinfo_repo_mock.go │ ├── noticequeue/ │ │ └── notice_queue.go │ ├── notification/ │ │ ├── external_notification.go │ │ ├── invite_answer_notification.go │ │ ├── new_answer_notification.go │ │ ├── new_comment_notification.go │ │ ├── new_question_notification.go │ │ └── notification_service.go │ ├── notification_common/ │ │ └── notification.go │ ├── object_info/ │ │ └── object_info.go │ ├── permission/ │ │ ├── answer_permission.go │ │ ├── comment_permission.go │ │ ├── permission_name.go │ │ ├── question_permission.go │ │ └── tag_permission.go │ ├── plugin_common/ │ │ └── plugin_common_service.go │ ├── provider.go │ ├── question_common/ │ │ └── question.go │ ├── rank/ │ │ └── rank_service.go │ ├── reason/ │ │ └── reason_service.go │ ├── reason_common/ │ │ └── reason.go │ ├── report/ │ │ └── report_service.go │ ├── report_common/ │ │ └── report_common.go │ ├── report_handle/ │ │ └── report_handle.go │ ├── review/ │ │ └── review_service.go │ ├── revision/ │ │ └── revision.go │ ├── revision_common/ │ │ └── revision_service.go │ ├── role/ │ │ ├── power_service.go │ │ ├── role_power_rel_service.go │ │ ├── role_service.go │ │ └── user_role_rel_service.go │ ├── search_common/ │ │ └── search.go │ ├── search_parser/ │ │ └── search_parser.go │ ├── service_config/ │ │ └── service_config.go │ ├── siteinfo/ │ │ └── siteinfo_service.go │ ├── siteinfo_common/ │ │ ├── siteinfo_service.go │ │ └── siteinfo_service_test.go │ ├── tag/ │ │ └── tag_service.go │ ├── tag_common/ │ │ └── tag_common.go │ ├── unique/ │ │ └── uniqid_service.go │ ├── uploader/ │ │ └── upload.go │ ├── user_admin/ │ │ └── user_backyard.go │ ├── user_common/ │ │ └── user.go │ ├── user_external_login/ │ │ ├── user_center_login_service.go │ │ └── user_external_login_service.go │ └── user_notification_config/ │ └── user_notification_config_service.go ├── licenserc.toml ├── pkg/ │ ├── checker/ │ │ ├── chinese.go │ │ ├── email.go │ │ ├── file_type.go │ │ ├── password.go │ │ ├── path_ignore.go │ │ ├── question_link.go │ │ ├── question_link_test.go │ │ ├── reserved_username.go │ │ ├── url.go │ │ ├── username.go │ │ └── zero_string.go │ ├── converter/ │ │ ├── array.go │ │ ├── markdown.go │ │ ├── str.go │ │ └── user.go │ ├── day/ │ │ ├── day.go │ │ └── day_test.go │ ├── dir/ │ │ └── dir.go │ ├── display/ │ │ └── url.go │ ├── encryption/ │ │ └── md5.go │ ├── gravatar/ │ │ ├── gravatar.go │ │ └── gravatar_test.go │ ├── htmltext/ │ │ ├── htmltext.go │ │ └── htmltext_test.go │ ├── obj/ │ │ └── obj.go │ ├── random/ │ │ └── random_username.go │ ├── token/ │ │ └── token.go │ ├── uid/ │ │ ├── id.go │ │ └── sid.go │ └── writer/ │ └── writer.go ├── plugin/ │ ├── agent.go │ ├── base.go │ ├── cache.go │ ├── captcha.go │ ├── cdn.go │ ├── config.go │ ├── connector.go │ ├── embed.go │ ├── filter.go │ ├── importer.go │ ├── kv_storage.go │ ├── notification.go │ ├── parser.go │ ├── plugin.go │ ├── plugin_test/ │ │ ├── kv_storage_test.go │ │ └── plugin_main_test.go │ ├── render.go │ ├── reviewer.go │ ├── search.go │ ├── sidebar.go │ ├── storage.go │ ├── user_center.go │ └── user_config.go ├── script/ │ ├── build_plugin.sh │ ├── check-asf-header.sh │ ├── entrypoint.sh │ ├── gen-api.sh │ └── plugin_list └── ui/ ├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .lintstagedrc.json ├── .npmrc ├── .prettierrc.json ├── commitlint.config.js ├── config-overrides.js ├── package.json ├── pnpm-workspace.yaml ├── public/ │ ├── index.html │ ├── manifest.json │ └── robots.txt ├── scripts/ │ ├── env.js │ ├── importPlugins.js │ ├── loadPlugins.js │ ├── preinstall.js │ └── setup-eslint.js ├── src/ │ ├── App.test.tsx │ ├── App.tsx │ ├── behaviour/ │ │ └── useLegalClick.tsx │ ├── common/ │ │ ├── _variable.scss │ │ ├── color.scss │ │ ├── constants.ts │ │ ├── interface.ts │ │ ├── pattern.ts │ │ └── sideNavLayout.scss │ ├── components/ │ │ ├── AccordionNav/ │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── Actions/ │ │ │ └── index.tsx │ │ ├── AdminSideNav/ │ │ │ └── index.tsx │ │ ├── Avatar/ │ │ │ └── index.tsx │ │ ├── BaseUserCard/ │ │ │ └── index.tsx │ │ ├── BrandUpload/ │ │ │ └── index.tsx │ │ ├── BubbleAi/ │ │ │ └── index.tsx │ │ ├── BubbleUser/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── CardBadge/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Comment/ │ │ │ ├── components/ │ │ │ │ ├── ActionBar/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Form/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Reply/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Counts/ │ │ │ └── index.tsx │ │ ├── CustomSidebar/ │ │ │ └── index.tsx │ │ ├── Customize/ │ │ │ └── index.tsx │ │ ├── CustomizeTheme/ │ │ │ └── index.tsx │ │ ├── DiffContent/ │ │ │ └── index.tsx │ │ ├── Editor/ │ │ │ ├── EditorContext.ts │ │ │ ├── MarkdownEditor.tsx │ │ │ ├── Select/ │ │ │ │ └── index.tsx │ │ │ ├── ToolBars/ │ │ │ │ ├── blockquote.tsx │ │ │ │ ├── bold.tsx │ │ │ │ ├── code.tsx │ │ │ │ ├── file.tsx │ │ │ │ ├── heading.tsx │ │ │ │ ├── help.tsx │ │ │ │ ├── hr.tsx │ │ │ │ ├── image.tsx │ │ │ │ ├── indent.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── italic.tsx │ │ │ │ ├── link.tsx │ │ │ │ ├── ol.tsx │ │ │ │ ├── outdent.tsx │ │ │ │ ├── table.tsx │ │ │ │ └── ul.tsx │ │ │ ├── Viewer.tsx │ │ │ ├── hooks/ │ │ │ │ └── useImageUpload.ts │ │ │ ├── index.scss │ │ │ ├── index.tsx │ │ │ ├── toolItem.tsx │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ ├── codemirror/ │ │ │ │ ├── adapter.ts │ │ │ │ ├── base.ts │ │ │ │ ├── commands.ts │ │ │ │ └── events.ts │ │ │ └── index.ts │ │ ├── Empty/ │ │ │ └── index.tsx │ │ ├── FollowingTags/ │ │ │ └── index.tsx │ │ ├── Footer/ │ │ │ └── index.tsx │ │ ├── FormatTime/ │ │ │ └── index.tsx │ │ ├── Header/ │ │ │ ├── components/ │ │ │ │ ├── NavItems/ │ │ │ │ │ └── index.tsx │ │ │ │ └── SearchInput/ │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── HighlightText/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── HotQuestions/ │ │ │ └── index.tsx │ │ ├── HttpErrorContent/ │ │ │ └── index.tsx │ │ ├── Icon/ │ │ │ ├── index.tsx │ │ │ └── svg.tsx │ │ ├── ImgViewer/ │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── InitialLoadingPlaceholder/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Mentions/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── MobileSideNav/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Modal/ │ │ │ ├── BadgeModal.tsx │ │ │ ├── Confirm.tsx │ │ │ ├── LoginToContinueModal.tsx │ │ │ ├── Modal.tsx │ │ │ ├── index.tsx │ │ │ └── login.scss │ │ ├── Operate/ │ │ │ └── index.tsx │ │ ├── PageTags/ │ │ │ └── index.tsx │ │ ├── Pagination/ │ │ │ └── index.tsx │ │ ├── PinList/ │ │ │ └── index.tsx │ │ ├── PluginRender/ │ │ │ └── index.tsx │ │ ├── QueryGroup/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── QuestionList/ │ │ │ └── index.tsx │ │ ├── QuestionListLoader/ │ │ │ └── index.tsx │ │ ├── SchemaForm/ │ │ │ ├── README.md │ │ │ ├── components/ │ │ │ │ ├── Button.tsx │ │ │ │ ├── Check.tsx │ │ │ │ ├── Input.tsx │ │ │ │ ├── InputGroup.tsx │ │ │ │ ├── Legend.tsx │ │ │ │ ├── Select.tsx │ │ │ │ ├── Switch.tsx │ │ │ │ ├── TagSelector.tsx │ │ │ │ ├── Textarea.tsx │ │ │ │ ├── Timezone.tsx │ │ │ │ ├── Upload.tsx │ │ │ │ └── index.ts │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── Sender/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Share/ │ │ │ └── index.tsx │ │ ├── SideNav/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── TabNav/ │ │ │ └── index.tsx │ │ ├── Tag/ │ │ │ └── index.tsx │ │ ├── TagSelector/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── TagsLoader/ │ │ │ └── index.tsx │ │ ├── TextArea/ │ │ │ └── index.tsx │ │ ├── TimeZonePicker/ │ │ │ └── index.tsx │ │ ├── Toast/ │ │ │ └── index.tsx │ │ ├── Unactivate/ │ │ │ └── index.tsx │ │ ├── UploadImg/ │ │ │ └── index.tsx │ │ ├── UserCard/ │ │ │ └── index.tsx │ │ ├── WelcomeTitle/ │ │ │ └── index.tsx │ │ └── index.ts │ ├── hooks/ │ │ ├── index.ts │ │ ├── useActivationEmailModal/ │ │ │ └── index.tsx │ │ ├── useCaptchaModal/ │ │ │ └── index.tsx │ │ ├── useChangePasswordModal/ │ │ │ └── index.tsx │ │ ├── useChangeProfileModal/ │ │ │ └── index.tsx │ │ ├── useChangeUserRoleModal/ │ │ │ └── index.tsx │ │ ├── useExternalToast/ │ │ │ └── index.tsx │ │ ├── useLoginRedirect/ │ │ │ └── index.tsx │ │ ├── usePageTags/ │ │ │ └── index.tsx │ │ ├── usePageUsers/ │ │ │ └── index.tsx │ │ ├── usePrompt/ │ │ │ └── index.tsx │ │ ├── useReportModal/ │ │ │ └── index.tsx │ │ ├── useSkeletonControl/ │ │ │ └── index.tsx │ │ ├── useTagModal/ │ │ │ └── index.tsx │ │ ├── useToast/ │ │ │ └── index.tsx │ │ └── useUserModal/ │ │ └── index.tsx │ ├── i18n/ │ │ └── init.ts │ ├── index.scss │ ├── index.tsx │ ├── pages/ │ │ ├── 404/ │ │ │ ├── 403/ │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── 50X/ │ │ │ └── index.tsx │ │ ├── Admin/ │ │ │ ├── AiAssistant/ │ │ │ │ ├── components/ │ │ │ │ │ ├── Action/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── DetailModal/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── AiSettings/ │ │ │ │ └── index.tsx │ │ │ ├── Answers/ │ │ │ │ ├── components/ │ │ │ │ │ └── Action/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Apikeys/ │ │ │ │ ├── components/ │ │ │ │ │ ├── Action/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── AddOrEditModal/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── CreatedModal/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── index.tsx │ │ │ ├── Badges/ │ │ │ │ ├── components/ │ │ │ │ │ └── Action/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Branding/ │ │ │ │ └── index.tsx │ │ │ ├── CssAndHtml/ │ │ │ │ └── index.tsx │ │ │ ├── Dashboard/ │ │ │ │ ├── components/ │ │ │ │ │ ├── AnswerLinks/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── HealthStatus/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Statistics/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── SystemInfo/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── index.tsx │ │ │ ├── Files/ │ │ │ │ └── index.tsx │ │ │ ├── General/ │ │ │ │ └── index.tsx │ │ │ ├── Interface/ │ │ │ │ └── index.tsx │ │ │ ├── Login/ │ │ │ │ └── index.tsx │ │ │ ├── Mcp/ │ │ │ │ └── index.tsx │ │ │ ├── Plugins/ │ │ │ │ ├── Config/ │ │ │ │ │ └── index.tsx │ │ │ │ └── Installed/ │ │ │ │ └── index.tsx │ │ │ ├── Policies/ │ │ │ │ └── index.tsx │ │ │ ├── Privileges/ │ │ │ │ └── index.tsx │ │ │ ├── QaSettings/ │ │ │ │ └── index.tsx │ │ │ ├── Questions/ │ │ │ │ ├── components/ │ │ │ │ │ └── Action/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Security/ │ │ │ │ └── index.tsx │ │ │ ├── Seo/ │ │ │ │ └── index.tsx │ │ │ ├── Smtp/ │ │ │ │ └── index.tsx │ │ │ ├── TagsSettings/ │ │ │ │ └── index.tsx │ │ │ ├── Themes/ │ │ │ │ └── index.tsx │ │ │ ├── Users/ │ │ │ │ ├── components/ │ │ │ │ │ ├── Action/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── DeleteUserModal/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── SuspenseUserModal/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── UsersSettings/ │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── AiAssistant/ │ │ │ ├── components/ │ │ │ │ └── ConversationList/ │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Badges/ │ │ │ ├── Detail/ │ │ │ │ ├── components/ │ │ │ │ │ ├── Badge/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── HeaderLoader/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Loader/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── UserCard/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Install/ │ │ │ ├── components/ │ │ │ │ ├── FifthStep/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── FirstStep/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── FourthStep/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Progress/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecondStep/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ThirdStep/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.ts │ │ │ └── index.tsx │ │ ├── Layout/ │ │ │ └── index.tsx │ │ ├── Legal/ │ │ │ ├── Privacy/ │ │ │ │ └── index.tsx │ │ │ ├── Tos/ │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Maintenance/ │ │ │ └── index.tsx │ │ ├── Questions/ │ │ │ ├── Ask/ │ │ │ │ ├── components/ │ │ │ │ │ └── SearchQuestion/ │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Detail/ │ │ │ │ ├── components/ │ │ │ │ │ ├── Alert/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Answer/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── AnswerHead/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ContentLoader/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── InviteToAnswer/ │ │ │ │ │ │ ├── PeopleDropdown.scss │ │ │ │ │ │ ├── PeopleDropdown.tsx │ │ │ │ │ │ ├── index.scss │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── LinkedQuestions/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Question/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Reactions/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── RelatedQuestions/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── WriteAnswer/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── EditAnswer/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── Linked/ │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Review/ │ │ │ ├── components/ │ │ │ │ ├── ApproveDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── EditPostModal/ │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── FlagContent/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── QueuedContent/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ReviewType/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SuggestContent/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.ts │ │ │ ├── index.tsx │ │ │ └── utils/ │ │ │ └── generateData.ts │ │ ├── Search/ │ │ │ ├── components/ │ │ │ │ ├── AiCard/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Empty/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Head/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ListLoader/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SearchHead/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SearchItem/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Tips/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.ts │ │ │ └── index.tsx │ │ ├── SideNavLayout/ │ │ │ └── index.tsx │ │ ├── SideNavLayoutWithoutFooter/ │ │ │ └── index.tsx │ │ ├── Tags/ │ │ │ ├── Create/ │ │ │ │ └── index.tsx │ │ │ ├── Detail/ │ │ │ │ └── index.tsx │ │ │ ├── Edit/ │ │ │ │ └── index.tsx │ │ │ ├── Info/ │ │ │ │ ├── components/ │ │ │ │ │ └── MergeTagModal/ │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Timeline/ │ │ │ ├── components/ │ │ │ │ └── Item/ │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── UserCenter/ │ │ │ ├── Auth/ │ │ │ │ ├── components/ │ │ │ │ │ └── WeCom/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── service.ts │ │ │ │ └── index.tsx │ │ │ └── AuthFailed/ │ │ │ ├── components/ │ │ │ │ └── WeCom.tsx │ │ │ └── index.tsx │ │ └── Users/ │ │ ├── AccountForgot/ │ │ │ ├── components/ │ │ │ │ └── sendEmail.tsx │ │ │ └── index.tsx │ │ ├── ActivationResult/ │ │ │ └── index.tsx │ │ ├── ActiveEmail/ │ │ │ └── index.tsx │ │ ├── AuthCallback/ │ │ │ └── index.tsx │ │ ├── ChangeEmail/ │ │ │ ├── components/ │ │ │ │ └── sendEmail.tsx │ │ │ └── index.tsx │ │ ├── ConfirmNewEmail/ │ │ │ └── index.tsx │ │ ├── Login/ │ │ │ └── index.tsx │ │ ├── Logout/ │ │ │ └── index.tsx │ │ ├── Notifications/ │ │ │ ├── components/ │ │ │ │ ├── Achievements/ │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── Inbox/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.ts │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── OauthBindEmail/ │ │ │ └── index.tsx │ │ ├── PasswordReset/ │ │ │ └── index.tsx │ │ ├── Personal/ │ │ │ ├── components/ │ │ │ │ ├── Alert/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Answers/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Badges/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Comments/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── DefaultList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ListHead/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NavBar/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Overview/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Reputation/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TopList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── UserInfo/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Votes/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.ts │ │ │ └── index.tsx │ │ ├── Register/ │ │ │ ├── components/ │ │ │ │ └── SignUpForm/ │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Settings/ │ │ │ ├── Account/ │ │ │ │ ├── components/ │ │ │ │ │ ├── ModifyEmail/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ModifyPass/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── MyLogins/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── index.tsx │ │ │ ├── Interface/ │ │ │ │ └── index.tsx │ │ │ ├── Notification/ │ │ │ │ └── index.tsx │ │ │ ├── Plugins/ │ │ │ │ └── index.tsx │ │ │ ├── Profile/ │ │ │ │ └── index.tsx │ │ │ ├── components/ │ │ │ │ └── Nav/ │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Suspended/ │ │ │ └── index.tsx │ │ ├── Unsubscribe/ │ │ │ └── index.tsx │ │ └── index.tsx │ ├── plugins/ │ │ ├── builtin/ │ │ │ ├── HostingConnector/ │ │ │ │ ├── i18n/ │ │ │ │ │ ├── en_US.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh_CN.yaml │ │ │ │ ├── index.tsx │ │ │ │ └── info.yaml │ │ │ ├── SearchInfo/ │ │ │ │ ├── i18n/ │ │ │ │ │ ├── en_US.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh_CN.yaml │ │ │ │ ├── index.tsx │ │ │ │ ├── info.yaml │ │ │ │ └── services.ts │ │ │ ├── ThirdPartyConnector/ │ │ │ │ ├── i18n/ │ │ │ │ │ ├── en_US.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh_CN.yaml │ │ │ │ ├── index.tsx │ │ │ │ ├── info.yaml │ │ │ │ └── services.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── react-app-env.d.ts │ ├── router/ │ │ ├── RouteErrorBoundary.tsx │ │ ├── RouteGuard.tsx │ │ ├── alias.ts │ │ ├── index.tsx │ │ ├── pathFactory.ts │ │ └── routes.ts │ ├── services/ │ │ ├── admin/ │ │ │ ├── ai.ts │ │ │ ├── answer.ts │ │ │ ├── apikeys.ts │ │ │ ├── badges.ts │ │ │ ├── dashboard.ts │ │ │ ├── flag.ts │ │ │ ├── index.ts │ │ │ ├── mcp.ts │ │ │ ├── plugins.ts │ │ │ ├── question.ts │ │ │ ├── settings.ts │ │ │ ├── tags.ts │ │ │ └── users.ts │ │ ├── client/ │ │ │ ├── Oauth.ts │ │ │ ├── activity.ts │ │ │ ├── ai.ts │ │ │ ├── badges.ts │ │ │ ├── index.ts │ │ │ ├── legal.ts │ │ │ ├── notification.ts │ │ │ ├── personal.ts │ │ │ ├── question.ts │ │ │ ├── review.ts │ │ │ ├── revision.ts │ │ │ ├── search.ts │ │ │ ├── settings.ts │ │ │ ├── tag.ts │ │ │ ├── timeline.ts │ │ │ └── user.ts │ │ ├── common.ts │ │ ├── index.ts │ │ ├── install/ │ │ │ └── index.ts │ │ └── user-center/ │ │ └── index.ts │ ├── stores/ │ │ ├── aiControl.ts │ │ ├── branding.ts │ │ ├── commentReply.ts │ │ ├── customize.ts │ │ ├── errorCode.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ ├── loggedUserInfo.ts │ │ ├── loginSetting.ts │ │ ├── loginToContinue.ts │ │ ├── pageTags.ts │ │ ├── seoSetting.ts │ │ ├── sideNav.ts │ │ ├── siteInfo.ts │ │ ├── siteSecurity.ts │ │ ├── themeSetting.ts │ │ ├── toast.ts │ │ ├── userCenter.ts │ │ └── writeSetting.ts │ └── utils/ │ ├── animateGift.ts │ ├── color.ts │ ├── common.ts │ ├── floppyNavigation.ts │ ├── guard.ts │ ├── index.ts │ ├── localize.ts │ ├── pluginKit/ │ │ ├── index.ts │ │ ├── interface.ts │ │ └── utils.ts │ ├── progress.ts │ ├── request.ts │ ├── requestAi.ts │ ├── saveDraft.ts │ ├── storage.ts │ ├── storageWithExpires.ts │ └── userCenter.ts ├── static.go ├── template/ │ ├── 404.html │ ├── comment.html │ ├── footer.html │ ├── header.html │ ├── homepage.html │ ├── hot-question.html │ ├── opensearch.xml │ ├── page.html │ ├── question-detail.html │ ├── question.html │ ├── related-question.html │ ├── sidenav.html │ ├── sitemap-list.xml │ ├── sitemap.xml │ ├── sort-btns.html │ ├── tag-detail.html │ └── tags.html └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .asf.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # NOTE: All configurations could be found here: https://cwiki.apache.org/confluence/display/INFRA/Git+-+.asf.yaml+features github: description: "A Q&A platform software for teams at any scales. Whether it's a community forum, help center, or knowledge management platform, you can always count on Apache Answer." homepage: https://answer.apache.org labels: - react - go - golang - community - forum - question - typescript - q-and-a - hacktoberfest features: wiki: true issues: true projects: true discussions: false enabled_merge_buttons: squash: true rebase: true merge: false protected_branches: main: {} notifications: commits: commits@answer.apache.org issues: issues@answer.apache.org pullrequests: issues@answer.apache.org discussions: issues@answer.apache.org ================================================ FILE: .editorconfig ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.go] indent_style = tab indent_size = 2 [{Makefile, Dockerfile}] indent_style = tab indent_size = 4 [{*.yml, *.json}] indent_style = space indent_size = 2 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Report an issue to help the project improve. title: '' labels: bug type: 'Bug' assignees: '' --- ## Describe the bug A clear and concise description of what the bug is. ## To Reproduce Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error ## Expected behavior A clear and concise description of what you expected to happen. ## Screenshots If applicable, add screenshots or video to help explain your problem. ## Platform - Device: [e.g. Desktop, Mobile] - OS: [e.g. macOS] - Browser and version: [e.g. Chrome, Safari] - Version: [e.g. v1.2.0] ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. blank_issues_enabled: true contact_links: - name: Questions & Discussions url: https://meta.answer.dev about: If you have any questions while using. ================================================ FILE: .github/ISSUE_TEMPLATE/enhancement_request.md ================================================ --- name: Enhancement request about: Suggest an enhancement for this project. Improve an existing feature. title: '' labels: enhancement type: 'Feature' assignees: '' --- ## Is your enhancement request related to a problem? Please describe A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] ## Describe the solution you'd like A clear and concise description of what you want to happen. ## Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea or possible new feature for this project. title: '' labels: new-feature type: 'Feature' assignees: '' --- ## Is your feature request related to a problem? Please describe A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] ## Describe the solution you'd like A clear and concise description of what you want to happen. ## Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE/pull_request_template.md ================================================ Fixes # ## Proposed Changes - - - ================================================ FILE: .github/workflows/build-binary-for-release.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Build Binary For Release on: push: tags: - "v*" permissions: contents: write jobs: build-goreleaser: if: ${{ github.repository_owner == 'apache' }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v3 with: node-version: 20.18.1 - name: Node Build run: make install-ui-packages ui - name: Setup Go uses: actions/setup-go@v3 with: go-version: 1.23 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: distribution: goreleaser version: latest args: release --clean --skip=validate env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/upload-artifact@v4 with: name: answer path: ./dist/* ================================================ FILE: .github/workflows/build-image-for-latest-release.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Build Latest Docker Image For Release on: push: tags: - v2.* - v1.* - v0.* - "!v*-RC*" # pull_request: # branches: [ "main" ] jobs: build: if: ${{ github.repository_owner == 'apache' }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Docker meta id: meta uses: docker/metadata-action@v4 with: images: apache/answer tags: | type=raw,value=latest - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm64 push: true file: ./Dockerfile tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/build-image-for-manual.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Manual Build Docker Image For Release on: workflow_dispatch: inputs: tag_name: type: string required: true description: 'DockerHub img tag name' jobs: build: if: ${{ github.repository_owner == 'apache' }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Docker meta id: meta uses: docker/metadata-action@v4 with: images: apache/answer tags: | type=ref,enable=true,priority=600,prefix=,suffix=,event=branch type=semver,pattern={{version}} - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm64 push: true file: ./Dockerfile tags: apache/answer:${{ inputs.tag_name }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/build-image-for-release.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Build Docker Image For Release on: push: tags: - v2.* - v1.* - v0.* # pull_request: # branches: [ "main" ] jobs: build: if: ${{ github.repository_owner == 'apache' }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Docker meta id: meta uses: docker/metadata-action@v4 with: images: apache/answer tags: | type=ref,enable=true,priority=600,prefix=,suffix=,event=branch type=semver,pattern={{version}} - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm64 push: true file: ./Dockerfile tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/build-image-for-test.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Build Docker Image For Test on: push: branches: [ "test" ] jobs: build: if: ${{ github.repository_owner == 'apache' }} name: Build and Push runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Docker meta id: meta uses: docker/metadata-action@v4 with: images: apache/answer tags: | type=raw,value=test - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile platforms: linux/amd64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/check-asf-header.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: CI on: pull_request: branches: [ main ] push: branches: [ main ] # Concurrency strategy: # github.workflow: distinguish this workflow from others # github.event_name: distinguish `push` event from `pull_request` event # github.event.number: set to the number of the pull request if `pull_request` event # github.run_id: otherwise, it's a `push` or `schedule` event, only cancel if we rerun the workflow # # Reference: # https://docs.github.com/en/actions/using-jobs/using-concurrency # https://docs.github.com/en/actions/learn-github-actions/contexts#github-context concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }} cancel-in-progress: true jobs: check: name: Check and lint runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Check license header uses: korandoru/hawkeye@v3 ================================================ FILE: .github/workflows/lint.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Lint on: push: pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }} cancel-in-progress: true jobs: lint: name: Lint (${{ matrix.os }}) runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: fail-fast: false matrix: os: [ubuntu-latest] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: "1.23" cache: true - name: Run go mod tidy run: go mod tidy - name: Run golangci-lint run: make lint - name: Check for uncommitted changes shell: bash run: | if [ -n "$(git status --porcelain)" ]; then echo "::error::Uncommitted changes detected" git status git diff exit 1 fi ================================================ FILE: .gitignore ================================================ *.exe *.orig *.rej *.so *~ *.db .DS_Store ._* /.idea /.fleet /.vscode/*.log /cmd/answer/*.sh /cmd/answer/answer /cmd/answer/uploads/* /cmd/logs /configs/config-dev.yaml /go.work* /logs /ui/node_modules /ui/build/*/*/* /ui/build/*.json /ui/build/*.html /ui/build/*.txt /vendor Thumbs*.db tmp vendor/ /answer-data/ /answer /new_answer build/tools/ dist/ # Lint setup generated file .husky/ # Environment variables .env ================================================ FILE: .gitlab-ci.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. include: - project: "segmentfault/devops/templates" file: ".docker-build-push.yml" - project: "segmentfault/devops/templates" file: ".deploy-helm.yml" stages: - deploy-dev "deploy-to-local-develop-environment": stage: deploy-dev extends: .deploy-helm only: - test variables: LoadBalancerIP: 10.0.10.98 KubernetesCluster: dev KubernetesNamespace: "sf-web" InstallArgs: --set service.loadBalancerIP=${LoadBalancerIP} --set image.tag=latest --set replicaCount=1 --set serivce.targetPort=80 ChartName: answer InstallPolicy: replace ================================================ FILE: .golangci.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. version: "2" linters: exclusions: paths: - answer-data - ui - i18n enable: - asasalint # checks for pass []any as any in variadic func(...any) - asciicheck # checks that your code does not contain non-ASCII identifiers - bidichk # checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully - canonicalheader # checks whether net/http.Header uses canonical header - copyloopvar # detects places where loop variables are copied (Go 1.22+) - gocritic # provides diagnostics that check for bugs, performance and style issues - misspell # finds commonly misspelled English words in comments and strings - modernize # detects code that can be modernized to use newer Go features - testifylint # checks usage of github.com/stretchr/testify - unconvert # removes unnecessary type conversions - unparam # reports unused function parameters - whitespace # detects leading and trailing whitespace formatters: enable: - gofmt - goimports settings: gofmt: simplify: true rewrite-rules: - pattern: 'interface{}' replacement: 'any' ================================================ FILE: .goreleaser.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. version: 2 env: - GO11MODULE=on - GO111MODULE=on - GOPROXY=https://goproxy.io,direct - CGO_ENABLED=0 before: hooks: - go mod tidy release: draft: true builds: - id: build main: ./cmd/answer/. binary: answer ldflags: -s -w -X github.com/apache/answer/cmd.Version={{.RawVersion}} -X github.com/apache/answer/cmd.Revision={{.ShortCommit}} -X github.com/apache/answer/cmd.Time={{.Date}} -X github.com/apache/answer/cmd.BuildUser=goreleaser flags: -v goos: - linux - darwin goarch: - amd64 - arm64 - id: build-windows main: ./cmd/answer/. binary: answer ldflags: -s -w -X github.com/apache/answer/cmd.Version={{.RawVersion}} -X github.com/apache/answer/cmd.Revision={{.ShortCommit}} -X github.com/apache/answer/cmd.Time={{.Date}} -X github.com/apache/answer/cmd.BuildUser=goreleaser flags: -v goos: - windows goarch: - amd64 archives: - name_template: >- apache-answer-{{ .RawVersion }}-bin-{{ .Os }}-{{ .Arch }} files: - src: "docs/release/LICENSE" dst: LICENSE - src: "docs/release/NOTICE" dst: NOTICE - src: "docs/release/licenses/*" dst: licenses/ wrap_in_directory: true checksum: name_template: 'checksums.txt' snapshot: version_template: "{{ incpatch .Version }}" changelog: sort: asc filters: exclude: - '^docs:' - '^test:' source: enabled: true name_template: apache-answer-{{ .RawVersion }}-src prefix_template: "apache-answer-{{ .RawVersion }}-src/" # goreleaser release --skip-validate --skip-publish --clean ================================================ FILE: .vaunt/config.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. version: 0.0.1 achievements: - achievement: name: Visionary Architect icon: https://raw.githubusercontent.com/apache/answer/main/.vaunt/enhancement.png description: Awarded for bringing up enhancement, dream big! triggers: - trigger: actor: assignees action: issue condition: labels in ['enhancement'] & labels in ['LGTM'] - achievement: name: Bug Hunter icon: https://raw.githubusercontent.com/apache/answer/main/.vaunt/bug.png description: Awarded for identifying real bugs, well spotted! triggers: - trigger: actor: assignees action: issue condition: labels in ['bug'] & labels in ['LGTM'] ================================================ FILE: .vscode/settings.json ================================================ { "eslint.workingDirectories": [ "ui" ], "explorer.autoReveal": "focusNoScroll", "cSpell.words": [ "grecaptcha" ] } ================================================ FILE: Dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. FROM golang:1.24-alpine AS golang-builder LABEL maintainer="linkinstar@apache.org" ARG GOPROXY # ENV GOPROXY ${GOPROXY:-direct} # ENV GOPROXY=https://proxy.golang.com.cn,direct ENV GOPATH /go ENV GOROOT /usr/local/go ENV PACKAGE github.com/apache/answer ENV BUILD_DIR ${GOPATH}/src/${PACKAGE} ENV ANSWER_MODULE ${BUILD_DIR} ARG TAGS="sqlite sqlite_unlock_notify" ENV TAGS "bindata timetzdata $TAGS" ARG CGO_EXTRA_CFLAGS COPY . ${BUILD_DIR} WORKDIR ${BUILD_DIR} RUN apk --no-cache add build-base git bash nodejs npm && npm install -g pnpm@9.7.0 \ && make clean build RUN chmod 755 answer RUN ["/bin/bash","-c","script/build_plugin.sh"] RUN cp answer /usr/bin/answer RUN mkdir -p /data/uploads && chmod 777 /data/uploads \ && mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n FROM alpine LABEL maintainer="linkinstar@apache.org" ARG TIMEZONE ENV TIMEZONE=${TIMEZONE:-"Asia/Shanghai"} RUN apk update \ && apk --no-cache add \ bash \ ca-certificates \ curl \ dumb-init \ gettext \ openssh \ sqlite \ gnupg \ tzdata \ && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ && echo "${TIMEZONE}" > /etc/timezone COPY --from=golang-builder /usr/bin/answer /usr/bin/answer COPY --from=golang-builder /data /data COPY /script/entrypoint.sh /entrypoint.sh RUN chmod 755 /entrypoint.sh VOLUME /data EXPOSE 80 ENTRYPOINT ["/entrypoint.sh"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ .PHONY: build clean ui VERSION=2.0.0 BIN=answer DIR_SRC=./cmd/answer DOCKER_CMD=docker GO_ENV=CGO_ENABLED=0 GO111MODULE=on Revision=$(shell git rev-parse --short HEAD 2>/dev/null || echo "") GO_FLAGS=-ldflags="-X github.com/apache/answer/cmd.Version=$(VERSION) -X 'github.com/apache/answer/cmd.Revision=$(Revision)' -X 'github.com/apache/answer/cmd.Time=`date +%s`' -extldflags -static" GO=$(GO_ENV) "$(shell which go)" GOLANGCI_VERSION ?= v2.6.2 TOOLS_BIN := $(shell mkdir -p build/tools && realpath build/tools) GOLANGCI = $(TOOLS_BIN)/golangci-lint-$(GOLANGCI_VERSION) $(GOLANGCI): rm -f $(TOOLS_BIN)/golangci-lint* curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_VERSION)/install.sh | sh -s -- -b $(TOOLS_BIN) $(GOLANGCI_VERSION) mv $(TOOLS_BIN)/golangci-lint $(TOOLS_BIN)/golangci-lint-$(GOLANGCI_VERSION) build: generate @$(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC) # https://dev.to/thewraven/universal-macos-binaries-with-go-1-16-3mm3 universal: generate @GOOS=darwin GOARCH=amd64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_amd64 $(DIR_SRC) @GOOS=darwin GOARCH=arm64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_arm64 $(DIR_SRC) @lipo -create -output ${BIN} ${BIN}_amd64 ${BIN}_arm64 @rm -f ${BIN}_amd64 ${BIN}_arm64 generate: @$(GO) get github.com/swaggo/swag/cmd/swag@v1.16.3 @$(GO) get github.com/google/wire/cmd/wire@v0.5.0 @$(GO) get go.uber.org/mock/mockgen@v0.6.0 @$(GO) install github.com/swaggo/swag/cmd/swag@v1.16.3 @$(GO) install github.com/google/wire/cmd/wire@v0.5.0 @$(GO) install go.uber.org/mock/mockgen@v0.6.0 @$(GO) generate ./... @$(GO) mod tidy check: @mockgen -version @swag -v @wire flags test: @$(GO) test ./internal/repo/repo_test # clean all build result clean: @$(GO) clean ./... @rm -f $(BIN) install-ui-packages: @corepack enable @corepack prepare pnpm@9.7.0 --activate ui: @cd ui && pnpm pre-install && pnpm build && cd - lint: generate $(GOLANGCI) @bash ./script/check-asf-header.sh $(GOLANGCI) run lint-fix: generate $(GOLANGCI) @bash ./script/check-asf-header.sh $(GOLANGCI) run --fix all: clean build ================================================ FILE: NOTICE ================================================ Apache Answer Copyright 2023-2025 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (https://www.apache.org/). ================================================ FILE: README.md ================================================ logo # Apache Answer - Build Q&A platform A Q&A platform software for teams at any scales. Whether it’s a community forum, help center, or knowledge management platform, you can always count on Answer. To learn more about the project, visit [answer.apache.org](https://answer.apache.org). [![LICENSE](https://img.shields.io/github/license/apache/answer)](https://github.com/apache/answer/blob/main/LICENSE) [![Language](https://img.shields.io/badge/language-go-blue.svg)](https://golang.org/) [![Language](https://img.shields.io/badge/language-react-blue.svg)](https://reactjs.org/) [![Go Report Card](https://goreportcard.com/badge/github.com/apache/answer)](https://goreportcard.com/report/github.com/apache/answer) [![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5)](https://discord.gg/Jm7Y4cbUej) ## Screenshots ![screenshot](docs/img/screenshot.png) ## Quick start ### Running with docker ```bash docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:2.0.0 ``` For more information, see [Installation](https://answer.apache.org/docs/installation). ### Plugins Answer provides a plugin system for developers to create custom plugins and expand Answer’s features. You can find the [plugin documentation here](https://answer.apache.org/community/plugins). We value your feedback and suggestions to improve our documentation. If you have any comments or questions, please feel free to contact us. We’re excited to see what you can create using our plugin system! You can also check out the [plugins here](https://answer.apache.org/plugins). ## Building from Source ### Prerequisites - Golang >= 1.23 - Node.js >= 20 - pnpm >= 9 - [mockgen](https://github.com/uber-go/mock?tab=readme-ov-file#installation) >= 0.6.0 - [wire](https://github.com/google/wire/) >= 0.5.0 ### Build ```bash # Install wire and mockgen for building. You can run `make check` to check if they are installed. $ make generate # Install frontend dependencies and build $ make ui # Install backend dependencies and build $ make build ``` ## Contributing Contributions are always welcome! See [CONTRIBUTING](https://answer.apache.org/community/contributing) for ways to get started. ## License [Apache License 2.0](https://github.com/apache/answer/blob/main/LICENSE) ================================================ FILE: build/README.md ================================================ # /build Packaging and Continuous Integration. ================================================ FILE: charts/.helmignore ================================================ # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *.orig *~ # Various IDEs .project .idea/ *.tmproj .vscode/ ================================================ FILE: charts/Chart.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v2 name: answer description: A simple answer deployments for kubernetes type: application version: 0.1.0 appVersion: "1.0.7" ================================================ FILE: charts/README.md ================================================ # answer An open-source knowledge-based community software. You can use it quickly to build Q&A community for your products, customers, teams, and more. ## Prerequisites - Kubernetes 1.20+ ## Configuration The following table lists the configurable parameters of the answer chart and their default values. | Parameter | Description | Default | | --------- | ----------- | ------- | | `replicaCount` | Number of answer replicas | `1` | | `image.repository` | Image repository | `apache/answer` | | `image.pullPolicy` | Image pull policy | `Always` | | `image.tag` | Image tag | `latest` | | `env` | Optional environment variables for answer | `LOG_LEVEL: INFO` | | `extraContainers` | Optional sidecar containers to run along side answer | `[]` | | `persistence.enabled` | Enable or disable persistence for the /data volume | `true` | | `persistence.accessMode` | Specify the access mode of the persistent volume | `ReadWriteOnce` | | `persistence.size` | The size of the persistent volume | `5Gi` | | `persistence.annotations` | Annotations to add to the volume claim | `{}` | | `imagePullSecrets` | Reference to one or more secrets to be used when pulling images | `[]` | | `nameOverride` | nameOverride replaces the name of the chart in the Chart.yaml file, when this is used to construct Kubernetes object names. | | | `fullnameOverride` | fullnameOverride completely replaces the generated name. | | | `serviceAccount.create` | If `true`, create a new service account | `true` | | `serviceAccount.annotations` | Annotations to add to the service account | `{}` | | `serviceAccount.name` | Service account to be used. If not set and `serviceAccount.create` is `true`, a name is generated using the fullname template | | | `podAnnotations` | Annotations to add to the answer pod | `{}` | | `podSecurityContext` | Security context for the answer pod | `{}` refer to [Default Security Contexts](#default-security-contexts) | | `securityContext` | Security context for the answer container | `{}` refer to [Default Security Contexts](#default-security-contexts) | | `service.type` | The type of service to be used | `ClusterIP` | | `service.port` | The port that the service should listen on for requests. Also used as the container port. | `80` | | `ingress.enabled` | Enable or disable ingress. | `false` | | `resources` | CPU/memory resource requests/limits | `{}` | | `autoscaling.enabled` | Enable or disable pod autoscaling. If enabled, replicas are disabled. | `false` | | `nodeSelector` | Node labels for pod assignment | `{}` | | `tolerations` | Node tolerations for pod assignment | `[]` | | `affinity` | Node affinity for pod assignment | `{}` | ### Default Security Contexts The default pod-level and container-level security contexts, below, adhere to the [restricted](https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted) Pod Security Standards policies. Default pod-level securityContext: ```yaml runAsNonRoot: true seccompProfile: type: RuntimeDefault ``` Default containerSecurityContext: ```yaml allowPrivilegeEscalation: false capabilities: drop: - ALL ``` ### Installing with a Values file ```console $ helm install answer -f values.yaml . ``` > **Tip**: You can use the default [values.yaml] ## TODO Publish the chart to Artifacthub and add proper installation instructions. E.G. > **NOTE**: This is not currently a valid installation option. ```console $ helm repo add apache https://charts.answer.apache.org/ $ helm repo update $ helm install apache/answer -n mynamespace ``` ================================================ FILE: charts/templates/_helpers.tpl ================================================ {{/* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */}} {{- define "answer.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "answer.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define "answer.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "answer.labels" -}} helm.sh/chart: {{ include "answer.chart" . }} {{ include "answer.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "answer.selectorLabels" -}} app.kubernetes.io/name: {{ include "answer.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} {{- define "answer.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "answer.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} ================================================ FILE: charts/templates/deployment.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "answer.fullname" . }} labels: {{- include "answer.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include "answer.selectorLabels" . | nindent 6 }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "answer.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "answer.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: {{ .Values.service.port }} protocol: TCP livenessProbe: httpGet: path: / port: http readinessProbe: httpGet: path: / port: http resources: {{- toYaml .Values.resources | nindent 12 }} {{- if .Values.env }} env: {{- range .Values.env }} - name: {{ .name }} {{- if .value | quote }} value: {{ .value | quote }} {{- end }} {{- if .valueFrom }} valueFrom: {{- toYaml .valueFrom | nindent 16 }} {{- end }} {{- end }} {{- end }} volumeMounts: - name: data mountPath: "/data" {{- if .Values.extraContainers }} {{- toYaml .Values.extraContainers | nindent 8 }} {{- end }} volumes: - name: data {{- if .Values.persistence.enabled }} persistentVolumeClaim: claimName: {{ include "answer.fullname" . }}-claim {{- else }} emptyDir: {} {{- end -}} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} ================================================ FILE: charts/templates/hpa.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {{ if .Values.autoscaling.enabled -}} apiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler metadata: name: {{ include "answer.fullname" . }} labels: {{- include "answer.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: {{ include "answer.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - type: Resource resource: name: cpu targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} {{- end }} {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - type: Resource resource: name: memory targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- end }} {{- end }} ================================================ FILE: charts/templates/ingress.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {{ if .Values.ingress.enabled -}} {{- $fullName := include "answer.fullname" . -}} {{- $svcPort := .Values.service.port -}} {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} {{- end }} {{- end }} {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1 {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1beta1 {{- else -}} apiVersion: extensions/v1beta1 {{- end }} kind: Ingress metadata: name: {{ $fullName }} labels: {{- include "answer.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} ingressClassName: {{ .Values.ingress.className }} {{- end }} {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} pathType: {{ .pathType }} {{- end }} backend: {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} service: name: {{ $fullName }} port: number: {{ $svcPort }} {{- else }} serviceName: {{ $fullName }} servicePort: {{ $svcPort }} {{- end }} {{- end }} {{- end }} {{- end }} ================================================ FILE: charts/templates/pvc.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {{ if .Values.persistence.enabled -}} kind: PersistentVolumeClaim apiVersion: v1 metadata: name: {{ include "answer.fullname" . }}-claim {{- with .Values.persistence.annotations }} annotations: {{ toYaml . | indent 4 }} {{- end }} labels: {{- include "answer.labels" . | nindent 4 }} spec: {{- if .Values.persistence.storageClass }} {{- if (eq "-" .Values.persistence.storageClass) }} storageClassName: "" {{- else }} storageClassName: "{{ .Values.persistence.storageClass }}" {{- end }} {{- end }} accessModes: - {{ .Values.persistence.accessMode | quote }} resources: requests: storage: {{ .Values.persistence.size | quote }} {{- with .Values.persistence.dataSource }} dataSource: name: {{ .name }} kind: {{ .kind | default "VolumeSnapshot" }} apiGroup: {{ .apiGroup | default "snapshot.storage.k8s.io" }} {{- end }} {{- end }} ================================================ FILE: charts/templates/service.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: Service metadata: name: {{ include "answer.fullname" . }} labels: {{- include "answer.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http selector: {{- include "answer.selectorLabels" . | nindent 4 }} ================================================ FILE: charts/templates/serviceaccount.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {{ if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "answer.serviceAccountName" . }} labels: {{- include "answer.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} {{- end }} ================================================ FILE: charts/values.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Default values for answer. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 image: repository: apache/answer pullPolicy: Always # Overrides the image tag whose default is the chart appVersion. tag: "latest" # Environment variables # Configure environment variables below # https://answer.apache.org/docs/env env: - name: LOG_LEVEL # [DEBUG INFO WARN ERROR] value: "INFO" # uncomment the below values to use AUTO_INSTALL and not have to go through the setup process. # Once used to do the initial setup, these variables won't be used moving forward. # You must at a minimum comment AUTO_INSTALL after initial setup to prevent an error about the database already being initiated. # - name: AUTO_INSTALL # value: "true" # - name: DB_TYPE # value: "sqlite3" # # DB_FILE Only for sqlite3 # - name: DB_FILE # value: "/data/answer.db" # - name: LANGUAGE # value: "en-US" # - name: SITE_NAME # value: "MyAnswer" # - name: SITE_URL # value: "http://localhost:80" # - name: CONTACT_EMAIL # value: "support@mydomain.com" # - name: ADMIN_NAME # # lowercase # value: "myadmin" # - name: ADMIN_PASSWORD # # 32 Characters MAX # value: "MyInsecurePasswordInTheRepo!" # # Use valueFrom to use a secret # # valueFrom: # # secretKeyRef: # # key: answer-admin-password # # name: answer-secrets # - name: ADMIN_EMAIL # value: "myAdmin@mydomain.com" # Configure extra containers extraContainers: [] # - name: cloudsql-proxy # image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.1.2 # command: # - /cloud-sql-proxy # args: # - project:region:instance # - --port=5432 # - --auto-iam-authn # ports: # - containerPort: 5432 # Persistence for the /data volume # Without persistence, your uploads and config.yaml will not be remembered between restarts. persistence: enabled: true # If set to "-", storageClassName: "", which disables dynamic provisioning # If undefined (the default) or set to null, no storageClassName spec is # set, choosing the default provisioner. (gp2 on AWS, standard on # GKE, AWS & OpenStack) # storageClass: "-" accessMode: ReadWriteOnce size: 5Gi annotations: {} # To restore a PVC from a VolumeSnapshot, set the dataSource; # the kind and apiGroup are optional and default to the shown values dataSource: {} # name: my-volume-snapshot # kind: VolumeSnapshot # apiGroup: snapshot.storage.k8s.io imagePullSecrets: [] nameOverride: "" fullnameOverride: "" serviceAccount: # Specifies whether a service account should be created create: true # Annotations to add to the service account annotations: {} # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" podAnnotations: {} podSecurityContext: {} # fsGroup: 2000 securityContext: {} # capabilities: # drop: # - ALL # readOnlyRootFilesystem: true # runAsNonRoot: true # runAsUser: 1000 service: type: ClusterIP port: 80 ingress: enabled: false className: "" annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - host: answer.local paths: - path: / pathType: ImplementationSpecific tls: [] # - secretName: answer-tls # hosts: # - answer.local resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # cpu: 100m # memory: 128Mi # requests: # cpu: 100m # memory: 128Mi autoscaling: enabled: false minReplicas: 1 maxReplicas: 100 targetCPUUtilizationPercentage: 80 # targetMemoryUtilizationPercentage: 80 nodeSelector: {} tolerations: [] affinity: {} ================================================ FILE: cmd/answer/main.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ //go:generate go run github.com/swaggo/swag/cmd/swag init -g ./cmd/answer/main.go -d ../../ -o ../../docs package main import ( answercmd "github.com/apache/answer/cmd" ) // main godoc // @title Apache Answer // @description Apache Answer API // @BasePath / // @securityDefinitions.apikey ApiKeyAuth // @in header // @name Authorization func main() { answercmd.Main() } ================================================ FILE: cmd/command.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package answercmd import ( "context" "fmt" "os" "strings" "github.com/apache/answer/internal/base/conf" "github.com/apache/answer/internal/base/path" "github.com/apache/answer/internal/cli" "github.com/apache/answer/internal/install" "github.com/apache/answer/internal/migrations" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/log" "github.com/spf13/cobra" ) var ( // dataDirPath save all answer application data in this directory. like config file, upload file... dataDirPath string // dumpDataPath dump data path dumpDataPath string // place to build new answer buildDir string // plugins needed to build in answer application buildWithPlugins []string // build output path buildOutput string // This config is used to upgrade the database from a specific version manually. // If you want to upgrade the database to version 1.1.0, you can use `answer upgrade -f v1.1.0`. upgradeVersion string // The fields that need to be set to the default value configFields []string // i18nSourcePath i18n from path i18nSourcePath string // i18nTargetPath i18n to path i18nTargetPath string // resetPasswordEmail user email for password reset resetPasswordEmail string // resetPasswordPassword new password for password reset resetPasswordPassword string ) func init() { rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time) rootCmd.PersistentFlags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/") dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/") buildCmd.Flags().StringSliceVarP(&buildWithPlugins, "with", "w", []string{}, "plugins needed to build") buildCmd.Flags().StringVarP(&buildOutput, "output", "o", "", "build output path") buildCmd.Flags().StringVarP(&buildDir, "build-dir", "b", "", "dir for build process") upgradeCmd.Flags().StringVarP(&upgradeVersion, "from", "f", "", "upgrade from specific version, eg: -f v1.1.0") configCmd.Flags().StringSliceVarP(&configFields, "with", "w", []string{}, "the fields that need to be set to the default value, eg: -w allow_password_login") i18nCmd.Flags().StringVarP(&i18nSourcePath, "source", "s", "", "i18n source path, eg: -s ./i18n/source") i18nCmd.Flags().StringVarP(&i18nTargetPath, "target", "t", "", "i18n target path, eg: -t ./i18n/target") resetPasswordCmd.Flags().StringVarP(&resetPasswordEmail, "email", "e", "", "user email address") resetPasswordCmd.Flags().StringVarP(&resetPasswordPassword, "password", "p", "", "new password (not recommended, will be recorded in shell history)") for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd, configCmd, i18nCmd, resetPasswordCmd} { rootCmd.AddCommand(cmd) } } var ( rootCmd = &cobra.Command{ Use: "answer", Short: "Answer is a minimalist open source Q&A community.", Long: `Answer is a minimalist open source Q&A community. To run answer, use: - 'answer init' to initialize the required environment. - 'answer run' to launch application.`, } runCmd = &cobra.Command{ Use: "run", Short: "Run Answer", Long: `Start running Answer`, Run: func(_ *cobra.Command, _ []string) { path.FormatAllPath(dataDirPath) fmt.Println("config file path: ", path.GetConfigFilePath()) fmt.Println("Answer is starting..........................") runApp() }, } initCmd = &cobra.Command{ Use: "init", Short: "Initialize Answer", Long: `Initialize Answer with specified configuration`, Run: func(_ *cobra.Command, _ []string) { // check config file and database. if config file exists and database is already created, init done cli.InstallAllInitialEnvironment(dataDirPath) configFileExist := cli.CheckConfigFile(path.GetConfigFilePath()) if configFileExist { fmt.Println("config file exists, try to read the config...") c, err := conf.ReadConfig(path.GetConfigFilePath()) if err != nil { fmt.Println("read config failed: ", err.Error()) return } fmt.Println("config file read successfully, try to connect database...") if cli.CheckDBTableExist(c.Data.Database) { fmt.Println("connect to database successfully and table already exists, do nothing.") return } } // start installation server to install install.Run(path.GetConfigFilePath()) }, } upgradeCmd = &cobra.Command{ Use: "upgrade", Short: "Upgrade Answer", Long: `Upgrade Answer to the latest version`, Run: func(_ *cobra.Command, _ []string) { log.SetLogger(log.NewStdLogger(os.Stdout)) path.FormatAllPath(dataDirPath) cli.InstallI18nBundle(true) c, err := conf.ReadConfig(path.GetConfigFilePath()) if err != nil { fmt.Println("read config failed: ", err.Error()) return } if err = migrations.Migrate(c.Debug, c.Data.Database, c.Data.Cache, upgradeVersion); err != nil { fmt.Println("migrate failed: ", err.Error()) return } fmt.Println("upgrade done") }, } dumpCmd = &cobra.Command{ Use: "dump", Short: "Back up data", Long: `Back up database into an SQL file`, Run: func(_ *cobra.Command, _ []string) { fmt.Println("Answer is backing up data") path.FormatAllPath(dataDirPath) c, err := conf.ReadConfig(path.GetConfigFilePath()) if err != nil { fmt.Println("read config failed: ", err.Error()) return } err = cli.DumpAllData(c.Data.Database, dumpDataPath) if err != nil { fmt.Println("dump failed: ", err.Error()) return } fmt.Println("Answer backed up the data successfully.") }, } checkCmd = &cobra.Command{ Use: "check", Short: "Check the required environment", Long: `Check if the current environment meets the startup requirements`, Run: func(_ *cobra.Command, _ []string) { path.FormatAllPath(dataDirPath) fmt.Println("Start checking the required environment...") if cli.CheckConfigFile(path.GetConfigFilePath()) { fmt.Println("config file exists [✔]") } else { fmt.Println("config file not exists [x]") } if cli.CheckUploadDir() { fmt.Println("upload directory exists [✔]") } else { fmt.Println("upload directory not exists [x]") } c, err := conf.ReadConfig(path.GetConfigFilePath()) if err != nil { fmt.Println("read config failed: ", err.Error()) return } if cli.CheckDBConnection(c.Data.Database) { fmt.Println("db connection successfully [✔]") } else { fmt.Println("db connection failed [x]") } fmt.Println("check environment all done") }, } buildCmd = &cobra.Command{ Use: "build", Short: "Build Answer with plugins", Long: `Build a new Answer with plugins that you need`, Run: func(_ *cobra.Command, _ []string) { fmt.Printf("try to build a new answer with plugins:\n%s\n", strings.Join(buildWithPlugins, "\n")) err := cli.BuildNewAnswer(buildDir, buildOutput, buildWithPlugins, cli.OriginalAnswerInfo{ Version: Version, Revision: Revision, Time: Time, }) if err != nil { fmt.Printf("build failed %v\n", err) os.Exit(1) } fmt.Printf("build new answer successfully %s\n", buildOutput) }, } pluginCmd = &cobra.Command{ Use: "plugin", Short: "Print all plugins packed in the binary", Long: `Print all plugins packed in the binary`, Run: func(_ *cobra.Command, _ []string) { _ = plugin.CallBase(func(base plugin.Base) error { info := base.Info() fmt.Printf("%s[%s] made by %s\n", info.SlugName, info.Version, info.Author) return nil }) }, } configCmd = &cobra.Command{ Use: "config", Short: "Set some config to default value", Long: `Set some config to default value`, Run: func(_ *cobra.Command, _ []string) { path.FormatAllPath(dataDirPath) c, err := conf.ReadConfig(path.GetConfigFilePath()) if err != nil { fmt.Println("read config failed: ", err.Error()) return } field := &cli.ConfigField{} fmt.Println(configFields) if len(configFields) > 0 { switch configFields[0] { case "allow_password_login": field.AllowPasswordLogin = true case "deactivate_plugin": if len(configFields) > 1 { field.DeactivatePluginSlugName = configFields[1] } default: fmt.Printf("field %s not support\n", configFields[0]) } } err = cli.SetDefaultConfig(c.Data.Database, c.Data.Cache, field) if err != nil { fmt.Println("set default config failed: ", err.Error()) } else { fmt.Println("set default config successfully") } }, } i18nCmd = &cobra.Command{ Use: "i18n", Short: "Overwrite i18n files", Long: `Merge i18n files from plugins to original i18n files. It will overwrite the original i18n files`, Run: func(_ *cobra.Command, _ []string) { if err := cli.ReplaceI18nFilesLocal(i18nTargetPath); err != nil { fmt.Printf("replace i18n files failed %v\n", err) } else { fmt.Printf("replace i18n files successfully\n") } fmt.Printf("try to merge i18n files from %q to %q\n", i18nSourcePath, i18nTargetPath) if err := cli.MergeI18nFilesLocal(i18nTargetPath, i18nSourcePath); err != nil { fmt.Printf("merge i18n files failed %v\n", err) } else { fmt.Printf("merge i18n files successfully\n") } }, } resetPasswordCmd = &cobra.Command{ Use: "passwd", Aliases: []string{"password", "reset-password"}, Short: "Reset user password", Long: "Reset user password by email address.", Example: ` # Interactive mode (recommended, safest) answer passwd -C ./answer-data # Specify email only (will prompt for password securely) answer passwd -C ./answer-data --email user@example.com answer passwd -C ./answer-data -e user@example.com # Specify email and password (NOT recommended, will be recorded in shell history) answer passwd -C ./answer-data -e user@example.com -p newpassword123`, Run: func(cmd *cobra.Command, args []string) { opts := &cli.ResetPasswordOptions{ Email: resetPasswordEmail, Password: resetPasswordPassword, } if err := cli.ResetPassword(context.Background(), dataDirPath, opts); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } }, } ) // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main(). It only needs to happen once to the rootCmd. func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } ================================================ FILE: cmd/main.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package answercmd import ( "context" "fmt" "os" "time" "github.com/apache/answer/internal/base/conf" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/cron" "github.com/apache/answer/internal/base/path" "github.com/apache/answer/internal/schema" "github.com/gin-gonic/gin" "github.com/joho/godotenv" "github.com/segmentfault/pacman" "github.com/segmentfault/pacman/contrib/log/zap" "github.com/segmentfault/pacman/contrib/server/http" "github.com/segmentfault/pacman/log" ) func init() { // Load .env if present, ignore error to keep backward compatibility _ = godotenv.Load() } // go build -ldflags "-X github.com/apache/answer/cmd.Version=x.y.z" var ( // Name is the name of the project Name = "answer" // Version is the version of the project Version = "0.0.0" // Revision is the git short commit revision number // If built without a Git repository, this field will be empty. Revision = "" // Time is the build time of the project Time = "" // GoVersion is the go version of the project GoVersion = "1.23" // log level logLevel = os.Getenv("LOG_LEVEL") // log path logPath = os.Getenv("LOG_PATH") ) // Main // @securityDefinitions.apikey ApiKeyAuth // @in header // @name Authorization func Main() { log.SetLogger(zap.NewLogger( log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath))) Execute() } func runApp() { c, err := conf.ReadConfig(path.GetConfigFilePath()) if err != nil { panic(err) } app, cleanup, err := initApplication( c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, c.UI, log.GetLogger()) if err != nil { panic(err) } constant.Version = Version constant.Revision = Revision constant.GoVersion = GoVersion schema.AppStartTime = time.Now() fmt.Println("answer Version:", constant.Version, " Revision:", constant.Revision) defer cleanup() if err := app.Run(context.Background()); err != nil { panic(err) } } func newApplication(serverConf *conf.Server, server *gin.Engine, manager *cron.ScheduledTaskManager) *pacman.Application { manager.Run() return pacman.NewApp( pacman.WithName(Name), pacman.WithVersion(Version), pacman.WithServer(http.NewServer(server, serverConf.HTTP.Addr)), ) } ================================================ FILE: cmd/wire.go ================================================ //go:build wireinject /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // The build tag makes sure the stub is not built in the final build. package answercmd import ( "github.com/apache/answer/internal/base/conf" "github.com/apache/answer/internal/base/cron" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/server" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/controller" templaterender "github.com/apache/answer/internal/controller/template_render" "github.com/apache/answer/internal/controller_admin" "github.com/apache/answer/internal/repo" "github.com/apache/answer/internal/router" "github.com/apache/answer/internal/service" "github.com/apache/answer/internal/service/service_config" "github.com/google/wire" "github.com/segmentfault/pacman" "github.com/segmentfault/pacman/log" ) // initApplication init application. func initApplication( debug bool, serverConf *conf.Server, dbConf *data.Database, cacheConf *data.CacheConf, i18nConf *translator.I18n, swaggerConf *router.SwaggerConfig, serviceConf *service_config.ServiceConfig, uiConf *server.UI, logConf log.Logger) (*pacman.Application, func(), error) { panic(wire.Build( server.ProviderSetServer, router.ProviderSetRouter, controller.ProviderSetController, controller_admin.ProviderSetController, templaterender.ProviderSetTemplateRenderController, service.ProviderSetService, cron.ProviderSetService, repo.ProviderSetRepo, translator.ProviderSet, middleware.ProviderSetMiddleware, newApplication, )) } ================================================ FILE: cmd/wire_gen.go ================================================ //go:build !wireinject // +build !wireinject /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Code generated by Wire. DO NOT EDIT. //go:generate go run github.com/google/wire/cmd/wire package answercmd import ( "github.com/apache/answer/internal/base/conf" "github.com/apache/answer/internal/base/cron" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/server" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/controller" "github.com/apache/answer/internal/controller/template_render" "github.com/apache/answer/internal/controller_admin" "github.com/apache/answer/internal/repo/activity" "github.com/apache/answer/internal/repo/activity_common" "github.com/apache/answer/internal/repo/ai_conversation" "github.com/apache/answer/internal/repo/answer" "github.com/apache/answer/internal/repo/api_key" "github.com/apache/answer/internal/repo/auth" "github.com/apache/answer/internal/repo/badge" "github.com/apache/answer/internal/repo/badge_award" "github.com/apache/answer/internal/repo/badge_group" "github.com/apache/answer/internal/repo/captcha" "github.com/apache/answer/internal/repo/collection" "github.com/apache/answer/internal/repo/comment" "github.com/apache/answer/internal/repo/config" "github.com/apache/answer/internal/repo/export" "github.com/apache/answer/internal/repo/file_record" "github.com/apache/answer/internal/repo/limit" "github.com/apache/answer/internal/repo/meta" notification2 "github.com/apache/answer/internal/repo/notification" "github.com/apache/answer/internal/repo/plugin_config" "github.com/apache/answer/internal/repo/question" "github.com/apache/answer/internal/repo/rank" "github.com/apache/answer/internal/repo/reason" "github.com/apache/answer/internal/repo/report" "github.com/apache/answer/internal/repo/review" "github.com/apache/answer/internal/repo/revision" "github.com/apache/answer/internal/repo/role" "github.com/apache/answer/internal/repo/search_common" "github.com/apache/answer/internal/repo/site_info" "github.com/apache/answer/internal/repo/tag" "github.com/apache/answer/internal/repo/tag_common" "github.com/apache/answer/internal/repo/unique" "github.com/apache/answer/internal/repo/user" "github.com/apache/answer/internal/repo/user_external_login" "github.com/apache/answer/internal/repo/user_notification_config" "github.com/apache/answer/internal/router" "github.com/apache/answer/internal/service/action" activity2 "github.com/apache/answer/internal/service/activity" activity_common2 "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/activityqueue" ai_conversation2 "github.com/apache/answer/internal/service/ai_conversation" "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/apikey" auth2 "github.com/apache/answer/internal/service/auth" badge2 "github.com/apache/answer/internal/service/badge" collection2 "github.com/apache/answer/internal/service/collection" "github.com/apache/answer/internal/service/collection_common" comment2 "github.com/apache/answer/internal/service/comment" "github.com/apache/answer/internal/service/comment_common" config2 "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/dashboard" "github.com/apache/answer/internal/service/eventqueue" export2 "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/feature_toggle" file_record2 "github.com/apache/answer/internal/service/file_record" "github.com/apache/answer/internal/service/follow" "github.com/apache/answer/internal/service/importer" meta2 "github.com/apache/answer/internal/service/meta" "github.com/apache/answer/internal/service/meta_common" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/notification" "github.com/apache/answer/internal/service/notification_common" "github.com/apache/answer/internal/service/object_info" "github.com/apache/answer/internal/service/plugin_common" "github.com/apache/answer/internal/service/question_common" rank2 "github.com/apache/answer/internal/service/rank" reason2 "github.com/apache/answer/internal/service/reason" report2 "github.com/apache/answer/internal/service/report" "github.com/apache/answer/internal/service/report_handle" review2 "github.com/apache/answer/internal/service/review" "github.com/apache/answer/internal/service/revision_common" role2 "github.com/apache/answer/internal/service/role" "github.com/apache/answer/internal/service/search_parser" "github.com/apache/answer/internal/service/service_config" "github.com/apache/answer/internal/service/siteinfo" "github.com/apache/answer/internal/service/siteinfo_common" tag2 "github.com/apache/answer/internal/service/tag" tag_common2 "github.com/apache/answer/internal/service/tag_common" "github.com/apache/answer/internal/service/uploader" "github.com/apache/answer/internal/service/user_admin" "github.com/apache/answer/internal/service/user_common" user_external_login2 "github.com/apache/answer/internal/service/user_external_login" user_notification_config2 "github.com/apache/answer/internal/service/user_notification_config" "github.com/segmentfault/pacman" "github.com/segmentfault/pacman/log" ) // Injectors from wire.go: // initApplication init application. func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cacheConf *data.CacheConf, i18nConf *translator.I18n, swaggerConf *router.SwaggerConfig, serviceConf *service_config.ServiceConfig, uiConf *server.UI, logConf log.Logger) (*pacman.Application, func(), error) { staticRouter := router.NewStaticRouter(serviceConf) i18nTranslator, err := translator.NewTranslator(i18nConf) if err != nil { return nil, nil, err } engine, err := data.NewDB(debug, dbConf) if err != nil { return nil, nil, err } cache, cleanup, err := data.NewCache(cacheConf) if err != nil { return nil, nil, err } dataData, cleanup2, err := data.NewData(engine, cache) if err != nil { cleanup() return nil, nil, err } siteInfoRepo := site_info.NewSiteInfo(dataData) siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) langController := controller.NewLangController(i18nTranslator, siteInfoCommonService) authRepo := auth.NewAuthRepo(dataData) apiKeyRepo := api_key.NewAPIKeyRepo(dataData) authService := auth2.NewAuthService(authRepo, apiKeyRepo) userRepo := user.NewUserRepo(dataData) uniqueIDRepo := unique.NewUniqueIDRepo(dataData) configRepo := config.NewConfigRepo(dataData) configService := config2.NewConfigService(configRepo) activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configService) userRankRepo := rank.NewUserRankRepo(dataData, configService) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configService) emailRepo := export.NewEmailRepo(dataData) emailService := export2.NewEmailService(configService, emailRepo, siteInfoCommonService) userRoleRelRepo := role.NewUserRoleRelRepo(dataData) roleRepo := role.NewRoleRepo(dataData) roleService := role2.NewRoleService(roleRepo) userRoleRelService := role2.NewUserRoleRelService(userRoleRelRepo, roleService) userCommon := usercommon.NewUserCommon(userRepo, userRoleRelService, authService, siteInfoCommonService) userExternalLoginRepo := user_external_login.NewUserExternalLoginRepo(dataData) userNotificationConfigRepo := user_notification_config.NewUserNotificationConfigRepo(dataData) userNotificationConfigService := user_notification_config2.NewUserNotificationConfigService(userRepo, userNotificationConfigRepo) userExternalLoginService := user_external_login2.NewUserExternalLoginService(userRepo, userCommon, userExternalLoginRepo, emailService, siteInfoCommonService, userActiveActivityRepo, userNotificationConfigService) questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo) answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo) voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo) tagRelRepo := tag.NewTagRelRepo(dataData, uniqueIDRepo) tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo) revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo) revisionService := revision_common.NewRevisionService(revisionRepo, userRepo) service := activityqueue.NewService() tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService, service) collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo) collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo) answerCommon := answercommon.NewAnswerCommon(answerRepo) metaRepo := meta.NewMetaRepo(dataData) metaCommonService := metacommon.NewMetaCommonService(metaRepo) questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaCommonService, configService, service, revisionRepo, siteInfoCommonService, dataData) eventqueueService := eventqueue.NewService() fileRecordRepo := file_record.NewFileRecordRepo(dataData) fileRecordService := file_record2.NewFileRecordService(fileRecordRepo, revisionRepo, serviceConf, siteInfoCommonService, userCommon) userService := content.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo, userNotificationConfigService, questionCommon, eventqueueService, fileRecordService) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) userController := controller.NewUserController(authService, userService, captchaService, emailService, siteInfoCommonService, userNotificationConfigService) commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService) noticequeueService := noticequeue.NewService() externalService := noticequeue.NewExternalService() reviewRepo := review.NewReviewRepo(dataData) reviewService := review2.NewReviewService(reviewRepo, objService, userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, externalService, tagCommonService, questionCommon, noticequeueService, siteInfoCommonService, commentCommonRepo) commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, noticequeueService, externalService, service, eventqueueService, reviewService) rolePowerRelRepo := role.NewRolePowerRelRepo(dataData) rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService) rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configService) limitRepo := limit.NewRateLimitRepo(dataData) rateLimitMiddleware := middleware.NewRateLimitMiddleware(limitRepo) commentController := controller.NewCommentController(commentService, rankService, captchaService, rateLimitMiddleware) reportRepo := report.NewReportRepo(dataData, uniqueIDRepo) tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, service) answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, noticequeueService) answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService) externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalService, userExternalLoginRepo, siteInfoCommonService) questionService := content.NewQuestionService(activityRepo, questionRepo, answerRepo, tagCommonService, tagService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, collectionCommon, answerActivityService, emailService, noticequeueService, externalService, service, siteInfoCommonService, externalNotificationService, reviewService, configService, eventqueueService, reviewRepo) answerService := content.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, noticequeueService, externalService, service, reviewService, eventqueueService) reportHandle := report_handle.NewReportHandle(questionService, answerService, commentService) reportService := report2.NewReportService(reportRepo, objService, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService, eventqueueService) reportController := controller.NewReportController(reportService, rankService, captchaService) contentVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, noticequeueService) voteService := content.NewVoteService(contentVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService, eventqueueService) voteController := controller.NewVoteController(voteService, rankService, captchaService) tagController := controller.NewTagController(tagService, tagCommonService, rankService) followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo) followController := controller.NewFollowController(followService) collectionGroupRepo := collection.NewCollectionGroupRepo(dataData) collectionService := collection2.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon) collectionController := controller.NewCollectionController(collectionService) questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService, captchaService, rateLimitMiddleware) answerController := controller.NewAnswerController(answerService, rankService, captchaService, siteInfoCommonService, rateLimitMiddleware) searchParser := search_parser.NewSearchParser(tagCommonService, userCommon) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon, tagCommonService) searchService := content.NewSearchService(searchParser, searchRepo) searchController := controller.NewSearchController(searchService, captchaService) reviewActivityRepo := activity.NewReviewActivityRepo(dataData, activityRepo, userRankRepo, configService) contentRevisionService := content.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService, noticequeueService, service, reportRepo, reviewService, reviewActivityRepo) revisionController := controller.NewRevisionController(contentRevisionService, rankService) rankController := controller.NewRankController(rankService) userAdminRepo := user.NewUserAdminRepo(dataData, authRepo) notificationRepo := notification2.NewNotificationRepo(dataData) pluginUserConfigRepo := plugin_config.NewPluginUserConfigRepo(dataData) badgeAwardRepo := badge_award.NewBadgeAwardRepo(dataData, uniqueIDRepo) userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo, siteInfoCommonService, emailService, questionRepo, answerRepo, commentCommonRepo, userExternalLoginRepo, notificationRepo, pluginUserConfigRepo, badgeAwardRepo) userAdminController := controller_admin.NewUserAdminController(userAdminService) reasonRepo := reason.NewReasonRepo(configService) reasonService := reason2.NewReasonService(reasonRepo) reasonController := controller.NewReasonController(reasonService) themeController := controller_admin.NewThemeController() siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configService, questionCommon, fileRecordService) siteInfoController := controller_admin.NewSiteInfoController(siteInfoService) controllerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, noticequeueService, userExternalLoginRepo, siteInfoCommonService) badgeRepo := badge.NewBadgeRepo(dataData, uniqueIDRepo) notificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService, userRepo, reportRepo, reviewService, badgeRepo) notificationController := controller.NewNotificationController(notificationService, rankService) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, reviewService, revisionRepo, dataData) dashboardController := controller.NewDashboardController(dashboardService) uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService, fileRecordService) uploadController := controller.NewUploadController(uploaderService) activityActivityRepo := activity.NewActivityRepo(dataData, configService) activityCommon := activity_common2.NewActivityCommon(activityRepo, service) commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo) activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaCommonService, configService) activityController := controller.NewActivityController(activityService) roleController := controller_admin.NewRoleController(roleService) pluginConfigRepo := plugin_config.NewPluginConfigRepo(dataData) importerService := importer.NewImporterService(questionService, rankService, userCommon) pluginCommonService := plugin_common.NewPluginCommonService(pluginConfigRepo, pluginUserConfigRepo, configService, dataData, importerService) pluginController := controller_admin.NewPluginController(pluginCommonService) permissionController := controller.NewPermissionController(rankService) userPluginController := controller.NewUserPluginController(pluginCommonService) reviewController := controller.NewReviewController(reviewService, rankService, captchaService) metaService := meta2.NewMetaService(metaCommonService, userCommon, answerRepo, questionRepo, eventqueueService) metaController := controller.NewMetaController(metaService) badgeGroupRepo := badge_group.NewBadgeGroupRepo(dataData, uniqueIDRepo) eventRuleRepo := badge.NewEventRuleRepo(dataData) badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, badgeRepo, userCommon, objService, noticequeueService) badgeEventService := badge2.NewBadgeEventService(dataData, eventqueueService, badgeRepo, eventRuleRepo, badgeAwardService) badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService, siteInfoCommonService) badgeController := controller.NewBadgeController(badgeService, badgeAwardService) controller_adminBadgeController := controller_admin.NewBadgeController(badgeService) apiKeyService := apikey.NewAPIKeyService(apiKeyRepo) adminAPIKeyController := controller_admin.NewAdminAPIKeyController(apiKeyService) featureToggleService := feature_toggle.NewFeatureToggleService(siteInfoRepo) mcpController := controller.NewMCPController(searchService, siteInfoCommonService, tagCommonService, questionCommon, commentRepo, userCommon, answerRepo, featureToggleService) aiConversationRepo := ai_conversation.NewAIConversationRepo(dataData) aiConversationService := ai_conversation2.NewAIConversationService(aiConversationRepo, userCommon) aiController := controller.NewAIController(searchService, siteInfoCommonService, tagCommonService, questionCommon, commentRepo, userCommon, answerRepo, mcpController, aiConversationService, featureToggleService) aiConversationController := controller.NewAIConversationController(aiConversationService, featureToggleService) aiConversationAdminController := controller_admin.NewAIConversationAdminController(aiConversationService, featureToggleService) answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController, controller_adminBadgeController, adminAPIKeyController, aiController, aiConversationController, aiConversationAdminController, mcpController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService) authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService) avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService) shortIDMiddleware := middleware.NewShortIDMiddleware(siteInfoCommonService) templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, siteInfoCommonService, questionRepo) templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService, eventqueueService, userService, questionService) templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController, authUserMiddleware) connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService) userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService) userCenterController := controller.NewUserCenterController(userCenterLoginService, siteInfoCommonService) captchaController := controller.NewCaptchaController() embedController := controller.NewEmbedController() renderController := controller.NewRenderController() sidebarController := controller.NewSidebarController() pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController, captchaController, embedController, renderController, sidebarController) ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, shortIDMiddleware, templateRouter, pluginAPIRouter, uiConf) scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService, fileRecordService, userAdminService, serviceConf) application := newApplication(serverConf, ginEngine, scheduledTaskManager) return application, func() { cleanup2() cleanup() }, nil } ================================================ FILE: configs/config.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package configs import _ "embed" //go:embed config.yaml var Config []byte //go:embed path_ignore.yaml var PathIgnore []byte //go:embed reserved-usernames.json var ReservedUsernames []byte ================================================ FILE: configs/config.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. server: http: addr: 0.0.0.0:80 data: database: driver: "sqlite3" connection: "/data/sqlite3/answer.db" cache: file_path: "/data/cache/cache.db" i18n: bundle_dir: "/data/i18n" swaggerui: show: true protocol: http host: 127.0.0.1 address: ':80' service_config: upload_path: "/data/uploads" clean_up_uploads: true clean_orphan_uploads_period_hours: 48 purge_deleted_files_period_days: 30 ui: public_url: '/' api_url: '/' base_url: '' api_base_url: '' ================================================ FILE: configs/path_ignore.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # url path reserves the keywords list questions: - ask tags: - create users: - unsubscribe - settings - login - register - account-recovery - change-email - password-reset - account-activation - confirm-new-email - account-suspended - confirm-email - auth-landing ================================================ FILE: configs/reserved-usernames.json ================================================ ["0","100","101","102","1xx","200","201","202","203","204","205","206","207","226","2xx","300","301","302","303","304","305","307","308","3xx","400","401","402","403","404","405","406","407","408","409","410","411","412","413","414","415","416","417","418","422","423","424","426","428","429","431","451","4xx","500","501","502","503","504","505","506","507","511","5xx","7xx","about","abuse","access","account","account-activation","account-recovery","account-suspended","accounts","activate","activities","activity","ad","add","address","adm","admin","administration","administrator","ads","adult","advertising","affiliate","affiliates","ajax","all","alpha","analysis","analytics","android","anon","anonymous","api","app","apps","archive","archives","article","asct","asset","atom","auth","auth-landing","authentication","autoconfig","avatar","backup","balancer-manager","bank","banner","banners","beta","billing","bin","blog","blogs","board","book","bookmark","bot","bots","broadcasthost","bug","bugs","business","cache","cadastro","calendar","call","campaign","cancel","captcha","career","careers","cart","categories","category","cgi","cgi-bin","changelog","change-email","chat","check","checking","checkout","client","cliente","clients","code","codereview","comercial","comment","comments","communities","community","company","compare","compras","config","configuration","confirm-new-email","confirm-email","connect","contact","contact-us","contact_us","contactus","contest","contribute","corp","create","crypt","css","dashboard","data","db","default","delete","demo","design","designer","destroy","dev","devel","developer","developers","diagram","diary","dict","dictionary","die","dir","direct_messages","directory","dist","dns","doc","docker","docs","documentation","domain","download","downloads","ecommerce","edit","editor","edu","education","email","employment","empty","end","enterprise","entries","entry","error","errors","eval","event","everyone","exit","explore","export","facebook","faq","favorite","favorites","fbi","feature","features","feed","feedback","feeds","file","files","firewall","first","flash","fleet","fleets","flog","follow","followers","following","forgot","forgot-password","forgot_password","forgotpassword","form","forum","forums","founder","free","friend","friends","ftp","gadget","gadgets","game","games","get","ghost","gift","gifts","gist","git","github","graph","group","groups","guest","guests","help","home","homepage","hooks","host","hosting","hostmaster","hostname","howto","hpg","html","http","httpd","https","i","iamges","icon","icons","id","idea","ideas","image","images","imap","img","index","indice","info","information","inquiry","instagram","intranet","invitations","invite","ip","ipad","iphone","irc","is","isatap","issue","issues","it","item","items","java","javascript","job","jobs","join","js","json","jump","keys","keyserver","knowledgebase","language","languages","last","ldap-status","legal","license","link","links","linux","list","lists","local","localdomain","localhost","log","log-in","log-out","log_in","log_out","login","logout","logs","m","mac","mail","mail1","mail2","mail3","mail4","mail5","mailer","mailer-daemon","mailing","maintenance","manager","manual","map","maps","marketing","master","me","media","member","members","message","messages","messenger","microblog","microblogs","mine","mis","mob","mobile","movie","movies","mp3","msg","msn","music","musicas","mx","my","mysql","name","named","names","namespace","namespaces","nan","navi","navigation","net","network","new","news","newsletter","nick","nickname","no-reply","nobody","noc","noreply","notes","noticias","notification","notifications","notify","ns","ns1","ns10","ns2","ns3","ns4","ns5","ns6","ns7","ns8","ns9","null","oauth","oauth_clients","offer","offers","official","old","online","openid","operator","ops","order","orders","organization","organizations","orgs","overview","owner","owners","package","page","pager","pages","panel","passwd","password","password-reset","patch","payment","perl","phone","photo","photoalbum","photos","php","phpmyadmin","phppgadmin","phpredisadmin","pic","pics","ping","plan","plans","plugin","plugins","policy","pop","pop3","popular","portal","post","postfix","postmaster","posts","pr","premium","press","price","pricing","privacy","privacy-policy","privacy_policy","privacypolicy","private","product","products","profile","project","projects","promo","pub","public","purpose","put","pw","python","query","random","ranking","read","readme","recent","recruit","recruitment","register","registration","release","releases","remote","remove","replies","reply","report","reports","repositories","repository","req","request","requests","res","reset","reset-password","reset_password","resetpassword","resource","resources","roc","root","rss","ruby","rule","rules","sag","sale","sales","sample","samples","save","school","script","scripts","search","secure","security","self","send","server","server-info","server-status","service","services","session","sessions","setting","settings","setup","share","shop","show","sign-in","sign-up","sign_in","sign_up","signin","signout","signup","site","sitemap","sites","smartphone","smtp","soporte","source","spec","special","sql","src","ssh","ssl","ssladmin","ssladministrator","sslwebmaster","staff","stage","staging","start","stat","state","static","stats","status","store","stores","stories","style","styleguide","styles","stylesheet","stylesheets","subdomain","subscribe","subscriptions","suporte","support","svn","swf","sys","sysadmin","sysadministrator","system","tablet","tablets","tag","tags","talk","task","tasks","team","teams","tech","telnet","term","terms","terms-of-service","terms_of_service","termsofservice","test","test1","test2","test3","teste","testing","tests","theme","themes","thread","threads","tls","tmp","todo","token","tokenserver","tool","tools","top","topic","topics","tos","tour","translations","trends","tutorial","tux","tv","twitter","undef","unfollow","unsubscribe","update","upload","uploads","uptime","url","usage","usenet","user","username","users","usr","usuario","util","uucp","vendas","ver","version","video","videos","visitor","vpn","watch","weather","web","webhook","webhooks","webmail","webmaster","website","websites","welcome","widget","widgets","wiki","win","windows","word","work","works","workshop","wpad","ww","wws","www","www1","www2","www3","www4","www5","www6","www7","wwws","wwww","xfn","xml","xmpp","xpg","xxx","yaml","year","yml","you","yourdomain","yourname","yoursite","yourusername"] ================================================ FILE: crowdin.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. files: - source: /i18n/en_US.yaml translation: /i18n/%locale_with_underscore%.yaml ================================================ FILE: docker-compose.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. version: "3" services: answer: image: apache/answer ports: - '9080:80' restart: on-failure volumes: - answer-data:/data volumes: answer-data: ================================================ FILE: docs/docs.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", "contact": {}, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { "/": { "get": { "description": "if config file not exist try to redirect to install page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "installation" ], "summary": "if config file not exist try to redirect to install page", "responses": {} } }, "/answer/admin/api/ai-config": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get AI configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get AI configuration", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteAIResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update AI configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update AI configuration", "parameters": [ { "description": "AI config", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteAIReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/ai-models": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "get AI models", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get AI models", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetAIModelResp" } } } } ] } } } } }, "/answer/admin/api/ai-provider": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get AI provider configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get AI provider configuration", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetAIProviderResp" } } } } ] } } } } }, "/answer/admin/api/ai/conversation": { "get": { "description": "get conversation detail for admin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation-admin" ], "summary": "get conversation detail for admin", "parameters": [ { "type": "string", "description": "conversation id", "name": "conversation_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.AIConversationAdminDetailResp" } } } ] } } } }, "delete": { "description": "delete conversation and its related records for admin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation-admin" ], "summary": "delete conversation for admin", "parameters": [ { "description": "apikey", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AIConversationAdminDeleteReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/ai/conversation/page": { "get": { "description": "get conversation list for admin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation-admin" ], "summary": "get conversation list for admin", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.AIConversationAdminListItem" } } } } ] } } } ] } } } } }, "/answer/admin/api/answer/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Status:[available,deleted,pending]", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "AdminAnswerPage admin answer page", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "enum": [ "available", "deleted", "pending" ], "type": "string", "description": "user status", "name": "status", "in": "query" }, { "type": "string", "description": "answer id or question title", "name": "query", "in": "query" }, { "type": "string", "description": "question id", "name": "question_id", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/answer/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update answer status", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update answer status", "parameters": [ { "description": "AdminUpdateAnswerStatusReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AdminUpdateAnswerStatusReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/api-key": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update apikey", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update apikey", "parameters": [ { "description": "apikey", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateAPIKeyReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add apikey", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "add apikey", "parameters": [ { "description": "apikey", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddAPIKeyReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.AddAPIKeyResp" } } } ] } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "delete apikey", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "delete apikey", "parameters": [ { "description": "apikey", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.DeleteAPIKeyReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/api-key/all": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get all api keys", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get all api keys", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetAPIKeyResp" } } } } ] } } } } }, "/answer/admin/api/badge/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update badge status", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AdminBadge" ], "summary": "update badge status", "parameters": [ { "description": "UpdateBadgeStatusReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateBadgeStatusReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/badges": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "list all badges by page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AdminBadge" ], "summary": "list all badges by page", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "enum": [ "", "active", "inactive" ], "type": "string", "description": "badge status", "name": "status", "in": "query" }, { "type": "string", "description": "search param", "name": "q", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetBadgeListPagedResp" } } } } ] } } } } }, "/answer/admin/api/dashboard": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "DashboardInfo", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "DashboardInfo", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/delete/permanently": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "delete permanently", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "delete permanently", "parameters": [ { "description": "DeletePermanentlyReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.DeletePermanentlyReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/language/options": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Get language options", "produces": [ "application/json" ], "tags": [ "Lang" ], "summary": "Get language options", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/mcp-config": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get MCP configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get MCP configuration", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteMCPResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update MCP configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update MCP configuration", "parameters": [ { "description": "MCP config", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteMCPReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/plugin/config": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get plugin config", "produces": [ "application/json" ], "tags": [ "AdminPlugin" ], "summary": "get plugin config", "parameters": [ { "type": "string", "description": "plugin_slug_name", "name": "plugin_slug_name", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetPluginConfigResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update plugin config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AdminPlugin" ], "summary": "update plugin config", "parameters": [ { "description": "UpdatePluginConfigReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdatePluginConfigReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/plugin/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update plugin status", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AdminPlugin" ], "summary": "update plugin status", "parameters": [ { "description": "UpdatePluginStatusReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdatePluginStatusReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/plugins": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get plugin list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AdminPlugin" ], "summary": "get plugin list", "parameters": [ { "type": "string", "description": "status: active/inactive", "name": "status", "in": "query" }, { "type": "boolean", "description": "have config", "name": "have_config", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetPluginListResp" } } } } ] } } } } }, "/answer/admin/api/question/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Status:[available,closed,deleted,pending]", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "AdminQuestionPage admin question page", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "enum": [ "available", "closed", "deleted", "pending" ], "type": "string", "description": "user status", "name": "status", "in": "query" }, { "type": "string", "description": "question id or title", "name": "query", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/question/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update question status", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update question status", "parameters": [ { "description": "AdminUpdateQuestionStatusReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AdminUpdateQuestionStatusReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/reasons": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get reasons by object type and action", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "reason" ], "summary": "get reasons by object type and action", "parameters": [ { "enum": [ "question", "answer", "comment", "user" ], "type": "string", "description": "object_type", "name": "object_type", "in": "query", "required": true }, { "enum": [ "status", "close", "flag", "review" ], "type": "string", "description": "action", "name": "action", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/roles": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get role list", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get role list", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetRoleResp" } } } } ] } } } } }, "/answer/admin/api/setting/privileges": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "GetPrivilegesConfig get privileges config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "GetPrivilegesConfig get privileges config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetPrivilegesConfigResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update privileges config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update privileges config", "parameters": [ { "description": "config", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdatePrivilegesConfigReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/setting/smtp": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "GetSMTPConfig get smtp config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "GetSMTPConfig get smtp config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetSMTPConfigResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update smtp config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update smtp config", "parameters": [ { "description": "smtp config", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateSMTPConfigReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/advanced": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site advanced setting", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site advanced setting", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteAdvancedResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site advanced info", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site advanced info", "parameters": [ { "description": "advanced settings", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteAdvancedReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/branding": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site interface", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteBrandingResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site info branding", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site info branding", "parameters": [ { "description": "branding info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteBrandingReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/custom-css-html": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site info custom html css config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site info custom html css config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site custom css html config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site custom css html config", "parameters": [ { "description": "login info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteCustomCssHTMLReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/general": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site general information", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteGeneralResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site general information", "parameters": [ { "description": "general", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteGeneralReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/interface": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site interface", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteInterfaceSettingsResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site info interface", "parameters": [ { "description": "general", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteInterfaceReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/login": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site info login config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site info login config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteLoginResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site login", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site login", "parameters": [ { "description": "login info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteLoginReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/polices": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Get the policies information for the site", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "Get the policies information for the site", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SitePoliciesResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site policies configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site policies configuration", "parameters": [ { "description": "write info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SitePoliciesReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/question": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site questions setting", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site questions setting", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteQuestionsResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site question settings", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site question settings", "parameters": [ { "description": "questions settings", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteQuestionsReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/security": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Get the security information for the site", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "Get the security information for the site", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteSecurityResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site security configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site security configuration", "parameters": [ { "description": "write info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteSecurityReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/seo": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site seo information", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site seo information", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteSeoResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site seo information", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site seo information", "parameters": [ { "description": "seo", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteSeoReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/tag": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site tags setting", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site tags setting", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteTagsResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site tag settings", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site tag settings", "parameters": [ { "description": "tags settings", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteTagsReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/theme": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site info theme config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site info theme config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteThemeResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site custom css html config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site custom css html config", "parameters": [ { "description": "login info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteThemeReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/users": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site user config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site user config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteUsersResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site info config about users", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site info config about users", "parameters": [ { "description": "users info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteUsersReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/users-settings": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site interface", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteUsersSettingsResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site info users settings", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site info users settings", "parameters": [ { "description": "general", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteUsersSettingsReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/theme/options": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Get theme options", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "Get theme options", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/user": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "add user", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddUserReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/user/activation": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user activation", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get user activation", "parameters": [ { "type": "string", "description": "user id", "name": "user_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetUserActivationResp" } } } ] } } } } }, "/answer/admin/api/user/password": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user password", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update user password", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserPasswordReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/user/profile": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "edit user profile", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "edit user profile", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.EditUserProfileReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/user/role": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user role", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update user role", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserRoleReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/user/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update user", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserStatusReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/users": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add users", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "add users", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddUsersReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/users/activation": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "send user activation", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "send user activation", "parameters": [ { "description": "SendUserActivationReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SendUserActivationReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/users/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user page", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get user page", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "search query: email, username or id:[id]", "name": "query", "in": "query" }, { "type": "boolean", "description": "staff user", "name": "staff", "in": "query" }, { "enum": [ "suspended", "deleted", "inactive" ], "type": "string", "description": "user status", "name": "status", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "records": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUserPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/activity/timeline": { "get": { "description": "get object timeline", "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "get object timeline", "parameters": [ { "type": "string", "description": "object id", "name": "object_id", "in": "query" }, { "type": "string", "description": "tag slug name", "name": "tag_slug_name", "in": "query" }, { "enum": [ "question", "answer", "tag" ], "type": "string", "description": "object type", "name": "object_type", "in": "query" }, { "type": "boolean", "description": "is show vote", "name": "show_vote", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetObjectTimelineResp" } } } ] } } } } }, "/answer/api/v1/activity/timeline/detail": { "get": { "description": "get object timeline detail", "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "get object timeline detail", "parameters": [ { "type": "string", "description": "revision id", "name": "revision_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetObjectTimelineResp" } } } ] } } } } }, "/answer/api/v1/ai/conversation": { "get": { "description": "get conversation detail", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation" ], "summary": "get conversation detail", "parameters": [ { "type": "string", "description": "conversation id", "name": "conversation_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.AIConversationDetailResp" } } } ] } } } } }, "/answer/api/v1/ai/conversation/page": { "get": { "description": "get conversation list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation" ], "summary": "get conversation list", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.AIConversationListItem" } } } } ] } } } ] } } } } }, "/answer/api/v1/ai/conversation/vote": { "post": { "description": "vote record", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation" ], "summary": "vote record", "parameters": [ { "description": "vote request", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AIConversationVoteReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/answer": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "Update Answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "Update Answer", "parameters": [ { "description": "AnswerUpdateReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AnswerUpdateReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "Add Answer", "parameters": [ { "description": "add answer request", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AnswerAddReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "delete answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "delete answer", "parameters": [ { "description": "answer", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RemoveAnswerReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/answer/acceptance": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "Accept Answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "Accept Answer", "parameters": [ { "description": "AcceptAnswerReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AcceptAnswerReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/answer/info": { "get": { "description": "Get Answer Detail", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "Get Answer Detail", "parameters": [ { "type": "string", "description": "id", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetAnswerInfoResp" } } } ] } } } } }, "/answer/api/v1/answer/page": { "get": { "description": "AnswerList \u003cbr\u003e \u003cb\u003eorder\u003c/b\u003e (default or updated)", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "AnswerList", "parameters": [ { "type": "string", "description": "question_id", "name": "question_id", "in": "query", "required": true }, { "type": "string", "description": "order", "name": "order", "in": "query", "required": true }, { "type": "string", "description": "page", "name": "page", "in": "query", "required": true }, { "type": "string", "description": "page_size", "name": "page_size", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/answer/recover": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "recover the deleted answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "recover answer", "parameters": [ { "description": "answer", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RecoverAnswerReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/badge": { "get": { "description": "get badge info", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "api-badge" ], "summary": "get badge info", "parameters": [ { "type": "string", "default": "string", "description": "id", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetBadgeInfoResp" } } } ] } } } } }, "/answer/api/v1/badge/awards/page": { "get": { "description": "get badge award list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "api-badge" ], "summary": "get badge award list", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "badge id", "name": "badge_id", "in": "query", "required": true }, { "type": "string", "description": "only list the award by username", "name": "username", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetBadgeInfoResp" } } } ] } } } } }, "/answer/api/v1/badge/user/awards": { "get": { "description": "get user badge award list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "api-badge" ], "summary": "get user badge award list", "parameters": [ { "type": "string", "description": "user name", "name": "username", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" } } } } ] } } } } }, "/answer/api/v1/badge/user/awards/recent": { "get": { "description": "get user badge award list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "api-badge" ], "summary": "get user badge award list", "parameters": [ { "type": "string", "description": "user name", "name": "username", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" } } } } ] } } } } }, "/answer/api/v1/badges": { "get": { "description": "list all badges group by group", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "api-badge" ], "summary": "list all badges group by group", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetBadgeListResp" } } } } ] } } } } }, "/answer/api/v1/collection/switch": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add collection", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Collection" ], "summary": "add collection", "parameters": [ { "description": "collection", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.CollectionSwitchReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.CollectionSwitchResp" } } } ] } } } } }, "/answer/api/v1/comment": { "get": { "description": "get comment by id", "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "get comment by id", "parameters": [ { "type": "string", "description": "id", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetCommentResp" } } } } ] } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update comment", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "update comment", "parameters": [ { "description": "comment", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateCommentReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add comment", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "add comment", "parameters": [ { "description": "comment", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddCommentReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetCommentResp" } } } ] } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "remove comment", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "remove comment", "parameters": [ { "description": "comment", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RemoveCommentReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/comment/page": { "get": { "description": "get comment page", "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "get comment page", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "object id", "name": "object_id", "in": "query", "required": true }, { "enum": [ "vote" ], "type": "string", "description": "query condition", "name": "query_cond", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetCommentResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/connector/binding/email": { "post": { "description": "external login binding user send email", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "PluginConnector" ], "summary": "external login binding user send email", "parameters": [ { "description": "external login binding user send email", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailResp" } } } ] } } } } }, "/answer/api/v1/connector/info": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get all enabled connectors", "produces": [ "application/json" ], "tags": [ "PluginConnector" ], "summary": "get all enabled connectors", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.ConnectorInfoResp" } } } } ] } } } } }, "/answer/api/v1/connector/user/info": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get all connectors info about user", "produces": [ "application/json" ], "tags": [ "PluginConnector" ], "summary": "get all connectors info about user", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.ConnectorUserInfoResp" } } } } ] } } } } }, "/answer/api/v1/connector/user/unbinding": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "unbind external user login", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "PluginConnector" ], "summary": "unbind external user login", "parameters": [ { "description": "ExternalLoginUnbindingReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.ExternalLoginUnbindingReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/embed/config": { "get": { "description": "get embed plugin config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Plugin" ], "summary": "get embed plugin config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/plugin.EmbedConfig" } } } } ] } } } } }, "/answer/api/v1/file": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "upload file", "consumes": [ "multipart/form-data" ], "tags": [ "Upload" ], "summary": "upload file", "parameters": [ { "enum": [ "post", "post_attachment", "avatar", "branding" ], "type": "string", "description": "identify the source of the file upload", "name": "source", "in": "formData", "required": true }, { "type": "file", "description": "file", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "string" } } } ] } } } } }, "/answer/api/v1/follow": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "follow object or cancel follow operation", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Activity" ], "summary": "follow object or cancel follow operation", "parameters": [ { "description": "follow", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.FollowReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.FollowResp" } } } ] } } } } }, "/answer/api/v1/follow/tags": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user follow tags", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Activity" ], "summary": "update user follow tags", "parameters": [ { "description": "follow", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateFollowTagsReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/language/config": { "get": { "description": "get language config mapping", "produces": [ "application/json" ], "tags": [ "Lang" ], "summary": "get language config mapping", "parameters": [ { "type": "string", "description": "Accept-Language", "name": "Accept-Language", "in": "header", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/language/options": { "get": { "description": "Get language options", "produces": [ "application/json" ], "tags": [ "Lang" ], "summary": "Get language options", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/meta/reaction": { "get": { "description": "get reaction for an object", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Meta" ], "summary": "get reaction", "parameters": [ { "type": "string", "description": "object_id", "name": "object_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.ReactionRespItem" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update reaction. if not exist, add one", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Meta" ], "summary": "add or update reaction", "parameters": [ { "description": "reaction", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateReactionReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/notification/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get notification list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Notification" ], "summary": "get notification list", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "enum": [ "inbox", "achievement" ], "type": "string", "description": "type", "name": "type", "in": "query", "required": true }, { "enum": [ "all", "posts", "invites", "votes" ], "type": "string", "description": "inbox_type", "name": "inbox_type", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/notification/read/state": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "ClearUnRead", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Notification" ], "summary": "ClearUnRead", "parameters": [ { "description": "NotificationClearIDRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.NotificationClearIDRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/notification/read/state/all": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "ClearUnRead", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Notification" ], "summary": "ClearUnRead", "parameters": [ { "description": "NotificationClearRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.NotificationClearRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/notification/status": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "GetRedDot", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Notification" ], "summary": "GetRedDot", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "DelRedDot", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Notification" ], "summary": "DelRedDot", "parameters": [ { "description": "NotificationClearRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.NotificationClearRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/permission": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "check user permission", "produces": [ "application/json" ], "tags": [ "Permission" ], "summary": "check user permission", "parameters": [ { "type": "string", "description": "access-token", "name": "Authorization", "in": "header", "required": true }, { "enum": [ "question.add", "question.edit", "question.edit_without_review", "question.delete", "question.close", "question.reopen", "question.vote_up", "question.vote_down", "question.pin", "question.unpin", "question.hide", "question.show", "answer.add", "answer.edit", "answer.edit_without_review", "answer.delete", "answer.accept", "answer.vote_up", "answer.vote_down", "answer.invite_someone_to_answer", "comment.add", "comment.edit", "comment.delete", "comment.vote_up", "comment.vote_down", "report.add", "tag.add", "tag.edit", "tag.edit_slug_name", "tag.edit_without_review", "tag.delete", "tag.synonym", "link.url_limit", "vote.detail", "answer.audit", "question.audit", "tag.audit", "tag.use_reserved_tag" ], "type": "string", "description": "permission key", "name": "action", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": { "type": "boolean" } } } } ] } } } } }, "/answer/api/v1/personal/answer/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "list personal answers", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Personal" ], "summary": "list personal answers", "parameters": [ { "type": "string", "default": "string", "description": "username", "name": "username", "in": "query", "required": true }, { "enum": [ "newest", "score" ], "type": "string", "description": "order", "name": "order", "in": "query", "required": true }, { "type": "string", "default": "0", "description": "page", "name": "page", "in": "query", "required": true }, { "type": "string", "default": "20", "description": "page_size", "name": "page_size", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/personal/collection/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "list personal collections", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Collection" ], "summary": "list personal collections", "parameters": [ { "type": "string", "default": "0", "description": "page", "name": "page", "in": "query", "required": true }, { "type": "string", "default": "20", "description": "page_size", "name": "page_size", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/personal/comment/page": { "get": { "description": "user personal comment list", "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "user personal comment list", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "username", "name": "username", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetCommentPersonalWithPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/personal/qa/top": { "get": { "description": "UserTop", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "UserTop", "parameters": [ { "type": "string", "default": "string", "description": "username", "name": "username", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/personal/rank/page": { "get": { "description": "user personal rank list", "produces": [ "application/json" ], "tags": [ "Rank" ], "summary": "user personal rank list", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "username", "name": "username", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetRankPersonalPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/personal/user/info": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "GetOtherUserInfoByUsername", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "GetOtherUserInfoByUsername", "parameters": [ { "type": "string", "description": "username", "name": "username", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetOtherUserInfoResp" } } } ] } } } } }, "/answer/api/v1/personal/vote/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user personal votes", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Activity" ], "summary": "get user personal votes", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetVoteWithPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/plugin/status": { "get": { "description": "get all plugins status", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Plugin" ], "summary": "get all plugins status", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetPluginListResp" } } } } ] } } } } }, "/answer/api/v1/post/render": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "render post content", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Upload" ], "summary": "render post content", "parameters": [ { "description": "PostRenderReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.PostRenderReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "update question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionUpdate" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "add question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionAdd" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "delete question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "delete question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RemoveQuestionReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/answer": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add question and answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "add question and answer", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionAddByAnswer" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/info": { "get": { "description": "get question details", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "get question details", "parameters": [ { "type": "string", "default": "1", "description": "Question TagID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/question/invite": { "get": { "description": "get question invite user info", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "get question invite user info", "parameters": [ { "type": "string", "default": "1", "description": "Question ID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update question invite user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "update question invite user", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionUpdateInviteUser" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/link": { "get": { "description": "get question link", "tags": [ "Question" ], "summary": "get question link", "parameters": [ { "minimum": 1, "type": "integer", "name": "in_days", "in": "query" }, { "enum": [ "newest", "active", "hot", "score", "unanswered", "recommend", "frequent" ], "type": "string", "name": "order", "in": "query" }, { "minimum": 1, "type": "integer", "name": "page", "in": "query" }, { "maximum": 100, "minimum": 1, "type": "integer", "name": "page_size", "in": "query" }, { "type": "string", "name": "question_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.QuestionPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/question/operation": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "Operation question \\n operation [pin unpin hide show]", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "Operation question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.OperationQuestionReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/page": { "get": { "description": "get questions by page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "get questions by page", "parameters": [ { "description": "QuestionPageReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionPageReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.QuestionPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/question/recommend/page": { "get": { "description": "get recommend questions by page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "get recommend questions by page", "parameters": [ { "description": "QuestionPageReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionPageReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.QuestionPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/question/recover": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "recover deleted question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "recover deleted question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionRecoverReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/reopen": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "reopen question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "reopen question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.ReopenQuestionReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/similar": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "fuzzy query similar questions based on title", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "fuzzy query similar questions based on title", "parameters": [ { "type": "string", "default": "string", "description": "title", "name": "title", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/similar/tag": { "get": { "description": "Search Similar Question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "Search Similar Question", "parameters": [ { "type": "string", "default": "", "description": "question_id", "name": "question_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/question/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "Close question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "Close question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.CloseQuestionReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/tags": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get tag list", "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get tag list", "parameters": [ { "type": "string", "description": "tag", "name": "tag", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetTagBasicResp" } } } } ] } } } } }, "/answer/api/v1/reasons": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get reasons by object type and action", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "reason" ], "summary": "get reasons by object type and action", "parameters": [ { "enum": [ "question", "answer", "comment", "user" ], "type": "string", "description": "object_type", "name": "object_type", "in": "query", "required": true }, { "enum": [ "status", "close", "flag", "review" ], "type": "string", "description": "action", "name": "action", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/render/config": { "get": { "description": "GetRenderConfig", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "PluginRender" ], "summary": "GetRenderConfig", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/plugin.RenderConfig" } } } ] } } } } }, "/answer/api/v1/report": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add report \u003cbr\u003e source (question, answer, comment, user)", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Report" ], "summary": "add report", "parameters": [ { "description": "report", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddReportReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/report/review": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "review report", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Report" ], "summary": "review report", "parameters": [ { "description": "flag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.ReviewReportReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/report/unreviewed/post": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get unreviewed report post page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Report" ], "summary": "get unreviewed report post page", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetReportListPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/review/pending/post": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update review", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Review" ], "summary": "update review", "parameters": [ { "description": "review", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateReviewReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/review/pending/post/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get unreviewed post page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Review" ], "summary": "get unreviewed post page", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "string", "description": "object_id", "name": "object_id", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUnreviewedPostPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/reviewing/type": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get reviewing type", "produces": [ "application/json" ], "tags": [ "Revision" ], "summary": "get reviewing type", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetReviewingTypeResp" } } } } ] } } } } }, "/answer/api/v1/revisions": { "get": { "description": "get revision list", "produces": [ "application/json" ], "tags": [ "Revision" ], "summary": "get revision list", "parameters": [ { "type": "string", "description": "object id", "name": "object_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetRevisionResp" } } } } ] } } } } }, "/answer/api/v1/revisions/audit": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "revision audit operation:approve or reject", "produces": [ "application/json" ], "tags": [ "Revision" ], "summary": "revision audit", "parameters": [ { "description": "audit", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RevisionAuditReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/revisions/edit/check": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "check can update revision", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Revision" ], "summary": "check can update revision", "parameters": [ { "type": "string", "default": "string", "description": "id", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/revisions/unreviewed": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get unreviewed revision list", "produces": [ "application/json" ], "tags": [ "Revision" ], "summary": "get unreviewed revision list", "parameters": [ { "type": "string", "description": "page id", "name": "page", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUnreviewedRevisionResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/search": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "search object", "produces": [ "application/json" ], "tags": [ "Search" ], "summary": "search object", "parameters": [ { "type": "string", "description": "query string", "name": "q", "in": "query", "required": true }, { "enum": [ "newest", "active", "score", "relevance" ], "type": "string", "description": "order", "name": "order", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SearchResp" } } } ] } } } } }, "/answer/api/v1/search/desc": { "get": { "description": "get search description", "produces": [ "application/json" ], "tags": [ "Search" ], "summary": "get search description", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SearchResp" } } } ] } } } } }, "/answer/api/v1/siteinfo": { "get": { "description": "get site info", "produces": [ "application/json" ], "tags": [ "site" ], "summary": "get site info", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteInfoResp" } } } ] } } } } }, "/answer/api/v1/siteinfo/legal": { "get": { "description": "get site legal info", "produces": [ "application/json" ], "tags": [ "site" ], "summary": "get site legal info", "parameters": [ { "enum": [ "tos", "privacy" ], "type": "string", "description": "legal information type", "name": "info_type", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetSiteLegalInfoResp" } } } ] } } } } }, "/answer/api/v1/tag": { "get": { "description": "get tag one", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get tag one", "parameters": [ { "type": "string", "description": "tag id", "name": "tag_id", "in": "query", "required": true }, { "type": "string", "description": "tag name", "name": "tag_name", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetTagResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "update tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateTagReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "add tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddTagReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "delete tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "delete tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RemoveTagReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/tag/merge": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "merge tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "merge tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddTagReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/tag/recover": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "recover delete tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "recover delete tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RecoverTagReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/tag/synonym": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "update tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateTagSynonymReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/tag/synonyms": { "get": { "description": "get tag synonyms", "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get tag synonyms", "parameters": [ { "type": "integer", "description": "tag id", "name": "tag_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetTagSynonymsResp" } } } ] } } } } }, "/answer/api/v1/tags": { "get": { "description": "get tags list by slug name", "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get tags list", "parameters": [ { "type": "array", "items": { "type": "string" }, "collectionFormat": "csv", "description": "string collection", "name": "tags", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetTagBasicResp" } } } } ] } } } } }, "/answer/api/v1/tags/following": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get following tag list", "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get following tag list", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetFollowingTagsResp" } } } } ] } } } } }, "/answer/api/v1/tags/page": { "get": { "description": "get tag page", "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get tag page", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "slug_name", "name": "slug_name", "in": "query" }, { "enum": [ "popular", "name", "newest" ], "type": "string", "description": "query condition", "name": "query_cond", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetTagPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/user/action/record": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "ActionRecord", "tags": [ "User" ], "summary": "ActionRecord", "parameters": [ { "enum": [ "login", "e_mail", "find_pass" ], "type": "string", "description": "action", "name": "action", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.ActionRecordResp" } } } ] } } } } }, "/answer/api/v1/user/email": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "user change email verification", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "user change email verification", "parameters": [ { "description": "UserChangeEmailVerifyReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserChangeEmailVerifyReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/email/change/code": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "send email to the user email then change their email", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "send email to the user email then change their email", "parameters": [ { "description": "UserChangeEmailSendCodeReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserChangeEmailSendCodeReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/email/verification": { "post": { "description": "UserVerifyEmail", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserVerifyEmail", "parameters": [ { "type": "string", "default": "", "description": "code", "name": "code", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.UserLoginResp" } } } ] } } } } }, "/answer/api/v1/user/email/verification/send": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "UserVerifyEmailSend", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserVerifyEmailSend", "parameters": [ { "type": "string", "default": "", "description": "captcha_id", "name": "captcha_id", "in": "query" }, { "type": "string", "default": "", "description": "captcha_code", "name": "captcha_code", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/user/info": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user info, if user no login response http code is 200, but user info is null", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "GetUserInfoByUserID", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetCurrentLoginUserInfoResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "UserUpdateInfo update user info", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserUpdateInfo update user info", "parameters": [ { "type": "string", "description": "access-token", "name": "Authorization", "in": "header", "required": true }, { "description": "UpdateInfoRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateInfoRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/info/search": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "SearchUserListByName", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "SearchUserListByName", "parameters": [ { "type": "string", "description": "username", "name": "username", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetOtherUserInfoResp" } } } ] } } } } }, "/answer/api/v1/user/interface": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "UserUpdateInterface update user interface config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserUpdateInterface update user interface config", "parameters": [ { "type": "string", "description": "access-token", "name": "Authorization", "in": "header", "required": true }, { "description": "UpdateInfoRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/login/email": { "post": { "description": "UserEmailLogin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserEmailLogin", "parameters": [ { "description": "UserEmailLogin", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserEmailLoginReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.UserLoginResp" } } } ] } } } } }, "/answer/api/v1/user/logout": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "user logout", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "user logout", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/notification/config": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user's notification config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "update user's notification config", "parameters": [ { "description": "UpdateUserNotificationConfigReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserNotificationConfigReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user's notification config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "get user's notification config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetUserNotificationConfigResp" } } } ] } } } } }, "/answer/api/v1/user/notification/unsubscribe": { "put": { "description": "unsubscribe notification", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "unsubscribe notification", "parameters": [ { "description": "UserUnsubscribeNotificationReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserUnsubscribeNotificationReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/password": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "UserModifyPassWord", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserModifyPassWord", "parameters": [ { "description": "UserModifyPasswordReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserModifyPasswordReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/password/replacement": { "post": { "description": "UseRePassWord", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UseRePassWord", "parameters": [ { "description": "UserRePassWordRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserRePassWordRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/user/password/reset": { "post": { "description": "RetrievePassWord", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "RetrievePassWord", "parameters": [ { "description": "UserRetrievePassWordRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserRetrievePassWordRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/user/plugin/config": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user plugin config", "produces": [ "application/json" ], "tags": [ "UserPlugin" ], "summary": "get user plugin config", "parameters": [ { "type": "string", "description": "plugin_slug_name", "name": "plugin_slug_name", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetPluginConfigResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user plugin config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "UserPlugin" ], "summary": "update user plugin config", "parameters": [ { "description": "UpdatePluginConfigReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserPluginConfigReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/plugin/configs": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get plugin list that used for user.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "UserPlugin" ], "summary": "get plugin list that used for user.", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUserPluginListResp" } } } } ] } } } } }, "/answer/api/v1/user/ranking": { "get": { "description": "get user ranking", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "get user ranking", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.UserRankingResp" } } } ] } } } } }, "/answer/api/v1/user/register/email": { "post": { "description": "UserRegisterByEmail", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserRegisterByEmail", "parameters": [ { "description": "UserRegisterReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserRegisterReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.UserLoginResp" } } } ] } } } } }, "/answer/api/v1/user/staff": { "get": { "description": "get user staff", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "get user staff", "parameters": [ { "type": "string", "description": "username", "name": "username", "in": "query", "required": true }, { "type": "string", "description": "page_size", "name": "page_size", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetUserStaffResp" } } } ] } } } } }, "/answer/api/v1/vote/down": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add vote", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Activity" ], "summary": "vote down", "parameters": [ { "description": "vote", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.VoteReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.VoteResp" } } } ] } } } } }, "/answer/api/v1/vote/up": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add vote", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Activity" ], "summary": "vote up", "parameters": [ { "description": "vote", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.VoteReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.VoteResp" } } } ] } } } } }, "/custom.css": { "get": { "description": "get site custom CSS", "produces": [ "text/css" ], "tags": [ "site" ], "summary": "get site custom CSS", "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/installation/base-info": { "post": { "description": "init base info", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "installation" ], "summary": "init base info", "parameters": [ { "description": "InitBaseInfoReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/install.InitBaseInfoReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/installation/config-file/check": { "post": { "description": "check config file if exist when installation", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "installation" ], "summary": "check config file if exist when installation", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/install.CheckConfigFileResp" } } } ] } } } } }, "/installation/db/check": { "post": { "description": "check database if exist when installation", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "installation" ], "summary": "check database if exist when installation", "parameters": [ { "description": "CheckDatabaseReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/install.CheckDatabaseReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/install.CheckConfigFileResp" } } } ] } } } } }, "/installation/init": { "post": { "description": "init environment", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "installation" ], "summary": "init environment", "parameters": [ { "description": "CheckDatabaseReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/install.CheckDatabaseReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/installation/language/config": { "get": { "description": "get installation language config mapping", "produces": [ "application/json" ], "tags": [ "Lang" ], "summary": "get installation language config mapping", "parameters": [ { "type": "string", "description": "installation language", "name": "lang", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/installation/language/options": { "get": { "description": "get installation language options", "produces": [ "application/json" ], "tags": [ "Lang" ], "summary": "get installation language options", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/translator.LangOption" } } } } ] } } } } }, "/personal/question/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "list personal questions", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Personal" ], "summary": "list personal questions", "parameters": [ { "type": "string", "default": "string", "description": "username", "name": "username", "in": "query", "required": true }, { "enum": [ "newest", "score" ], "type": "string", "description": "order", "name": "order", "in": "query", "required": true }, { "type": "string", "default": "0", "description": "page", "name": "page", "in": "query", "required": true }, { "type": "string", "default": "20", "description": "page_size", "name": "page_size", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/robots.txt": { "get": { "description": "get site robots information", "produces": [ "application/json" ], "tags": [ "site" ], "summary": "get site robots information", "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } } }, "definitions": { "constant.NotificationChannelKey": { "type": "string", "enum": [ "email" ], "x-enum-varnames": [ "EmailChannel" ] }, "constant.Privilege": { "type": "object", "properties": { "key": { "type": "string" }, "label": { "type": "string" }, "value": { "type": "integer", "minimum": 1 } } }, "entity.BadgeLevel": { "type": "integer", "enum": [ 1, 2, 3 ], "x-enum-varnames": [ "BadgeLevelBronze", "BadgeLevelSilver", "BadgeLevelGold" ] }, "handler.RespBody": { "type": "object", "properties": { "code": { "description": "http code", "type": "integer" }, "data": { "description": "response data" }, "msg": { "description": "response message", "type": "string" }, "reason": { "description": "reason key", "type": "string" } } }, "install.CheckConfigFileResp": { "type": "object", "properties": { "config_file_exist": { "type": "boolean" }, "db_connection_success": { "type": "boolean" }, "db_table_exist": { "type": "boolean" } } }, "install.CheckDatabaseReq": { "type": "object", "required": [ "db_type" ], "properties": { "db_file": { "type": "string" }, "db_host": { "type": "string" }, "db_name": { "type": "string" }, "db_password": { "type": "string" }, "db_type": { "type": "string", "enum": [ "postgres", "sqlite3", "mysql" ] }, "db_username": { "type": "string" }, "ssl_cert": { "type": "string" }, "ssl_enabled": { "type": "boolean" }, "ssl_key": { "type": "string" }, "ssl_mode": { "type": "string" }, "ssl_root_cert": { "type": "string" } } }, "install.InitBaseInfoReq": { "type": "object", "required": [ "contact_email", "email", "external_content_display", "lang", "name", "password", "site_name", "site_url" ], "properties": { "contact_email": { "type": "string", "maxLength": 500 }, "email": { "type": "string", "maxLength": 500 }, "external_content_display": { "type": "string", "enum": [ "always_display", "ask_before_display" ] }, "lang": { "type": "string", "maxLength": 30 }, "login_required": { "type": "boolean" }, "name": { "type": "string", "maxLength": 30, "minLength": 2 }, "password": { "type": "string", "maxLength": 32, "minLength": 8 }, "site_name": { "type": "string", "maxLength": 30 }, "site_url": { "type": "string", "maxLength": 512 } } }, "pager.PageModel": { "type": "object", "properties": { "count": { "type": "integer" }, "list": {} } }, "plugin.EmbedConfig": { "type": "object", "properties": { "enable": { "type": "boolean" }, "platform": { "type": "string" } } }, "plugin.RenderConfig": { "type": "object", "properties": { "select_theme": { "type": "string" } } }, "schema.AIConversationAdminDeleteReq": { "type": "object", "required": [ "conversation_id" ], "properties": { "conversation_id": { "type": "string" } } }, "schema.AIConversationAdminDetailResp": { "type": "object", "properties": { "conversation_id": { "type": "string" }, "created_at": { "type": "integer" }, "records": { "type": "array", "items": { "$ref": "#/definitions/schema.AIConversationRecord" } }, "topic": { "type": "string" }, "user_info": { "$ref": "#/definitions/schema.AIConversationUserInfo" } } }, "schema.AIConversationAdminListItem": { "type": "object", "properties": { "created_at": { "type": "integer" }, "helpful_count": { "type": "integer" }, "id": { "type": "string" }, "topic": { "type": "string" }, "unhelpful_count": { "type": "integer" }, "user_info": { "$ref": "#/definitions/schema.AIConversationUserInfo" } } }, "schema.AIConversationDetailResp": { "type": "object", "properties": { "conversation_id": { "type": "string" }, "created_at": { "type": "integer" }, "records": { "type": "array", "items": { "$ref": "#/definitions/schema.AIConversationRecord" } }, "topic": { "type": "string" }, "updated_at": { "type": "integer" } } }, "schema.AIConversationListItem": { "type": "object", "properties": { "conversation_id": { "type": "string" }, "created_at": { "type": "integer" }, "topic": { "type": "string" } } }, "schema.AIConversationRecord": { "type": "object", "properties": { "chat_completion_id": { "type": "string" }, "content": { "type": "string" }, "created_at": { "type": "integer" }, "helpful": { "type": "integer" }, "role": { "type": "string" }, "unhelpful": { "type": "integer" } } }, "schema.AIConversationUserInfo": { "type": "object", "properties": { "avatar": { "type": "string" }, "display_name": { "type": "string" }, "id": { "type": "string" }, "rank": { "type": "integer" }, "username": { "type": "string" } } }, "schema.AIConversationVoteReq": { "type": "object", "required": [ "chat_completion_id", "vote_type" ], "properties": { "cancel": { "type": "boolean" }, "chat_completion_id": { "type": "string" }, "vote_type": { "type": "string", "enum": [ "helpful", "unhelpful" ] } } }, "schema.AIPromptConfig": { "type": "object", "properties": { "en_us": { "type": "string" }, "zh_cn": { "type": "string" } } }, "schema.AcceptAnswerReq": { "type": "object", "required": [ "question_id" ], "properties": { "answer_id": { "type": "string" }, "question_id": { "type": "string", "maxLength": 30 } } }, "schema.ActObjectInfo": { "type": "object", "properties": { "answer_id": { "type": "string" }, "display_name": { "type": "string" }, "main_tag_slug_name": { "type": "string" }, "object_type": { "type": "string" }, "question_id": { "type": "string" }, "title": { "type": "string" }, "username": { "type": "string" } } }, "schema.ActObjectTimeline": { "type": "object", "properties": { "activity_id": { "type": "string" }, "activity_type": { "type": "string" }, "cancelled": { "type": "boolean" }, "cancelled_at": { "type": "integer" }, "comment": { "type": "string" }, "created_at": { "type": "integer" }, "object_id": { "type": "string" }, "object_type": { "type": "string" }, "revision_id": { "type": "string" }, "user_info": { "$ref": "#/definitions/schema.UserBasicInfo" } } }, "schema.ActionRecordResp": { "type": "object", "properties": { "captcha_id": { "type": "string" }, "captcha_img": { "type": "string" }, "verify": { "type": "boolean" } } }, "schema.AddAPIKeyReq": { "type": "object", "required": [ "description", "scope" ], "properties": { "description": { "type": "string", "maxLength": 150 }, "scope": { "type": "string", "enum": [ "read-only", "global" ] } } }, "schema.AddAPIKeyResp": { "type": "object", "properties": { "access_key": { "type": "string" } } }, "schema.AddCommentReq": { "type": "object", "required": [ "object_id", "original_text" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "mention_username_list": { "description": "@ user id list", "type": "array", "items": { "type": "string" } }, "object_id": { "description": "object id", "type": "string" }, "original_text": { "description": "original comment content", "type": "string", "maxLength": 600, "minLength": 2 }, "reply_comment_id": { "description": "reply comment id", "type": "string" } } }, "schema.AddReportReq": { "type": "object", "required": [ "object_id", "report_type" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "content": { "description": "report content", "type": "string", "maxLength": 500 }, "object_id": { "description": "object id", "type": "string", "maxLength": 20 }, "report_type": { "description": "report type", "type": "integer" } } }, "schema.AddTagReq": { "type": "object", "required": [ "display_name", "original_text", "slug_name" ], "properties": { "display_name": { "description": "display_name", "type": "string", "maxLength": 35 }, "original_text": { "description": "original text", "type": "string", "maxLength": 65536 }, "slug_name": { "description": "slug_name", "type": "string", "maxLength": 35 } } }, "schema.AddUserReq": { "type": "object", "required": [ "display_name", "email", "password" ], "properties": { "display_name": { "type": "string", "maxLength": 30, "minLength": 2 }, "email": { "type": "string", "maxLength": 500 }, "password": { "type": "string", "maxLength": 32, "minLength": 8 } } }, "schema.AddUsersReq": { "type": "object", "properties": { "users": { "description": "users info line by line", "type": "string" } } }, "schema.AdminUpdateAnswerStatusReq": { "type": "object", "required": [ "answer_id", "status" ], "properties": { "answer_id": { "type": "string" }, "status": { "type": "string", "enum": [ "available", "deleted" ] } } }, "schema.AdminUpdateQuestionStatusReq": { "type": "object", "required": [ "question_id", "status" ], "properties": { "question_id": { "type": "string" }, "status": { "type": "string", "enum": [ "available", "closed", "deleted" ] } } }, "schema.AnswerAddReq": { "type": "object", "required": [ "content" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "content": { "type": "string", "maxLength": 65535, "minLength": 6 }, "question_id": { "type": "string" } } }, "schema.AnswerInfo": { "type": "object", "properties": { "accepted": { "type": "integer" }, "collected": { "type": "boolean" }, "content": { "type": "string" }, "create_time": { "type": "integer" }, "html": { "type": "string" }, "id": { "type": "string" }, "member_actions": { "description": "MemberActions", "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "question_id": { "type": "string" }, "question_info": { "$ref": "#/definitions/schema.QuestionInfoResp" }, "status": { "type": "integer" }, "update_time": { "type": "integer" }, "update_user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "vote_count": { "type": "integer" }, "vote_status": { "type": "string" } } }, "schema.AnswerUpdateReq": { "type": "object", "required": [ "content" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "content": { "type": "string", "maxLength": 65535, "minLength": 6 }, "edit_summary": { "type": "string" }, "id": { "type": "string" }, "title": { "type": "string" } } }, "schema.AvatarInfo": { "type": "object", "properties": { "custom": { "type": "string", "maxLength": 200 }, "gravatar": { "type": "string", "maxLength": 200 }, "type": { "type": "string", "maxLength": 100 } } }, "schema.BadgeListInfo": { "type": "object", "properties": { "award_count": { "description": "badge award count", "type": "integer" }, "earned_count": { "description": "badge earned count", "type": "integer" }, "icon": { "description": "badge icon", "type": "string" }, "id": { "description": "badge id", "type": "string" }, "level": { "description": "badge level", "allOf": [ { "$ref": "#/definitions/entity.BadgeLevel" } ] }, "name": { "description": "badge name", "type": "string" } } }, "schema.BadgeStatus": { "type": "string", "enum": [ "active", "inactive" ], "x-enum-varnames": [ "BadgeStatusActive", "BadgeStatusInactive" ] }, "schema.CloseQuestionReq": { "type": "object", "required": [ "id" ], "properties": { "close_msg": { "description": "close_type", "type": "string" }, "close_type": { "description": "close_type", "type": "integer" }, "id": { "type": "string" } } }, "schema.CollectionSwitchReq": { "type": "object", "required": [ "group_id", "object_id" ], "properties": { "bookmark": { "type": "boolean" }, "group_id": { "type": "string" }, "object_id": { "type": "string" } } }, "schema.CollectionSwitchResp": { "type": "object", "properties": { "object_collection_count": { "type": "integer" } } }, "schema.ConfigField": { "type": "object", "properties": { "description": { "type": "string" }, "name": { "type": "string" }, "options": { "type": "array", "items": { "$ref": "#/definitions/schema.ConfigFieldOption" } }, "required": { "type": "boolean" }, "title": { "type": "string" }, "type": { "type": "string" }, "ui_options": { "$ref": "#/definitions/schema.ConfigFieldUIOptions" }, "value": {} } }, "schema.ConfigFieldOption": { "type": "object", "properties": { "label": { "type": "string" }, "value": { "type": "string" } } }, "schema.ConfigFieldUIOptions": { "type": "object", "properties": { "action": { "$ref": "#/definitions/schema.UIOptionAction" }, "class_name": { "type": "string" }, "field_class_name": { "type": "string" }, "input_type": { "type": "string" }, "label": { "type": "string" }, "placeholder": { "type": "string" }, "rows": { "type": "string" }, "text": { "type": "string" }, "variant": { "type": "string" } } }, "schema.ConnectorInfoResp": { "type": "object", "properties": { "icon": { "type": "string" }, "link": { "type": "string" }, "name": { "type": "string" } } }, "schema.ConnectorUserInfoResp": { "type": "object", "properties": { "binding": { "type": "boolean" }, "external_id": { "type": "string" }, "icon": { "type": "string" }, "link": { "type": "string" }, "name": { "type": "string" } } }, "schema.DeleteAPIKeyReq": { "type": "object", "properties": { "id": { "type": "integer" } } }, "schema.DeletePermanentlyReq": { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "users", "questions", "answers" ] } } }, "schema.EditUserProfileReq": { "type": "object", "required": [ "display_name", "email", "user_id" ], "properties": { "display_name": { "type": "string", "maxLength": 30, "minLength": 2 }, "email": { "type": "string", "maxLength": 500 }, "user_id": { "type": "string" }, "username": { "type": "string", "maxLength": 30, "minLength": 2 } } }, "schema.ExternalLoginBindingUserSendEmailReq": { "type": "object", "required": [ "binding_key", "email" ], "properties": { "binding_key": { "type": "string", "maxLength": 100 }, "email": { "type": "string", "maxLength": 512 }, "must": { "description": "If must is true, whatever email if exists, try to bind user.\nIf must is false, when email exist, will only be prompted with a warning.", "type": "boolean" } } }, "schema.ExternalLoginBindingUserSendEmailResp": { "type": "object", "properties": { "access_token": { "type": "string" }, "email_exist_and_must_be_confirmed": { "type": "boolean" } } }, "schema.ExternalLoginUnbindingReq": { "type": "object", "required": [ "external_id" ], "properties": { "external_id": { "type": "string", "maxLength": 128 } } }, "schema.FollowReq": { "type": "object", "required": [ "object_id" ], "properties": { "is_cancel": { "description": "is cancel", "type": "boolean" }, "object_id": { "description": "object id", "type": "string" } } }, "schema.FollowResp": { "type": "object", "properties": { "follows": { "description": "the followers of object", "type": "integer" }, "is_followed": { "description": "if user is followed object will be true,otherwise false", "type": "boolean" } } }, "schema.GetAIModelResp": { "type": "object", "properties": { "created": { "type": "integer" }, "id": { "type": "string" }, "object": { "type": "string" }, "owned_by": { "type": "string" } } }, "schema.GetAIProviderResp": { "type": "object", "properties": { "default_api_host": { "type": "string" }, "display_name": { "type": "string" }, "name": { "type": "string" } } }, "schema.GetAPIKeyResp": { "type": "object", "properties": { "access_key": { "type": "string" }, "created_at": { "type": "integer" }, "description": { "type": "string" }, "id": { "type": "integer" }, "last_used_at": { "type": "integer" }, "scope": { "type": "string" } } }, "schema.GetAnswerInfoResp": { "type": "object", "properties": { "info": { "$ref": "#/definitions/schema.AnswerInfo" }, "question": { "$ref": "#/definitions/schema.QuestionInfoResp" } } }, "schema.GetBadgeInfoResp": { "type": "object", "properties": { "award_count": { "description": "badge award count", "type": "integer" }, "description": { "description": "badge description", "type": "string" }, "earned_count": { "description": "badge earned count", "type": "integer" }, "icon": { "description": "badge icon", "type": "string" }, "id": { "description": "badge id", "type": "string" }, "is_single": { "description": "badge is single or multiple", "type": "boolean" }, "level": { "description": "badge level", "allOf": [ { "$ref": "#/definitions/entity.BadgeLevel" } ] }, "name": { "description": "badge name", "type": "string" } } }, "schema.GetBadgeListPagedResp": { "type": "object", "properties": { "award_count": { "description": "badge award count", "type": "integer" }, "description": { "description": "badge description", "type": "string" }, "earned": { "description": "badge earned count", "type": "boolean" }, "group_name": { "description": "badge group name", "type": "string" }, "icon": { "description": "badge icon", "type": "string" }, "id": { "description": "badge id", "type": "string" }, "level": { "description": "badge level", "allOf": [ { "$ref": "#/definitions/entity.BadgeLevel" } ] }, "name": { "description": "badge name", "type": "string" }, "status": { "description": "badge status", "allOf": [ { "$ref": "#/definitions/schema.BadgeStatus" } ] } } }, "schema.GetBadgeListResp": { "type": "object", "properties": { "badges": { "description": "badge list info", "type": "array", "items": { "$ref": "#/definitions/schema.BadgeListInfo" } }, "group_name": { "description": "badge group name", "type": "string" } } }, "schema.GetCommentPersonalWithPageResp": { "type": "object", "properties": { "answer_id": { "description": "answer id", "type": "string" }, "comment_id": { "description": "comment id", "type": "string" }, "content": { "description": "content", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "object_id": { "description": "object id", "type": "string" }, "object_type": { "description": "object type", "type": "string", "enum": [ "question", "answer", "tag", "comment" ] }, "question_id": { "description": "question id", "type": "string" }, "title": { "description": "title", "type": "string" }, "url_title": { "description": "url title", "type": "string" } } }, "schema.GetCommentResp": { "type": "object", "properties": { "comment_id": { "description": "comment id", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "is_vote": { "description": "current user if already vote this comment", "type": "boolean" }, "member_actions": { "description": "MemberActions", "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "object_id": { "description": "object id", "type": "string" }, "original_text": { "description": "original comment content", "type": "string" }, "parsed_text": { "description": "parsed comment content", "type": "string" }, "reply_comment_id": { "description": "reply comment id", "type": "string" }, "reply_user_display_name": { "description": "reply user display name", "type": "string" }, "reply_user_id": { "description": "reply user id", "type": "string" }, "reply_user_status": { "description": "reply user status", "type": "string" }, "reply_username": { "description": "reply user username", "type": "string" }, "user_avatar": { "description": "user avatar", "type": "string" }, "user_display_name": { "description": "user display name", "type": "string" }, "user_id": { "description": "user id", "type": "string" }, "user_status": { "description": "user status", "type": "string" }, "username": { "description": "username", "type": "string" }, "vote_count": { "description": "user vote amount", "type": "integer" } } }, "schema.GetCurrentLoginUserInfoResp": { "type": "object", "properties": { "access_token": { "description": "access token", "type": "string" }, "answer_count": { "description": "answer count", "type": "integer" }, "authority_group": { "description": "authority group", "type": "integer" }, "avatar": { "$ref": "#/definitions/schema.AvatarInfo" }, "bio": { "description": "bio markdown", "type": "string" }, "bio_html": { "description": "bio html", "type": "string" }, "color_scheme": { "description": "Color scheme", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "display_name": { "description": "display name", "type": "string" }, "e_mail": { "description": "email", "type": "string" }, "follow_count": { "description": "follow count", "type": "integer" }, "have_password": { "description": "user have password", "type": "boolean" }, "id": { "description": "user id", "type": "string" }, "language": { "description": "language", "type": "string" }, "last_login_date": { "description": "last login date", "type": "integer" }, "location": { "description": "location", "type": "string" }, "mail_status": { "description": "mail status(1 pass 2 to be verified)", "type": "integer" }, "mobile": { "description": "mobile", "type": "string" }, "notice_status": { "description": "notice status(1 on 2off)", "type": "integer" }, "question_count": { "description": "question count", "type": "integer" }, "rank": { "description": "rank", "type": "integer" }, "role_id": { "description": "role id", "type": "integer" }, "status": { "description": "user status", "type": "string" }, "suspended_until": { "description": "suspended until timestamp", "type": "integer" }, "username": { "description": "username", "type": "string" }, "visit_token": { "description": "visit token", "type": "string" }, "website": { "description": "website", "type": "string" } } }, "schema.GetFollowingTagsResp": { "type": "object", "properties": { "display_name": { "description": "display name", "type": "string" }, "main_tag_slug_name": { "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", "type": "string" }, "recommend": { "type": "boolean" }, "reserved": { "type": "boolean" }, "slug_name": { "description": "slug name", "type": "string" }, "tag_id": { "description": "tag id", "type": "string" } } }, "schema.GetObjectTimelineResp": { "type": "object", "properties": { "object_info": { "$ref": "#/definitions/schema.ActObjectInfo" }, "timeline": { "type": "array", "items": { "$ref": "#/definitions/schema.ActObjectTimeline" } } } }, "schema.GetOtherUserInfoByUsernameResp": { "type": "object", "properties": { "answer_count": { "description": "answer count", "type": "integer" }, "avatar": { "description": "avatar", "type": "string" }, "bio": { "description": "bio markdown", "type": "string" }, "bio_html": { "description": "bio html", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "display_name": { "description": "display name", "type": "string" }, "follow_count": { "description": "email\nfollow count", "type": "integer" }, "id": { "description": "user id", "type": "string" }, "last_login_date": { "description": "last login date", "type": "integer" }, "location": { "description": "location", "type": "string" }, "mobile": { "description": "mobile", "type": "string" }, "question_count": { "description": "question count", "type": "integer" }, "rank": { "description": "rank", "type": "integer" }, "status": { "type": "string" }, "status_msg": { "type": "string" }, "suspended_until": { "description": "suspended until timestamp", "type": "integer" }, "username": { "description": "username", "type": "string" }, "website": { "description": "website", "type": "string" } } }, "schema.GetOtherUserInfoResp": { "type": "object", "properties": { "info": { "$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp" } } }, "schema.GetPluginConfigResp": { "type": "object", "properties": { "config_fields": { "type": "array", "items": { "$ref": "#/definitions/schema.ConfigField" } }, "description": { "type": "string" }, "name": { "type": "string" }, "slug_name": { "type": "string" }, "version": { "type": "string" } } }, "schema.GetPluginListResp": { "type": "object", "properties": { "description": { "type": "string" }, "enabled": { "type": "boolean" }, "have_config": { "type": "boolean" }, "link": { "type": "string" }, "name": { "type": "string" }, "slug_name": { "type": "string" }, "version": { "type": "string" } } }, "schema.GetPrivilegesConfigResp": { "type": "object", "properties": { "options": { "type": "array", "items": { "$ref": "#/definitions/schema.PrivilegeOption" } }, "selected_level": { "$ref": "#/definitions/schema.PrivilegeLevel" } } }, "schema.GetRankPersonalPageResp": { "type": "object", "properties": { "answer_id": { "description": "answer id", "type": "string" }, "content": { "description": "content", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "object_id": { "description": "object id", "type": "string" }, "object_type": { "description": "object type", "type": "string", "enum": [ "question", "answer", "tag", "comment" ] }, "question_id": { "description": "question id", "type": "string" }, "rank_type": { "description": "rank type", "type": "string" }, "reputation": { "description": "reputation", "type": "integer" }, "title": { "description": "title", "type": "string" }, "url_title": { "description": "url title", "type": "string" } } }, "schema.GetReportListPageResp": { "type": "object", "properties": { "answer_accepted": { "type": "boolean" }, "answer_count": { "type": "integer" }, "answer_id": { "type": "string" }, "author_user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "comment_id": { "type": "string" }, "created_at": { "type": "integer" }, "flag_id": { "type": "string" }, "object_id": { "type": "string" }, "object_show_status": { "type": "integer" }, "object_status": { "type": "integer" }, "object_type": { "type": "string", "enum": [ "question", "answer", "comment" ] }, "original_text": { "type": "string" }, "parsed_text": { "type": "string" }, "question_id": { "type": "string" }, "reason": { "$ref": "#/definitions/schema.ReasonItem" }, "reason_content": { "type": "string" }, "submit_at": { "type": "integer" }, "submitter_user": { "$ref": "#/definitions/schema.UserBasicInfo" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "url_title": { "type": "string" } } }, "schema.GetReviewingTypeResp": { "type": "object", "properties": { "label": { "type": "string" }, "name": { "type": "string" }, "todo_amount": { "type": "integer" } } }, "schema.GetRevisionResp": { "type": "object", "properties": { "content": {}, "create_at": { "type": "integer" }, "id": { "type": "string" }, "object_id": { "type": "string" }, "reason": { "type": "string" }, "status": { "type": "integer" }, "title": { "type": "string" }, "url_title": { "type": "string" }, "use_id": { "type": "string" }, "user_info": { "$ref": "#/definitions/schema.UserBasicInfo" } } }, "schema.GetRoleResp": { "type": "object", "properties": { "description": { "type": "string" }, "id": { "type": "integer" }, "name": { "type": "string" } } }, "schema.GetSMTPConfigResp": { "type": "object", "properties": { "encryption": { "description": "\"\" SSL TLS", "type": "string" }, "from_email": { "type": "string" }, "from_name": { "type": "string" }, "smtp_authentication": { "type": "boolean" }, "smtp_host": { "type": "string" }, "smtp_password": { "type": "string" }, "smtp_port": { "type": "integer" }, "smtp_username": { "type": "string" } } }, "schema.GetSiteLegalInfoResp": { "type": "object", "properties": { "privacy_policy_original_text": { "type": "string" }, "privacy_policy_parsed_text": { "type": "string" }, "terms_of_service_original_text": { "type": "string" }, "terms_of_service_parsed_text": { "type": "string" } } }, "schema.GetTagBasicResp": { "type": "object", "properties": { "display_name": { "type": "string" }, "recommend": { "type": "boolean" }, "reserved": { "type": "boolean" }, "slug_name": { "type": "string" }, "tag_id": { "type": "string" } } }, "schema.GetTagPageResp": { "type": "object", "properties": { "created_at": { "description": "created time", "type": "integer" }, "description": { "description": "description", "type": "string" }, "display_name": { "description": "display_name", "type": "string" }, "excerpt": { "description": "excerpt", "type": "string" }, "follow_count": { "description": "follower amount", "type": "integer" }, "is_follower": { "description": "is follower", "type": "boolean" }, "original_text": { "description": "original text", "type": "string" }, "parsed_text": { "description": "parsed_text", "type": "string" }, "question_count": { "description": "question amount", "type": "integer" }, "recommend": { "type": "boolean" }, "reserved": { "type": "boolean" }, "slug_name": { "description": "slug_name", "type": "string" }, "tag_id": { "description": "tag_id", "type": "string" }, "updated_at": { "description": "updated time", "type": "integer" } } }, "schema.GetTagResp": { "type": "object", "properties": { "created_at": { "type": "integer" }, "description": { "type": "string" }, "display_name": { "type": "string" }, "excerpt": { "type": "string" }, "follow_count": { "type": "integer" }, "is_follower": { "type": "boolean" }, "main_tag_slug_name": { "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", "type": "string" }, "member_actions": { "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "original_text": { "type": "string" }, "parsed_text": { "type": "string" }, "question_count": { "type": "integer" }, "recommend": { "type": "boolean" }, "reserved": { "type": "boolean" }, "slug_name": { "type": "string" }, "status": { "type": "string" }, "tag_id": { "type": "string" }, "updated_at": { "type": "integer" } } }, "schema.GetTagSynonymsResp": { "type": "object", "properties": { "member_actions": { "description": "MemberActions", "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "synonyms": { "description": "synonyms", "type": "array", "items": { "$ref": "#/definitions/schema.TagSynonym" } } } }, "schema.GetUnreviewedPostPageResp": { "type": "object", "properties": { "answer_id": { "type": "string" }, "author_user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "comment_id": { "type": "string" }, "created_at": { "type": "integer" }, "object_id": { "type": "string" }, "object_show_status": { "type": "integer" }, "object_status": { "type": "integer" }, "object_type": { "type": "string", "enum": [ "question", "answer", "comment" ] }, "original_text": { "type": "string" }, "parsed_text": { "type": "string" }, "question_id": { "type": "string" }, "reason": { "type": "string" }, "review_id": { "type": "integer" }, "submit_at": { "type": "integer" }, "submitter_display_name": { "type": "string" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "url_title": { "type": "string" } } }, "schema.GetUnreviewedRevisionResp": { "type": "object", "properties": { "info": { "$ref": "#/definitions/schema.UnreviewedRevisionInfoInfo" }, "type": { "type": "string" }, "unreviewed_info": { "$ref": "#/definitions/schema.GetRevisionResp" } } }, "schema.GetUserActivationResp": { "type": "object", "properties": { "activation_url": { "type": "string" } } }, "schema.GetUserBadgeAwardListResp": { "type": "object", "properties": { "earned_count": { "description": "badge award count", "type": "integer" }, "icon": { "description": "badge icon", "type": "string" }, "id": { "description": "badge id", "type": "string" }, "level": { "description": "badge level", "allOf": [ { "$ref": "#/definitions/entity.BadgeLevel" } ] }, "name": { "description": "badge name", "type": "string" } } }, "schema.GetUserNotificationConfigResp": { "type": "object", "properties": { "all_new_question": { "$ref": "#/definitions/schema.NotificationChannelConfig" }, "all_new_question_for_following_tags": { "$ref": "#/definitions/schema.NotificationChannelConfig" }, "inbox": { "$ref": "#/definitions/schema.NotificationChannelConfig" } } }, "schema.GetUserPageResp": { "type": "object", "properties": { "avatar": { "description": "avatar", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "deleted_at": { "description": "delete time", "type": "integer" }, "display_name": { "description": "display name", "type": "string" }, "e_mail": { "description": "email", "type": "string" }, "rank": { "description": "rank", "type": "integer" }, "role_id": { "description": "role id", "type": "integer" }, "role_name": { "description": "role name", "type": "string" }, "status": { "description": "user status(normal,suspended,deleted,inactive)", "type": "string" }, "suspended_at": { "description": "suspended time", "type": "integer" }, "suspended_until": { "description": "suspended until time", "type": "integer" }, "user_id": { "description": "user id", "type": "string" }, "username": { "description": "username", "type": "string" } } }, "schema.GetUserPluginListResp": { "type": "object", "properties": { "name": { "type": "string" }, "slug_name": { "type": "string" } } }, "schema.GetUserStaffResp": { "type": "object", "properties": { "avatar": { "description": "avatar", "type": "string" }, "display_name": { "description": "display name", "type": "string" }, "username": { "description": "username", "type": "string" } } }, "schema.GetVoteWithPageResp": { "type": "object", "properties": { "answer_id": { "description": "answer id", "type": "string" }, "content": { "description": "content", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "object_id": { "description": "object id", "type": "string" }, "object_type": { "description": "object type", "type": "string", "enum": [ "question", "answer", "tag", "comment" ] }, "question_id": { "description": "question id", "type": "string" }, "title": { "description": "title", "type": "string" }, "url_title": { "description": "url title", "type": "string" }, "vote_type": { "description": "vote type", "type": "string" } } }, "schema.LoadingAction": { "type": "object", "properties": { "state": { "type": "string" }, "text": { "type": "string" } } }, "schema.NotificationChannelConfig": { "type": "object", "properties": { "enable": { "type": "boolean" }, "key": { "$ref": "#/definitions/constant.NotificationChannelKey" } } }, "schema.NotificationClearIDRequest": { "type": "object", "properties": { "id": { "type": "string" } } }, "schema.NotificationClearRequest": { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "inbox", "achievement" ] } } }, "schema.OnCompleteAction": { "type": "object", "properties": { "refresh_form_config": { "type": "boolean" }, "toast_return_message": { "type": "boolean" } } }, "schema.Operation": { "type": "object", "properties": { "description": { "type": "string" }, "level": { "$ref": "#/definitions/schema.OperationLevel" }, "msg": { "type": "string" }, "time": { "type": "integer" }, "type": { "type": "string" } } }, "schema.OperationLevel": { "type": "string", "enum": [ "info", "danger", "warning", "secondary" ], "x-enum-varnames": [ "OperationLevelInfo", "OperationLevelDanger", "OperationLevelWarning", "OperationLevelSecondary" ] }, "schema.OperationQuestionReq": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string" }, "operation": { "description": "operation [pin unpin hide show]", "type": "string" } } }, "schema.PermissionMemberAction": { "type": "object", "properties": { "action": { "type": "string" }, "name": { "type": "string" }, "type": { "type": "string" } } }, "schema.PostRenderReq": { "type": "object", "properties": { "content": { "type": "string" } } }, "schema.PrivilegeLevel": { "type": "integer", "enum": [ 1, 2, 3, 99 ], "x-enum-varnames": [ "PrivilegeLevel1", "PrivilegeLevel2", "PrivilegeLevel3", "PrivilegeLevelCustom" ] }, "schema.PrivilegeOption": { "type": "object", "properties": { "level": { "$ref": "#/definitions/schema.PrivilegeLevel" }, "level_desc": { "type": "string" }, "privileges": { "type": "array", "items": { "$ref": "#/definitions/constant.Privilege" } } } }, "schema.QuestionAdd": { "type": "object", "required": [ "title" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "content": { "description": "content", "type": "string", "maxLength": 65535, "minLength": 0 }, "tags": { "description": "tags", "type": "array", "items": { "$ref": "#/definitions/schema.TagItem" } }, "title": { "description": "question title", "type": "string", "maxLength": 150, "minLength": 6 } } }, "schema.QuestionAddByAnswer": { "type": "object", "required": [ "answer_content", "title" ], "properties": { "answer_content": { "type": "string", "maxLength": 65535, "minLength": 6 }, "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "content": { "description": "content", "type": "string", "maxLength": 65535, "minLength": 0 }, "mention_username_list": { "type": "array", "items": { "type": "string" } }, "tags": { "description": "tags", "type": "array", "items": { "$ref": "#/definitions/schema.TagItem" } }, "title": { "description": "question title", "type": "string", "maxLength": 150, "minLength": 6 } } }, "schema.QuestionInfoResp": { "type": "object", "properties": { "accepted_answer_id": { "type": "string" }, "answer_count": { "type": "integer" }, "answered": { "type": "boolean" }, "collected": { "type": "boolean" }, "collection_count": { "type": "integer" }, "content": { "type": "string" }, "create_time": { "type": "integer" }, "description": { "type": "string" }, "edit_time": { "type": "integer" }, "extends_actions": { "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "first_answer_id": { "type": "string" }, "follow_count": { "type": "integer" }, "html": { "type": "string" }, "id": { "type": "string" }, "is_followed": { "type": "boolean" }, "last_answer_id": { "type": "string" }, "last_answered_user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "member_actions": { "description": "MemberActions", "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "operation": { "$ref": "#/definitions/schema.Operation" }, "pin": { "type": "integer" }, "show": { "type": "integer" }, "status": { "type": "integer" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "unique_view_count": { "type": "integer" }, "update_time": { "type": "integer" }, "update_user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "url_title": { "type": "string" }, "user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "view_count": { "type": "integer" }, "vote_count": { "type": "integer" }, "vote_status": { "type": "string" } } }, "schema.QuestionPageReq": { "type": "object", "properties": { "in_days": { "type": "integer", "minimum": 1 }, "order": { "type": "string", "enum": [ "newest", "active", "hot", "score", "unanswered", "recommend", "frequent" ] }, "page": { "type": "integer", "minimum": 1 }, "page_size": { "type": "integer", "minimum": 1 }, "tag": { "type": "string", "maxLength": 100 }, "username": { "type": "string", "maxLength": 100 } } }, "schema.QuestionPageResp": { "type": "object", "properties": { "accepted_answer_id": { "description": "answer information", "type": "string" }, "answer_count": { "type": "integer" }, "collection_count": { "type": "integer" }, "created_at": { "type": "integer" }, "description": { "type": "string" }, "follow_count": { "type": "integer" }, "id": { "type": "string" }, "last_answer_id": { "type": "string" }, "operated_at": { "description": "operator information", "type": "integer" }, "operation_type": { "type": "string" }, "operator": { "$ref": "#/definitions/schema.QuestionPageRespOperator" }, "pin": { "description": "1: unpin, 2: pin", "type": "integer" }, "show": { "description": "0: show, 1: hide", "type": "integer" }, "status": { "type": "integer" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "unique_view_count": { "type": "integer" }, "url_title": { "type": "string" }, "view_count": { "description": "question statistical information", "type": "integer" }, "vote_count": { "type": "integer" } } }, "schema.QuestionPageRespOperator": { "type": "object", "properties": { "avatar": { "type": "string" }, "display_name": { "type": "string" }, "id": { "type": "string" }, "rank": { "type": "integer" }, "status": { "type": "string" }, "username": { "type": "string" } } }, "schema.QuestionRecoverReq": { "type": "object", "required": [ "question_id" ], "properties": { "question_id": { "type": "string" } } }, "schema.QuestionUpdate": { "type": "object", "required": [ "id", "title" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "content": { "description": "content", "type": "string", "maxLength": 65535, "minLength": 0 }, "edit_summary": { "description": "edit summary", "type": "string" }, "id": { "description": "question id", "type": "string" }, "invite_user": { "type": "array", "items": { "type": "string" } }, "tags": { "description": "tags", "type": "array", "items": { "$ref": "#/definitions/schema.TagItem" } }, "title": { "description": "question title", "type": "string", "maxLength": 150, "minLength": 6 } } }, "schema.QuestionUpdateInviteUser": { "type": "object", "required": [ "id" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "id": { "type": "string" }, "invite_user": { "type": "array", "items": { "type": "string" } } } }, "schema.ReactionRespItem": { "type": "object", "properties": { "count": { "description": "Count is the number of users who reacted", "type": "integer" }, "emoji": { "description": "Emoji is the reaction emoji", "type": "string" }, "is_active": { "description": "IsActive is if current user has reacted", "type": "boolean" }, "tooltip": { "description": "Tooltip is the user's name who reacted", "type": "string" } } }, "schema.ReasonItem": { "type": "object", "properties": { "content_type": { "type": "string" }, "description": { "type": "string" }, "name": { "type": "string" }, "placeholder": { "type": "string" }, "reason_key": { "type": "string" }, "reason_type": { "type": "integer" } } }, "schema.RecoverAnswerReq": { "type": "object", "required": [ "answer_id" ], "properties": { "answer_id": { "type": "string" } } }, "schema.RecoverTagReq": { "type": "object", "required": [ "tag_id" ], "properties": { "tag_id": { "type": "string" } } }, "schema.RemoveAnswerReq": { "type": "object", "required": [ "id" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "id": { "type": "string" } } }, "schema.RemoveCommentReq": { "type": "object", "required": [ "comment_id" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "comment_id": { "description": "comment id", "type": "string" } } }, "schema.RemoveQuestionReq": { "type": "object", "required": [ "id" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "id": { "description": "question id", "type": "string" } } }, "schema.RemoveTagReq": { "type": "object", "required": [ "tag_id" ], "properties": { "tag_id": { "description": "tag_id", "type": "string" } } }, "schema.ReopenQuestionReq": { "type": "object", "properties": { "question_id": { "type": "string" } } }, "schema.ReviewReportReq": { "type": "object", "required": [ "flag_id", "operation_type" ], "properties": { "close_msg": { "type": "string" }, "close_type": { "type": "integer" }, "content": { "type": "string", "maxLength": 65535, "minLength": 6 }, "flag_id": { "type": "string" }, "operation_type": { "type": "string", "enum": [ "edit_post", "close_post", "delete_post", "unlist_post", "ignore_report" ] }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagItem" } }, "title": { "type": "string", "maxLength": 150, "minLength": 6 } } }, "schema.RevisionAuditReq": { "type": "object", "required": [ "id", "operation" ], "properties": { "id": { "description": "object id", "type": "string" }, "operation": { "description": "approve or reject", "type": "string" } } }, "schema.SearchObject": { "type": "object", "properties": { "accepted": { "type": "boolean" }, "answer_count": { "type": "integer" }, "created_at": { "type": "integer" }, "excerpt": { "type": "string" }, "id": { "type": "string" }, "question_id": { "type": "string" }, "status": { "description": "Status", "type": "string" }, "tags": { "description": "tags", "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "url_title": { "type": "string" }, "user_info": { "description": "user info", "allOf": [ { "$ref": "#/definitions/schema.SearchObjectUser" } ] }, "vote_count": { "type": "integer" } } }, "schema.SearchObjectUser": { "type": "object", "properties": { "display_name": { "type": "string" }, "id": { "type": "string" }, "rank": { "type": "integer" }, "status": { "type": "string" }, "username": { "type": "string" } } }, "schema.SearchResp": { "type": "object", "properties": { "count": { "type": "integer" }, "list": { "description": "search response", "type": "array", "items": { "$ref": "#/definitions/schema.SearchResult" } } } }, "schema.SearchResult": { "type": "object", "properties": { "object": { "description": "this object", "allOf": [ { "$ref": "#/definitions/schema.SearchObject" } ] }, "object_type": { "description": "object_type", "type": "string" } } }, "schema.SendUserActivationReq": { "type": "object", "required": [ "user_id" ], "properties": { "user_id": { "type": "string" } } }, "schema.SiteAIProvider": { "type": "object", "properties": { "api_host": { "type": "string", "maxLength": 512 }, "api_key": { "type": "string", "maxLength": 256 }, "model": { "type": "string", "maxLength": 100 }, "provider": { "type": "string", "maxLength": 50 } } }, "schema.SiteAIReq": { "type": "object", "properties": { "ai_providers": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteAIProvider" } }, "chosen_provider": { "type": "string", "maxLength": 50 }, "enabled": { "type": "boolean" }, "prompt_config": { "$ref": "#/definitions/schema.AIPromptConfig" } } }, "schema.SiteAIResp": { "type": "object", "properties": { "ai_providers": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteAIProvider" } }, "chosen_provider": { "type": "string", "maxLength": 50 }, "enabled": { "type": "boolean" }, "prompt_config": { "$ref": "#/definitions/schema.AIPromptConfig" } } }, "schema.SiteAdvancedReq": { "type": "object", "properties": { "authorized_attachment_extensions": { "type": "array", "items": { "type": "string" } }, "authorized_image_extensions": { "type": "array", "items": { "type": "string" } }, "max_attachment_size": { "type": "integer" }, "max_image_megapixel": { "type": "integer" }, "max_image_size": { "type": "integer" } } }, "schema.SiteAdvancedResp": { "type": "object", "properties": { "authorized_attachment_extensions": { "type": "array", "items": { "type": "string" } }, "authorized_image_extensions": { "type": "array", "items": { "type": "string" } }, "max_attachment_size": { "type": "integer" }, "max_image_megapixel": { "type": "integer" }, "max_image_size": { "type": "integer" } } }, "schema.SiteBrandingReq": { "type": "object", "properties": { "favicon": { "type": "string", "maxLength": 512 }, "logo": { "type": "string", "maxLength": 512 }, "mobile_logo": { "type": "string", "maxLength": 512 }, "square_icon": { "type": "string", "maxLength": 512 } } }, "schema.SiteBrandingResp": { "type": "object", "properties": { "favicon": { "type": "string", "maxLength": 512 }, "logo": { "type": "string", "maxLength": 512 }, "mobile_logo": { "type": "string", "maxLength": 512 }, "square_icon": { "type": "string", "maxLength": 512 } } }, "schema.SiteCustomCssHTMLReq": { "type": "object", "properties": { "custom_css": { "type": "string", "maxLength": 65536 }, "custom_footer": { "type": "string", "maxLength": 65536 }, "custom_head": { "type": "string", "maxLength": 65536 }, "custom_header": { "type": "string", "maxLength": 65536 }, "custom_sidebar": { "type": "string", "maxLength": 65536 } } }, "schema.SiteCustomCssHTMLResp": { "type": "object", "properties": { "custom_css": { "type": "string", "maxLength": 65536 }, "custom_footer": { "type": "string", "maxLength": 65536 }, "custom_head": { "type": "string", "maxLength": 65536 }, "custom_header": { "type": "string", "maxLength": 65536 }, "custom_sidebar": { "type": "string", "maxLength": 65536 } } }, "schema.SiteGeneralReq": { "type": "object", "required": [ "contact_email", "name", "site_url" ], "properties": { "contact_email": { "type": "string", "maxLength": 512 }, "description": { "type": "string", "maxLength": 2000 }, "name": { "type": "string", "maxLength": 128 }, "short_description": { "type": "string", "maxLength": 255 }, "site_url": { "type": "string", "maxLength": 512 } } }, "schema.SiteGeneralResp": { "type": "object", "required": [ "contact_email", "name", "site_url" ], "properties": { "contact_email": { "type": "string", "maxLength": 512 }, "description": { "type": "string", "maxLength": 2000 }, "name": { "type": "string", "maxLength": 128 }, "short_description": { "type": "string", "maxLength": 255 }, "site_url": { "type": "string", "maxLength": 512 } } }, "schema.SiteInfoResp": { "type": "object", "properties": { "ai_enabled": { "type": "boolean" }, "branding": { "$ref": "#/definitions/schema.SiteBrandingResp" }, "custom_css_html": { "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" }, "general": { "$ref": "#/definitions/schema.SiteGeneralResp" }, "interface": { "$ref": "#/definitions/schema.SiteInterfaceSettingsResp" }, "login": { "$ref": "#/definitions/schema.SiteLoginResp" }, "mcp_enabled": { "type": "boolean" }, "revision": { "type": "string" }, "site_advanced": { "$ref": "#/definitions/schema.SiteAdvancedResp" }, "site_legal": { "$ref": "#/definitions/schema.SiteLegalSimpleResp" }, "site_questions": { "$ref": "#/definitions/schema.SiteQuestionsResp" }, "site_security": { "$ref": "#/definitions/schema.SiteSecurityResp" }, "site_seo": { "$ref": "#/definitions/schema.SiteSeoResp" }, "site_tags": { "$ref": "#/definitions/schema.SiteTagsResp" }, "site_users": { "$ref": "#/definitions/schema.SiteUsersResp" }, "theme": { "$ref": "#/definitions/schema.SiteThemeResp" }, "users_settings": { "$ref": "#/definitions/schema.SiteUsersSettingsResp" }, "version": { "type": "string" } } }, "schema.SiteInterfaceReq": { "type": "object", "required": [ "language", "time_zone" ], "properties": { "language": { "type": "string", "maxLength": 128 }, "time_zone": { "type": "string", "maxLength": 128 } } }, "schema.SiteInterfaceSettingsResp": { "type": "object", "required": [ "language", "time_zone" ], "properties": { "language": { "type": "string", "maxLength": 128 }, "time_zone": { "type": "string", "maxLength": 128 } } }, "schema.SiteLegalSimpleResp": { "type": "object", "required": [ "external_content_display" ], "properties": { "external_content_display": { "type": "string", "enum": [ "always_display", "ask_before_display" ] } } }, "schema.SiteLoginReq": { "type": "object", "properties": { "allow_email_domains": { "type": "array", "items": { "type": "string" } }, "allow_email_registrations": { "type": "boolean" }, "allow_new_registrations": { "type": "boolean" }, "allow_password_login": { "type": "boolean" } } }, "schema.SiteLoginResp": { "type": "object", "properties": { "allow_email_domains": { "type": "array", "items": { "type": "string" } }, "allow_email_registrations": { "type": "boolean" }, "allow_new_registrations": { "type": "boolean" }, "allow_password_login": { "type": "boolean" } } }, "schema.SiteMCPReq": { "type": "object", "properties": { "enabled": { "type": "boolean" } } }, "schema.SiteMCPResp": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "http_header": { "type": "string" }, "type": { "type": "string" }, "url": { "type": "string" } } }, "schema.SitePoliciesReq": { "type": "object", "properties": { "privacy_policy_original_text": { "type": "string" }, "privacy_policy_parsed_text": { "type": "string" }, "terms_of_service_original_text": { "type": "string" }, "terms_of_service_parsed_text": { "type": "string" } } }, "schema.SitePoliciesResp": { "type": "object", "properties": { "privacy_policy_original_text": { "type": "string" }, "privacy_policy_parsed_text": { "type": "string" }, "terms_of_service_original_text": { "type": "string" }, "terms_of_service_parsed_text": { "type": "string" } } }, "schema.SiteQuestionsReq": { "type": "object", "properties": { "min_content": { "type": "integer", "maximum": 65535, "minimum": 0 }, "min_tags": { "type": "integer", "maximum": 5, "minimum": 0 }, "restrict_answer": { "type": "boolean" } } }, "schema.SiteQuestionsResp": { "type": "object", "properties": { "min_content": { "type": "integer", "maximum": 65535, "minimum": 0 }, "min_tags": { "type": "integer", "maximum": 5, "minimum": 0 }, "restrict_answer": { "type": "boolean" } } }, "schema.SiteSecurityReq": { "type": "object", "required": [ "external_content_display" ], "properties": { "check_update": { "type": "boolean" }, "external_content_display": { "type": "string", "enum": [ "always_display", "ask_before_display" ] }, "login_required": { "type": "boolean" } } }, "schema.SiteSecurityResp": { "type": "object", "required": [ "external_content_display" ], "properties": { "check_update": { "type": "boolean" }, "external_content_display": { "type": "string", "enum": [ "always_display", "ask_before_display" ] }, "login_required": { "type": "boolean" } } }, "schema.SiteSeoReq": { "type": "object", "required": [ "permalink", "robots" ], "properties": { "permalink": { "type": "integer", "maximum": 4, "minimum": 0 }, "robots": { "type": "string" } } }, "schema.SiteSeoResp": { "type": "object", "required": [ "permalink", "robots" ], "properties": { "permalink": { "type": "integer", "maximum": 4, "minimum": 0 }, "robots": { "type": "string" } } }, "schema.SiteTagsReq": { "type": "object", "properties": { "recommend_tags": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { "type": "boolean" }, "reserved_tags": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteWriteTag" } } } }, "schema.SiteTagsResp": { "type": "object", "properties": { "recommend_tags": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { "type": "boolean" }, "reserved_tags": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteWriteTag" } } } }, "schema.SiteThemeReq": { "type": "object", "required": [ "theme" ], "properties": { "color_scheme": { "type": "string", "maxLength": 100 }, "layout": { "type": "string", "enum": [ "Full-width", "Fixed-width" ] }, "theme": { "type": "string", "maxLength": 255 }, "theme_config": { "type": "object", "additionalProperties": {} } } }, "schema.SiteThemeResp": { "type": "object", "properties": { "color_scheme": { "type": "string" }, "layout": { "type": "string" }, "theme": { "type": "string" }, "theme_config": { "type": "object", "additionalProperties": {} }, "theme_options": { "type": "array", "items": { "$ref": "#/definitions/schema.ThemeOption" } } } }, "schema.SiteUsersReq": { "type": "object", "required": [ "default_avatar" ], "properties": { "allow_update_avatar": { "type": "boolean" }, "allow_update_bio": { "type": "boolean" }, "allow_update_display_name": { "type": "boolean" }, "allow_update_location": { "type": "boolean" }, "allow_update_username": { "type": "boolean" }, "allow_update_website": { "type": "boolean" }, "default_avatar": { "type": "string", "enum": [ "system", "gravatar" ] }, "gravatar_base_url": { "type": "string" } } }, "schema.SiteUsersResp": { "type": "object", "required": [ "default_avatar" ], "properties": { "allow_update_avatar": { "type": "boolean" }, "allow_update_bio": { "type": "boolean" }, "allow_update_display_name": { "type": "boolean" }, "allow_update_location": { "type": "boolean" }, "allow_update_username": { "type": "boolean" }, "allow_update_website": { "type": "boolean" }, "default_avatar": { "type": "string", "enum": [ "system", "gravatar" ] }, "gravatar_base_url": { "type": "string" } } }, "schema.SiteUsersSettingsReq": { "type": "object", "required": [ "default_avatar" ], "properties": { "default_avatar": { "type": "string", "enum": [ "system", "gravatar" ] }, "gravatar_base_url": { "type": "string" } } }, "schema.SiteUsersSettingsResp": { "type": "object", "required": [ "default_avatar" ], "properties": { "default_avatar": { "type": "string", "enum": [ "system", "gravatar" ] }, "gravatar_base_url": { "type": "string" } } }, "schema.SiteWriteTag": { "type": "object", "required": [ "slug_name" ], "properties": { "display_name": { "type": "string" }, "slug_name": { "type": "string" } } }, "schema.TagItem": { "type": "object", "properties": { "display_name": { "description": "display_name", "type": "string", "maxLength": 35 }, "original_text": { "description": "original text", "type": "string" }, "slug_name": { "description": "slug_name", "type": "string", "maxLength": 35 } } }, "schema.TagResp": { "type": "object", "properties": { "display_name": { "type": "string" }, "main_tag_slug_name": { "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", "type": "string" }, "recommend": { "type": "boolean" }, "reserved": { "type": "boolean" }, "slug_name": { "type": "string" } } }, "schema.TagSynonym": { "type": "object", "properties": { "display_name": { "description": "display name", "type": "string" }, "main_tag_slug_name": { "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", "type": "string" }, "slug_name": { "description": "slug name", "type": "string" }, "tag_id": { "description": "tag id", "type": "string" } } }, "schema.ThemeOption": { "type": "object", "properties": { "label": { "type": "string" }, "value": { "type": "string" } } }, "schema.UIOptionAction": { "type": "object", "properties": { "loading": { "$ref": "#/definitions/schema.LoadingAction" }, "method": { "type": "string" }, "on_complete": { "$ref": "#/definitions/schema.OnCompleteAction" }, "url": { "type": "string" } } }, "schema.UnreviewedRevisionInfoInfo": { "type": "object", "properties": { "answer_accepted": { "type": "boolean" }, "answer_count": { "type": "integer" }, "answer_id": { "type": "string" }, "comment_id": { "type": "string" }, "content": { "type": "string" }, "created_at": { "type": "integer" }, "html": { "type": "string" }, "object_creator_user_id": { "type": "string" }, "object_id": { "type": "string" }, "object_type": { "type": "string" }, "question_id": { "type": "string" }, "show_status": { "type": "integer" }, "status": { "type": "integer" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "url_title": { "type": "string" } } }, "schema.UpdateAPIKeyReq": { "type": "object", "required": [ "description", "id" ], "properties": { "description": { "type": "string", "maxLength": 150 }, "id": { "type": "integer" } } }, "schema.UpdateBadgeStatusReq": { "type": "object", "required": [ "id", "status" ], "properties": { "id": { "description": "badge id", "type": "string" }, "status": { "description": "badge status", "allOf": [ { "$ref": "#/definitions/schema.BadgeStatus" } ] } } }, "schema.UpdateCommentReq": { "type": "object", "required": [ "comment_id", "original_text" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "whether user can delete it", "type": "string" }, "comment_id": { "description": "comment id", "type": "string" }, "original_text": { "description": "original comment content", "type": "string", "maxLength": 600, "minLength": 2 } } }, "schema.UpdateFollowTagsReq": { "type": "object", "properties": { "slug_name_list": { "description": "tag slug name list", "type": "array", "items": { "type": "string" } } } }, "schema.UpdateInfoRequest": { "type": "object", "properties": { "avatar": { "$ref": "#/definitions/schema.AvatarInfo" }, "bio": { "type": "string", "maxLength": 4096 }, "display_name": { "type": "string", "maxLength": 30, "minLength": 2 }, "location": { "type": "string", "maxLength": 100 }, "username": { "type": "string", "maxLength": 30, "minLength": 2 }, "website": { "type": "string", "maxLength": 500 } } }, "schema.UpdatePluginConfigReq": { "type": "object", "required": [ "plugin_slug_name" ], "properties": { "config_fields": { "type": "object", "additionalProperties": {} }, "plugin_slug_name": { "type": "string", "maxLength": 100 } } }, "schema.UpdatePluginStatusReq": { "type": "object", "required": [ "plugin_slug_name" ], "properties": { "enabled": { "type": "boolean" }, "plugin_slug_name": { "type": "string", "maxLength": 100 } } }, "schema.UpdatePrivilegesConfigReq": { "type": "object", "required": [ "level" ], "properties": { "custom_privileges": { "type": "array", "items": { "$ref": "#/definitions/constant.Privilege" } }, "level": { "minimum": 1, "allOf": [ { "$ref": "#/definitions/schema.PrivilegeLevel" } ] } } }, "schema.UpdateReactionReq": { "type": "object", "required": [ "emoji", "object_id", "reaction" ], "properties": { "emoji": { "type": "string", "enum": [ "heart", "smile", "frown" ] }, "object_id": { "type": "string" }, "reaction": { "type": "string", "enum": [ "activate", "deactivate" ] } } }, "schema.UpdateReviewReq": { "type": "object", "required": [ "review_id", "status" ], "properties": { "review_id": { "type": "integer" }, "status": { "type": "string", "enum": [ "approve", "reject" ] } } }, "schema.UpdateSMTPConfigReq": { "type": "object", "properties": { "encryption": { "description": "\"\" SSL TLS", "type": "string", "enum": [ "SSL", "TLS" ] }, "from_email": { "type": "string", "maxLength": 256 }, "from_name": { "type": "string", "maxLength": 256 }, "smtp_authentication": { "type": "boolean" }, "smtp_host": { "type": "string", "maxLength": 256 }, "smtp_password": { "type": "string", "maxLength": 256 }, "smtp_port": { "type": "integer", "maximum": 65535, "minimum": 1 }, "smtp_username": { "type": "string", "maxLength": 256 }, "test_email_recipient": { "type": "string" } } }, "schema.UpdateTagReq": { "type": "object", "required": [ "tag_id" ], "properties": { "display_name": { "description": "display_name", "type": "string", "maxLength": 35 }, "edit_summary": { "description": "edit summary", "type": "string" }, "original_text": { "description": "original text", "type": "string" }, "slug_name": { "description": "slug_name", "type": "string", "maxLength": 35 }, "tag_id": { "description": "tag_id", "type": "string" } } }, "schema.UpdateTagSynonymReq": { "type": "object", "required": [ "synonym_tag_list", "tag_id" ], "properties": { "synonym_tag_list": { "description": "synonym tag list", "type": "array", "items": { "$ref": "#/definitions/schema.TagItem" } }, "tag_id": { "description": "tag_id", "type": "string" } } }, "schema.UpdateUserInterfaceRequest": { "type": "object", "required": [ "color_scheme", "language" ], "properties": { "color_scheme": { "description": "Color scheme", "type": "string", "maxLength": 100 }, "language": { "description": "language", "type": "string", "maxLength": 100 } } }, "schema.UpdateUserNotificationConfigReq": { "type": "object", "properties": { "all_new_question": { "$ref": "#/definitions/schema.NotificationChannelConfig" }, "all_new_question_for_following_tags": { "$ref": "#/definitions/schema.NotificationChannelConfig" }, "inbox": { "$ref": "#/definitions/schema.NotificationChannelConfig" } } }, "schema.UpdateUserPasswordReq": { "type": "object", "required": [ "password", "user_id" ], "properties": { "password": { "type": "string", "maxLength": 32, "minLength": 8 }, "user_id": { "type": "string" } } }, "schema.UpdateUserPluginConfigReq": { "type": "object", "required": [ "plugin_slug_name" ], "properties": { "config_fields": { "type": "object", "additionalProperties": {} }, "plugin_slug_name": { "type": "string", "maxLength": 100 } } }, "schema.UpdateUserRoleReq": { "type": "object", "required": [ "role_id", "user_id" ], "properties": { "role_id": { "description": "role id", "type": "integer" }, "user_id": { "description": "user id", "type": "string" } } }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ "status", "user_id" ], "properties": { "remove_all_content": { "type": "boolean" }, "status": { "type": "string", "enum": [ "normal", "suspended", "deleted", "inactive" ] }, "suspend_duration": { "type": "string", "enum": [ "24h", "48h", "72h", "7d", "14d", "1m", "2m", "3m", "6m", "1y", "forever" ] }, "user_id": { "type": "string" } } }, "schema.UserBasicInfo": { "type": "object", "properties": { "avatar": { "type": "string" }, "display_name": { "type": "string" }, "id": { "type": "string" }, "language": { "type": "string" }, "location": { "type": "string" }, "rank": { "type": "integer" }, "status": { "type": "string" }, "suspended_until": { "type": "integer" }, "username": { "type": "string" }, "website": { "type": "string" } } }, "schema.UserChangeEmailSendCodeReq": { "type": "object", "required": [ "e_mail" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "e_mail": { "type": "string", "maxLength": 500 }, "pass": { "type": "string", "maxLength": 32, "minLength": 8 } } }, "schema.UserChangeEmailVerifyReq": { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "maxLength": 500 } } }, "schema.UserEmailLoginReq": { "type": "object", "required": [ "e_mail", "pass" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "e_mail": { "type": "string", "maxLength": 500 }, "pass": { "type": "string", "maxLength": 32, "minLength": 8 } } }, "schema.UserLoginResp": { "type": "object", "properties": { "access_token": { "description": "access token", "type": "string" }, "answer_count": { "description": "answer count", "type": "integer" }, "authority_group": { "description": "authority group", "type": "integer" }, "avatar": { "description": "avatar", "type": "string" }, "bio": { "description": "bio markdown", "type": "string" }, "bio_html": { "description": "bio html", "type": "string" }, "color_scheme": { "description": "Color scheme", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "display_name": { "description": "display name", "type": "string" }, "e_mail": { "description": "email", "type": "string" }, "follow_count": { "description": "follow count", "type": "integer" }, "have_password": { "description": "user have password", "type": "boolean" }, "id": { "description": "user id", "type": "string" }, "language": { "description": "language", "type": "string" }, "last_login_date": { "description": "last login date", "type": "integer" }, "location": { "description": "location", "type": "string" }, "mail_status": { "description": "mail status(1 pass 2 to be verified)", "type": "integer" }, "mobile": { "description": "mobile", "type": "string" }, "notice_status": { "description": "notice status(1 on 2off)", "type": "integer" }, "question_count": { "description": "question count", "type": "integer" }, "rank": { "description": "rank", "type": "integer" }, "role_id": { "description": "role id", "type": "integer" }, "status": { "description": "user status", "type": "string" }, "suspended_until": { "description": "suspended until timestamp", "type": "integer" }, "username": { "description": "username", "type": "string" }, "visit_token": { "description": "visit token", "type": "string" }, "website": { "description": "website", "type": "string" } } }, "schema.UserModifyPasswordReq": { "type": "object", "required": [ "pass" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "old_pass": { "type": "string", "maxLength": 32, "minLength": 8 }, "pass": { "type": "string", "maxLength": 32, "minLength": 8 } } }, "schema.UserRankingResp": { "type": "object", "properties": { "staffs": { "type": "array", "items": { "$ref": "#/definitions/schema.UserRankingSimpleInfo" } }, "users_with_the_most_reputation": { "type": "array", "items": { "$ref": "#/definitions/schema.UserRankingSimpleInfo" } }, "users_with_the_most_vote": { "type": "array", "items": { "$ref": "#/definitions/schema.UserRankingSimpleInfo" } } } }, "schema.UserRankingSimpleInfo": { "type": "object", "properties": { "avatar": { "description": "avatar", "type": "string" }, "display_name": { "description": "display name", "type": "string" }, "rank": { "description": "rank", "type": "integer" }, "username": { "description": "username", "type": "string" }, "vote_count": { "description": "vote", "type": "integer" } } }, "schema.UserRePassWordRequest": { "type": "object", "required": [ "code", "pass" ], "properties": { "code": { "type": "string", "maxLength": 100 }, "pass": { "type": "string", "maxLength": 32 } } }, "schema.UserRegisterReq": { "type": "object", "required": [ "e_mail", "name", "pass" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "e_mail": { "type": "string", "maxLength": 500 }, "name": { "type": "string", "maxLength": 30, "minLength": 2 }, "pass": { "type": "string", "maxLength": 32, "minLength": 8 } } }, "schema.UserRetrievePassWordRequest": { "type": "object", "required": [ "e_mail" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "e_mail": { "type": "string", "maxLength": 500 } } }, "schema.UserUnsubscribeNotificationReq": { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "maxLength": 500 } } }, "schema.VoteReq": { "type": "object", "required": [ "object_id" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "is_cancel": { "type": "boolean" }, "object_id": { "type": "string" } } }, "schema.VoteResp": { "type": "object", "properties": { "down_votes": { "type": "integer" }, "up_votes": { "type": "integer" }, "vote_status": { "type": "string" }, "votes": { "type": "integer" } } }, "translator.LangOption": { "type": "object", "properties": { "label": { "type": "string" }, "progress": { "description": "Translation completion percentage", "type": "integer" }, "value": { "type": "string" } } } }, "securityDefinitions": { "ApiKeyAuth": { "type": "apiKey", "name": "Authorization", "in": "header" } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "", Host: "", BasePath: "/", Schemes: []string{}, Title: "Apache Answer", Description: "Apache Answer API", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", RightDelim: "}}", } func init() { swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) } ================================================ FILE: docs/release/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ============================================================================ APACHE ANSWER SUBCOMPONENTS: The Apache answer project contains subcomponents with separate copyright notices and license terms. Your use of the source code for the these subcomponents is subject to the terms and conditions of the following licenses. ======================================================================== Apache 2.0 licenses ======================================================================== The following components are provided under the Apache 2.0 License. (Apache License, Version 2.0) react-helmet-async (https://github.com/staylor/react-helmet-async) [link](./licenses/LICENSE-staylor-react-helmet-async.txt) (Apache License, Version 2.0) golang-mock (https://github.com/golang/mock) [link](./licenses/LICENSE-golang-mock.txt) (Apache License, Version 2.0) google-wire (https://github.com/google/wire) [link](./licenses/LICENSE-google-wire.txt) (Apache License, Version 2.0) mojocn-base64Captcha (https://github.com/mojocn/base64Captcha) [link](./licenses/LICENSE-mojocn-base64Captcha.txt) (Apache License, Version 2.0) ory-dockertest (https://github.com/ory/dockertest) [link](./licenses/LICENSE-ory-dockertest.txt) (Apache License, Version 2.0) spf13-cobra (https://github.com/spf13/cobra) [link](./licenses/LICENSE-spf13-cobra.txt) ======================================================================== MIT licenses ======================================================================== The following components are provided under the MIT License. See project link for details. (MIT License) axios (https://github.com/axios/axios) [link](./licenses/LICENSE-axios-axios.txt) (MIT License) bootstrap (https://github.com/twbs/bootstrap) [link](./licenses/LICENSE-twbs-bootstrap.txt) (MIT License) icons (https://github.com/twbs/icons) [link](./licenses/LICENSE-twbs-icons.txt) (MIT License) classnames (https://github.com/JedWatson/classnames) [link](./LICENSE-JedWatson-classnames.txt) (MIT License) codemirror (https://github.com/codemirror/basic-setup) [link](./licenses/LICENSE-codemirror-basic-setup.txt) (MIT License) @codemirror/lang-markdown (https://github.com/codemirror/lang-markdown) [link](./licenses/LICENSE-codemirror-lang-markdown.txt) (MIT License) @codemirror/language-data (https://github.com/codemirror/language-data) [link](./licenses/LICENSE-codemirror-language-data.txt) (MIT License) @codemirror/state (https://github.com/codemirror/state) [link](./licenses/LICENSE-codemirror-state.txt) (MIT License) @codemirror/view (https://github.com/codemirror/view) [link](./licenses/LICENSE-codemirror-view.txt) (MIT License) color (https://github.com/Qix-/color) [link](./licenses/LICENSE-Qix--color.txt) (MIT License) copy-to-clipboard (https://github.com/sudodoki/copy-to-clipboard) [link](./licenses/LICENSE-sudodoki-copy-to-clipboard.txt) (MIT License) dayjs (https://github.com/iamkun/dayjs) [link](./licenses/LICENSE-iamkun-dayjs.txt) (MIT License) i18next (https://github.com/i18next/i18next) [link](./licenses/LICENSE-i18next-i18next.txt) (MIT License) lodash (https://github.com/lodash/lodash) [link](./licenses/LICENSE-lodash-lodash.txt) (MIT License) marked (https://github.com/markedjs/marked) [link](./licenses/LICENSE-markedjs-marked.txt) (MIT License) next-share (https://github.com/Bunlong/next-share) [link](./licenses/LIcENSE-Bunlong-next-share.txt) (MIT License) node-qrcode (https://github.com/soldair/node-qrcode) [link](./licenses/LICENSE-soldair-qrcode.txt) (MIT License) react (https://github.com/facebook/react) [link](./licenses/LICENSE-facebook-react.txt) (MIT License) react-bootstrap (https://github.com/react-bootstrap/react-bootstrap) [link](./licenses/LICENSE-react-bootstrap-react-bootstrap.txt) (MIT License) react-i18next (https://github.com/i18next/react-i18next) [link](./licenses/LICENSE-i18next-react-i18next.txt) (MIT License) react-router (https://github.com/remix-run/react-router) [link](./licenses/LICENSE-remix-run-react-router.txt) (MIT License) swr (https://github.com/vercel/swr) [link](./licenses/LICENSE-vercel-swr.txt) (MIT License) zustand (https://github.com/pmndrs/zustand) [link](./licenses/LICENSE-pmndrs-zustand.txt) (MIT License) mozillazg-go-pinyin (https://github.com/mozillazg/go-pinyin) [link](./licenses/LICENSE-mozillazg-go-pinyin.txt) (MIT License) Machiel-slugify (https://github.com/Machiel/slugify) [link](./licenses/LICENSE-Machiel-slugify.txt) (MIT License) Masterminds-semver (https://github.com/Masterminds/semver) [link](./licenses/LICENSE-Masterminds-semver.txt) (MIT License) anargu-gin-brotli (https://github.com/anargu/gin-brotli) [link](./licenses/LICENSE-anargu-gin-brotli.txt) (MIT License) asaskevich-govalidator (https://github.com/asaskevich/govalidator) [link](./licenses/LICENSE-asaskevich-govalidator.txt) (MIT License) disintegration-imaging (https://github.com/disintegration/imaging) [link](./licenses/LICENSE-disintegration-imaging.txt) (MIT License) gin-gonic-gin (https://github.com/gin-gonic/gin) [link](./licenses/LICENSE-gin-gonic-gin.txt) (MIT License) go-playground-locales (https://github.com/go-playground/locales) [link](./licenses/LICENSE-go-playground-locales.txt) (MIT License) go-playground-universal-translator (https://github.com/go-playground/universal-translator) [link](./licenses/LICENSE-go-playground-universal-translator.txt) (MIT License) go-playground-validator (https://github.com/go-playground/validator) [link](./licenses/LICENSE-go-playground-validator.txt) (MIT License) goccy-go-json (https://github.com/goccy/go-json) [link](./licenses/LICENSE-goccy-go-json.txt) (MIT License) jinzhu-copier (https://github.com/jinzhu/copier) [link](./licenses/LICENSE-jinzhu-copier.txt) (MIT License) jinzhu-now (https://github.com/jinzhu/now) [link](./licenses/LICENSE-jinzhu-now.txt) (MIT License) jordan-wright-email (https://github.com/jordan-wright/email) [link](./licenses/LICENSE-jordan-wright-email.txt) (MIT License) lib-pq (https://github.com/lib/pq) [link](./licenses/LICENSE-lib-pq.txt) (MIT License) mattn-go-sqlite3 (https://github.com/mattn/go-sqlite3) [link](./licenses/LICENSE-mattn-go-sqlite3.txt) (MIT License) segmentfault-pacman (https://github.com/segmentfault/pacman) [link](./licenses/LICENSE-segmentfault-pacman.txt) (MIT License) robfig-cron (https://github.com/robfig/cron) [link](./licenses/LICENSE-robfig-cron.txt) (MIT License) scottleedavis-go-exif-remove (https://github.com/scottleedavis/go-exif-remove) [link](./licenses/LICENSE-scottleedavis-go-exif-remove.txt) (MIT License) stretchr-testify (https://github.com/stretchr/testify) [link](./licenses/LICENSE-stretchr-testify.txt) (MIT License) swaggo-files (https://github.com/swaggo/files) [link](./licenses/LICENSE-swaggo-files.txt) (MIT License) swaggo-gin-swagger (https://github.com/swaggo/gin-swagger) [link](./licenses/LICENSE-swaggo-gin-swagger.txt) (MIT License) swaggo-swag (https://github.com/swaggo/swag) [link](./licenses/LICENSE-swaggo-swag.txt) (MIT License) tidwall-gjson (https://github.com/tidwall/gjson) [link](./licenses/LICENSE-tidwall-gjson.txt) (MIT License) yuin-goldmark (https://github.com/yuin/goldmark) [link](./licenses/LICENSE-yuin-goldmark.txt) (MIT License) go-gomail-gomail (https://gopkg.in/gomail.v2) [link](./licenses/LICENSE-go-gomail-gomail.txt) (MIT License) front-matter (https://github.com/jxson/front-matter) [link](./licenses/LICENSE-jxson-front-matter.txt) (MIT License) js-sha256 (https://github.com/emn178/js-sha256) [link](./licenses/LICENSE-emn178-js-sha256.txt) ======================================================================== BSD licenses ======================================================================== The following components are provided under a BSD license. See project link for details. (BSD 2-Clause) bwmarrin-snowflake (https://github.com/bwmarrin/snowflake) [link](./licenses/LICENSE-bwmarrin-snowflake.txt) (BSD 2-Clause) xorm (https://xorm.io/xorm) [link](./licenses/LICENSE-xorm.txt) (BSD 3-Clause) google-uuid (https://github.com/google/uuid) [link](./licenses/LICENSE-google-uuid.txt) (BSD 3-Clause) grokify-html-strip-tags-go (https://github.com/grokify/html-strip-tags-go) [link](./licenses/LICENSE-grokify-html-strip-tags-go.txt) (BSD 3-Clause) microcosm-cc-bluemonday (https://github.com/microcosm-cc/bluemonday) [link](./licenses/LICENSE-microcosm-cc-bluemonday.txt) (BSD 3-Clause) cznic-sqlite (https://modernc.org/sqlite) [link](./licenses/LICENSE-cznic-sqlite.txt) (BSD 3-Clause) jsdiff (https://github.com/kpdecker/jsdiff) [link](./licenses/LICENSE-kpdecker-jsdiff.txt) (BSD 3-Clause) qs (https://github.com/ljharb/qs) [link](./licenses/LICENSE-ljharb-qs.txt) ======================================================================== ISC licenses ======================================================================== The following components are provided under a ISC license. See project link for details. (ISC License) node-semver (https://github.com/npm/node-semver) [link](./licenses/LICENSE-npm-node-semver.txt) ======================================================================== MIT and Apache-2.0 licenses ======================================================================== The following components are provided under a MIT and Apache-2.0 license. See project link for details. (MIT and Apache-2.0) go-yaml-yaml (https://gopkg.in/yaml.v3) [link](./licenses/LICENSE-go-yaml-yaml.txt) ======================================================================== MPL licenses ======================================================================== The following components are provided under a MPL license. See project link for details. (MPL-2.0) go-sql-driver-mysql (https://github.com/go-sql-driver/mysql) [link](./licenses/LICENSE-go-sql-driver-mysql.txt) ================================================ FILE: docs/release/NOTICE ================================================ Apache Answer Copyright 2025 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (https://www.apache.org/). ================================================ FILE: docs/release/licenses/LICENSE-JedWatson-classnames.txt ================================================ The MIT License (MIT) Copyright (c) 2018 Jed Watson 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: docs/release/licenses/LICENSE-Machiel-slugify.txt ================================================ The MIT License (MIT) Copyright (c) 2015 Machiel Molenaar 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: docs/release/licenses/LICENSE-Masterminds-semver.txt ================================================ Copyright (C) 2014-2019, Matt Butcher and Matt Farina 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: docs/release/licenses/LICENSE-Qix--color.txt ================================================ Copyright (c) 2012 Heather Arthur 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: docs/release/licenses/LICENSE-anargu-gin-brotli.txt ================================================ MIT License Copyright (c) 2021 Anthony Arostegui 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: docs/release/licenses/LICENSE-asaskevich-govalidator.txt ================================================ The MIT License (MIT) Copyright (c) 2014-2020 Alex Saskevich 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: docs/release/licenses/LICENSE-axios-axios.txt ================================================ # Copyright (c) 2014-present Matt Zabriskie & Collaborators 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: docs/release/licenses/LICENSE-bwmarrin-snowflake.txt ================================================ Copyright (c) 2016, Bruce All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: docs/release/licenses/LICENSE-codemirror-basic-setup.txt ================================================ MIT License Copyright (C) 2018-2021 by Marijn Haverbeke and others 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: docs/release/licenses/LICENSE-codemirror-lang-markdown.txt ================================================ MIT License Copyright (C) 2018-2021 by Marijn Haverbeke and others 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: docs/release/licenses/LICENSE-codemirror-language-data.txt ================================================ MIT License Copyright (C) 2018-2021 by Marijn Haverbeke and others 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: docs/release/licenses/LICENSE-codemirror-state.txt ================================================ MIT License Copyright (C) 2018-2021 by Marijn Haverbeke and others 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: docs/release/licenses/LICENSE-codemirror-view.txt ================================================ MIT License Copyright (C) 2018-2021 by Marijn Haverbeke and others 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: docs/release/licenses/LICENSE-cznic-sqlite.txt ================================================ Copyright (c) 2017 The Sqlite Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: docs/release/licenses/LICENSE-disintegration-imaging.txt ================================================ The MIT License (MIT) Copyright (c) 2012 Grigory Dryapak 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: docs/release/licenses/LICENSE-emn178-js-sha256.txt ================================================ Copyright (c) 2014-2024 Chen, Yi-Cyuan MIT License 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: docs/release/licenses/LICENSE-facebook-react.txt ================================================ MIT License Copyright (c) Meta Platforms, Inc. and affiliates. 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: docs/release/licenses/LICENSE-gin-gonic-gin.txt ================================================ The MIT License (MIT) Copyright (c) 2014 Manuel Martínez-Almeida 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: docs/release/licenses/LICENSE-go-gomail-gomail.txt ================================================ The MIT License (MIT) Copyright (c) 2014 Alexandre Cesaro 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: docs/release/licenses/LICENSE-go-playground-locales.txt ================================================ The MIT License (MIT) Copyright (c) 2016 Go Playground 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: docs/release/licenses/LICENSE-go-playground-universal-translator.txt ================================================ The MIT License (MIT) Copyright (c) 2016 Go Playground 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: docs/release/licenses/LICENSE-go-playground-validator.txt ================================================ The MIT License (MIT) Copyright (c) 2015 Dean Karn 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: docs/release/licenses/LICENSE-go-resty-resty.txt ================================================ The MIT License (MIT) Copyright (c) 2015-present Jeevanandam M., https://myjeeva.com 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: docs/release/licenses/LICENSE-go-sql-driver-mysql.txt ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: docs/release/licenses/LICENSE-go-yaml-yaml.txt ================================================ This project is covered by two different licenses: MIT and Apache. #### MIT License #### The following files were ported to Go from C files of libyaml, and thus are still covered by their original MIT license, with the additional copyright staring in 2011 when the project was ported over: apic.go emitterc.go parserc.go readerc.go scannerc.go writerc.go yamlh.go yamlprivateh.go Copyright (c) 2006-2010 Kirill Simonov Copyright (c) 2006-2011 Kirill Simonov 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. ### Apache License ### All the remaining project files are covered by the Apache license: Copyright (c) 2011-2019 Canonical Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: docs/release/licenses/LICENSE-goccy-go-json.txt ================================================ MIT License Copyright (c) 2020 Masaaki Goshima 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: docs/release/licenses/LICENSE-golang-org-x.txt ================================================ Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: docs/release/licenses/LICENSE-google-uuid.txt ================================================ Copyright (c) 2009,2014 Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: docs/release/licenses/LICENSE-google-wire.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: docs/release/licenses/LICENSE-grokify-html-strip-tags-go.txt ================================================ Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: docs/release/licenses/LICENSE-i18next-i18next.txt ================================================ The MIT License (MIT) Copyright (c) 2023 i18next 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: docs/release/licenses/LICENSE-i18next-react-i18next.txt ================================================ The MIT License (MIT) Copyright (c) 2023 i18next 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: docs/release/licenses/LICENSE-iamkun-dayjs.txt ================================================ MIT License Copyright (c) 2018-present, iamkun 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: docs/release/licenses/LICENSE-jinzhu-copier.txt ================================================ The MIT License (MIT) Copyright (c) 2015 Jinzhu 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: docs/release/licenses/LICENSE-jinzhu-now.txt ================================================ The MIT License (MIT) Copyright (c) 2013-NOW Jinzhu 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: docs/release/licenses/LICENSE-joho-godotenv.txt ================================================ Copyright (c) 2013 John Barton MIT License 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: docs/release/licenses/LICENSE-jordan-wright-email.txt ================================================ The MIT License (MIT) Copyright (c) 2013 Jordan Wright 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: docs/release/licenses/LICENSE-jxson-front-matter.txt ================================================ # The MIT License (MIT) Copyright (c) Jason Campbell ("Author") 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: docs/release/licenses/LICENSE-kpdecker-jsdiff.txt ================================================ Software License Agreement (BSD License) Copyright (c) 2009-2015, Kevin Decker All rights reserved. Redistribution and use of this software in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Kevin Decker nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: docs/release/licenses/LICENSE-lib-pq.txt ================================================ Copyright (c) 2011-2013, 'pq' Contributors Portions Copyright (C) 2011 Blake Mizerany 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: docs/release/licenses/LICENSE-ljharb-qs.txt ================================================ BSD 3-Clause License Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: docs/release/licenses/LICENSE-lodash-lodash.txt ================================================ The MIT License Copyright JS Foundation and other contributors Based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors This software consists of voluntary contributions made by many individuals. For exact contribution history, see the revision history available at https://github.com/lodash/lodash The following license applies to all parts of this software except as documented below: ==== 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. ==== Copyright and related rights for sample code are waived via CC0. Sample code is defined as all source code displayed within the prose of the documentation. CC0: http://creativecommons.org/publicdomain/zero/1.0/ ==== Files located in the node_modules and vendor directories are externally maintained libraries used by this software which have their own licenses; we recommend you read them, as their terms may differ from the terms above. ================================================ FILE: docs/release/licenses/LICENSE-mark3labs-mcp-go.txt ================================================ MIT License Copyright (c) 2024 Anthropic, PBC 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: docs/release/licenses/LICENSE-markedjs-marked.txt ================================================ # License information ## Contribution License Agreement If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. `` ## Marked Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) 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. ## Markdown Copyright © 2004, John Gruber http://daringfireball.net/ All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. ================================================ FILE: docs/release/licenses/LICENSE-mattn-go-sqlite3.txt ================================================ The MIT License (MIT) Copyright (c) 2014 Yasuhiro Matsumoto 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: docs/release/licenses/LICENSE-microcosm-cc-bluemonday.txt ================================================ SPDX short identifier: BSD-3-Clause https://opensource.org/licenses/BSD-3-Clause Copyright (c) 2014, David Kitchen All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the organisation (Microcosm) nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: docs/release/licenses/LICENSE-mojocn-base64Captcha.txt ================================================ Copyright 2019 Eric neochau@gmail.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: docs/release/licenses/LICENSE-mozillazg-go-pinyin.txt ================================================ The MIT License (MIT) Copyright (c) 2016 mozillazg 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: docs/release/licenses/LICENSE-npm-node-semver.txt ================================================ The ISC License Copyright (c) Isaac Z. Schlueter and Contributors Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: docs/release/licenses/LICENSE-ory-dockertest.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: docs/release/licenses/LICENSE-pmndrs-zustand.txt ================================================ MIT License Copyright (c) 2019 Paul Henschel 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: docs/release/licenses/LICENSE-react-bootstrap-react-bootstrap.txt ================================================ The MIT License (MIT) Copyright (c) 2014-present Stephen J. Collings, Matthew Honnibal, Pieter Vanderwerff 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: docs/release/licenses/LICENSE-remix-run-react-router.txt ================================================ MIT License Copyright (c) React Training LLC 2015-2019 Copyright (c) Remix Software Inc. 2020-2021 Copyright (c) Shopify Inc. 2022-2023 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: docs/release/licenses/LICENSE-robfig-cron.txt ================================================ Copyright (C) 2012 Rob Figueiredo All Rights Reserved. MIT LICENSE 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: docs/release/licenses/LICENSE-sashabaranov-go-openai.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: docs/release/licenses/LICENSE-scottleedavis-go-exif-remove.txt ================================================ MIT License Copyright (c) 2019 scott lee davis 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: docs/release/licenses/LICENSE-segmentfault-pacman.txt ================================================ MIT License Copyright (c) since 2022 The Segmentfault Development Team. 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: docs/release/licenses/LICENSE-soldair-qrcode.txt ================================================ The MIT License (MIT) Copyright (c) 2012 Ryan Day 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: docs/release/licenses/LICENSE-spf13-cobra.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ================================================ FILE: docs/release/licenses/LICENSE-staylor-react-helmet-async.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2018 The New York Times Company Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: docs/release/licenses/LICENSE-stretchr-testify.txt ================================================ MIT License Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors. 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: docs/release/licenses/LICENSE-sudodoki-copy-to-clipboard.txt ================================================ MIT License Copyright (c) 2017 sudodoki 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: docs/release/licenses/LICENSE-swaggo-files.txt ================================================ MIT License Copyright (c) 2019 Swaggo 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: docs/release/licenses/LICENSE-swaggo-gin-swagger.txt ================================================ MIT License Copyright (c) 2017 Swaggo 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: docs/release/licenses/LICENSE-swaggo-swag.txt ================================================ MIT License Copyright (c) 2017 Eason Lin 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: docs/release/licenses/LICENSE-tidwall-gjson.txt ================================================ The MIT License (MIT) Copyright (c) 2016 Josh Baker 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: docs/release/licenses/LICENSE-twbs-bootstrap.txt ================================================ The MIT License (MIT) Copyright (c) 2011-2023 The Bootstrap Authors 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: docs/release/licenses/LICENSE-twbs-icons.txt ================================================ The MIT License (MIT) Copyright (c) 2019-2023 The Bootstrap Authors 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: docs/release/licenses/LICENSE-uber-go-mock.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: docs/release/licenses/LICENSE-vercel-swr.txt ================================================ MIT License Copyright (c) 2023 Vercel, Inc. 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: docs/release/licenses/LICENSE-xorm.txt ================================================ Copyright (c) 2013 - 2015 The Xorm Authors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: docs/release/licenses/LICENSE-yuin-goldmark.txt ================================================ MIT License Copyright (c) 2019 Yusuke Inuzuka 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: docs/release/licenses/LIcENSE-Bunlong-next-share.txt ================================================ The MIT License (MIT) Copyright (c) 2021 Bunlong 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: docs/swagger.json ================================================ { "swagger": "2.0", "info": { "description": "Apache Answer API", "title": "Apache Answer", "contact": {} }, "basePath": "/", "paths": { "/": { "get": { "description": "if config file not exist try to redirect to install page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "installation" ], "summary": "if config file not exist try to redirect to install page", "responses": {} } }, "/answer/admin/api/ai-config": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get AI configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get AI configuration", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteAIResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update AI configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update AI configuration", "parameters": [ { "description": "AI config", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteAIReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/ai-models": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "get AI models", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get AI models", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetAIModelResp" } } } } ] } } } } }, "/answer/admin/api/ai-provider": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get AI provider configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get AI provider configuration", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetAIProviderResp" } } } } ] } } } } }, "/answer/admin/api/ai/conversation": { "get": { "description": "get conversation detail for admin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation-admin" ], "summary": "get conversation detail for admin", "parameters": [ { "type": "string", "description": "conversation id", "name": "conversation_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.AIConversationAdminDetailResp" } } } ] } } } }, "delete": { "description": "delete conversation and its related records for admin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation-admin" ], "summary": "delete conversation for admin", "parameters": [ { "description": "apikey", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AIConversationAdminDeleteReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/ai/conversation/page": { "get": { "description": "get conversation list for admin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation-admin" ], "summary": "get conversation list for admin", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.AIConversationAdminListItem" } } } } ] } } } ] } } } } }, "/answer/admin/api/answer/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Status:[available,deleted,pending]", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "AdminAnswerPage admin answer page", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "enum": [ "available", "deleted", "pending" ], "type": "string", "description": "user status", "name": "status", "in": "query" }, { "type": "string", "description": "answer id or question title", "name": "query", "in": "query" }, { "type": "string", "description": "question id", "name": "question_id", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/answer/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update answer status", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update answer status", "parameters": [ { "description": "AdminUpdateAnswerStatusReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AdminUpdateAnswerStatusReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/api-key": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update apikey", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update apikey", "parameters": [ { "description": "apikey", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateAPIKeyReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add apikey", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "add apikey", "parameters": [ { "description": "apikey", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddAPIKeyReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.AddAPIKeyResp" } } } ] } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "delete apikey", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "delete apikey", "parameters": [ { "description": "apikey", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.DeleteAPIKeyReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/api-key/all": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get all api keys", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get all api keys", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetAPIKeyResp" } } } } ] } } } } }, "/answer/admin/api/badge/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update badge status", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AdminBadge" ], "summary": "update badge status", "parameters": [ { "description": "UpdateBadgeStatusReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateBadgeStatusReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/badges": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "list all badges by page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AdminBadge" ], "summary": "list all badges by page", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "enum": [ "", "active", "inactive" ], "type": "string", "description": "badge status", "name": "status", "in": "query" }, { "type": "string", "description": "search param", "name": "q", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetBadgeListPagedResp" } } } } ] } } } } }, "/answer/admin/api/dashboard": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "DashboardInfo", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "DashboardInfo", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/delete/permanently": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "delete permanently", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "delete permanently", "parameters": [ { "description": "DeletePermanentlyReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.DeletePermanentlyReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/language/options": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Get language options", "produces": [ "application/json" ], "tags": [ "Lang" ], "summary": "Get language options", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/mcp-config": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get MCP configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get MCP configuration", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteMCPResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update MCP configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update MCP configuration", "parameters": [ { "description": "MCP config", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteMCPReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/plugin/config": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get plugin config", "produces": [ "application/json" ], "tags": [ "AdminPlugin" ], "summary": "get plugin config", "parameters": [ { "type": "string", "description": "plugin_slug_name", "name": "plugin_slug_name", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetPluginConfigResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update plugin config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AdminPlugin" ], "summary": "update plugin config", "parameters": [ { "description": "UpdatePluginConfigReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdatePluginConfigReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/plugin/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update plugin status", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AdminPlugin" ], "summary": "update plugin status", "parameters": [ { "description": "UpdatePluginStatusReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdatePluginStatusReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/plugins": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get plugin list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AdminPlugin" ], "summary": "get plugin list", "parameters": [ { "type": "string", "description": "status: active/inactive", "name": "status", "in": "query" }, { "type": "boolean", "description": "have config", "name": "have_config", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetPluginListResp" } } } } ] } } } } }, "/answer/admin/api/question/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Status:[available,closed,deleted,pending]", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "AdminQuestionPage admin question page", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "enum": [ "available", "closed", "deleted", "pending" ], "type": "string", "description": "user status", "name": "status", "in": "query" }, { "type": "string", "description": "question id or title", "name": "query", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/question/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update question status", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update question status", "parameters": [ { "description": "AdminUpdateQuestionStatusReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AdminUpdateQuestionStatusReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/reasons": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get reasons by object type and action", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "reason" ], "summary": "get reasons by object type and action", "parameters": [ { "enum": [ "question", "answer", "comment", "user" ], "type": "string", "description": "object_type", "name": "object_type", "in": "query", "required": true }, { "enum": [ "status", "close", "flag", "review" ], "type": "string", "description": "action", "name": "action", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/roles": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get role list", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get role list", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetRoleResp" } } } } ] } } } } }, "/answer/admin/api/setting/privileges": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "GetPrivilegesConfig get privileges config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "GetPrivilegesConfig get privileges config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetPrivilegesConfigResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update privileges config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update privileges config", "parameters": [ { "description": "config", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdatePrivilegesConfigReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/setting/smtp": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "GetSMTPConfig get smtp config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "GetSMTPConfig get smtp config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetSMTPConfigResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update smtp config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update smtp config", "parameters": [ { "description": "smtp config", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateSMTPConfigReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/advanced": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site advanced setting", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site advanced setting", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteAdvancedResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site advanced info", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site advanced info", "parameters": [ { "description": "advanced settings", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteAdvancedReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/branding": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site interface", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteBrandingResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site info branding", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site info branding", "parameters": [ { "description": "branding info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteBrandingReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/custom-css-html": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site info custom html css config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site info custom html css config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site custom css html config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site custom css html config", "parameters": [ { "description": "login info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteCustomCssHTMLReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/general": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site general information", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteGeneralResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site general information", "parameters": [ { "description": "general", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteGeneralReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/interface": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site interface", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteInterfaceSettingsResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site info interface", "parameters": [ { "description": "general", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteInterfaceReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/login": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site info login config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site info login config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteLoginResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site login", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site login", "parameters": [ { "description": "login info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteLoginReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/polices": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Get the policies information for the site", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "Get the policies information for the site", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SitePoliciesResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site policies configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site policies configuration", "parameters": [ { "description": "write info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SitePoliciesReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/question": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site questions setting", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site questions setting", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteQuestionsResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site question settings", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site question settings", "parameters": [ { "description": "questions settings", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteQuestionsReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/security": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Get the security information for the site", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "Get the security information for the site", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteSecurityResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site security configuration", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site security configuration", "parameters": [ { "description": "write info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteSecurityReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/seo": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site seo information", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site seo information", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteSeoResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site seo information", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site seo information", "parameters": [ { "description": "seo", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteSeoReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/tag": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site tags setting", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site tags setting", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteTagsResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site tag settings", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site tag settings", "parameters": [ { "description": "tags settings", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteTagsReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/theme": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site info theme config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site info theme config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteThemeResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site custom css html config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site custom css html config", "parameters": [ { "description": "login info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteThemeReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/users": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site user config", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site user config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteUsersResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site info config about users", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site info config about users", "parameters": [ { "description": "users info", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteUsersReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/siteinfo/users-settings": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get site interface", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteUsersSettingsResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update site info users settings", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update site info users settings", "parameters": [ { "description": "general", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SiteUsersSettingsReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/theme/options": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "Get theme options", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "Get theme options", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/user": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "add user", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddUserReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/user/activation": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user activation", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get user activation", "parameters": [ { "type": "string", "description": "user id", "name": "user_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetUserActivationResp" } } } ] } } } } }, "/answer/admin/api/user/password": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user password", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update user password", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserPasswordReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/user/profile": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "edit user profile", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "edit user profile", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.EditUserProfileReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/user/role": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user role", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update user role", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserRoleReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/user/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "update user", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserStatusReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/users": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add users", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "add users", "parameters": [ { "description": "user", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddUsersReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/users/activation": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "send user activation", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "send user activation", "parameters": [ { "description": "SendUserActivationReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.SendUserActivationReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/admin/api/users/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user page", "produces": [ "application/json" ], "tags": [ "admin" ], "summary": "get user page", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "search query: email, username or id:[id]", "name": "query", "in": "query" }, { "type": "boolean", "description": "staff user", "name": "staff", "in": "query" }, { "enum": [ "suspended", "deleted", "inactive" ], "type": "string", "description": "user status", "name": "status", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "records": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUserPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/activity/timeline": { "get": { "description": "get object timeline", "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "get object timeline", "parameters": [ { "type": "string", "description": "object id", "name": "object_id", "in": "query" }, { "type": "string", "description": "tag slug name", "name": "tag_slug_name", "in": "query" }, { "enum": [ "question", "answer", "tag" ], "type": "string", "description": "object type", "name": "object_type", "in": "query" }, { "type": "boolean", "description": "is show vote", "name": "show_vote", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetObjectTimelineResp" } } } ] } } } } }, "/answer/api/v1/activity/timeline/detail": { "get": { "description": "get object timeline detail", "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "get object timeline detail", "parameters": [ { "type": "string", "description": "revision id", "name": "revision_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetObjectTimelineResp" } } } ] } } } } }, "/answer/api/v1/ai/conversation": { "get": { "description": "get conversation detail", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation" ], "summary": "get conversation detail", "parameters": [ { "type": "string", "description": "conversation id", "name": "conversation_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.AIConversationDetailResp" } } } ] } } } } }, "/answer/api/v1/ai/conversation/page": { "get": { "description": "get conversation list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation" ], "summary": "get conversation list", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.AIConversationListItem" } } } } ] } } } ] } } } } }, "/answer/api/v1/ai/conversation/vote": { "post": { "description": "vote record", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ai-conversation" ], "summary": "vote record", "parameters": [ { "description": "vote request", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AIConversationVoteReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/answer": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "Update Answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "Update Answer", "parameters": [ { "description": "AnswerUpdateReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AnswerUpdateReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "Add Answer", "parameters": [ { "description": "add answer request", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AnswerAddReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "delete answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "delete answer", "parameters": [ { "description": "answer", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RemoveAnswerReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/answer/acceptance": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "Accept Answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "Accept Answer", "parameters": [ { "description": "AcceptAnswerReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AcceptAnswerReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/answer/info": { "get": { "description": "Get Answer Detail", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "Get Answer Detail", "parameters": [ { "type": "string", "description": "id", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetAnswerInfoResp" } } } ] } } } } }, "/answer/api/v1/answer/page": { "get": { "description": "AnswerList \u003cbr\u003e \u003cb\u003eorder\u003c/b\u003e (default or updated)", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "AnswerList", "parameters": [ { "type": "string", "description": "question_id", "name": "question_id", "in": "query", "required": true }, { "type": "string", "description": "order", "name": "order", "in": "query", "required": true }, { "type": "string", "description": "page", "name": "page", "in": "query", "required": true }, { "type": "string", "description": "page_size", "name": "page_size", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/answer/recover": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "recover the deleted answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Answer" ], "summary": "recover answer", "parameters": [ { "description": "answer", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RecoverAnswerReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/badge": { "get": { "description": "get badge info", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "api-badge" ], "summary": "get badge info", "parameters": [ { "type": "string", "default": "string", "description": "id", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetBadgeInfoResp" } } } ] } } } } }, "/answer/api/v1/badge/awards/page": { "get": { "description": "get badge award list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "api-badge" ], "summary": "get badge award list", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "badge id", "name": "badge_id", "in": "query", "required": true }, { "type": "string", "description": "only list the award by username", "name": "username", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetBadgeInfoResp" } } } ] } } } } }, "/answer/api/v1/badge/user/awards": { "get": { "description": "get user badge award list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "api-badge" ], "summary": "get user badge award list", "parameters": [ { "type": "string", "description": "user name", "name": "username", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" } } } } ] } } } } }, "/answer/api/v1/badge/user/awards/recent": { "get": { "description": "get user badge award list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "api-badge" ], "summary": "get user badge award list", "parameters": [ { "type": "string", "description": "user name", "name": "username", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" } } } } ] } } } } }, "/answer/api/v1/badges": { "get": { "description": "list all badges group by group", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "api-badge" ], "summary": "list all badges group by group", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetBadgeListResp" } } } } ] } } } } }, "/answer/api/v1/collection/switch": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add collection", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Collection" ], "summary": "add collection", "parameters": [ { "description": "collection", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.CollectionSwitchReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.CollectionSwitchResp" } } } ] } } } } }, "/answer/api/v1/comment": { "get": { "description": "get comment by id", "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "get comment by id", "parameters": [ { "type": "string", "description": "id", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetCommentResp" } } } } ] } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update comment", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "update comment", "parameters": [ { "description": "comment", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateCommentReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add comment", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "add comment", "parameters": [ { "description": "comment", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddCommentReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetCommentResp" } } } ] } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "remove comment", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "remove comment", "parameters": [ { "description": "comment", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RemoveCommentReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/comment/page": { "get": { "description": "get comment page", "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "get comment page", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "object id", "name": "object_id", "in": "query", "required": true }, { "enum": [ "vote" ], "type": "string", "description": "query condition", "name": "query_cond", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetCommentResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/connector/binding/email": { "post": { "description": "external login binding user send email", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "PluginConnector" ], "summary": "external login binding user send email", "parameters": [ { "description": "external login binding user send email", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailResp" } } } ] } } } } }, "/answer/api/v1/connector/info": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get all enabled connectors", "produces": [ "application/json" ], "tags": [ "PluginConnector" ], "summary": "get all enabled connectors", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.ConnectorInfoResp" } } } } ] } } } } }, "/answer/api/v1/connector/user/info": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get all connectors info about user", "produces": [ "application/json" ], "tags": [ "PluginConnector" ], "summary": "get all connectors info about user", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.ConnectorUserInfoResp" } } } } ] } } } } }, "/answer/api/v1/connector/user/unbinding": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "unbind external user login", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "PluginConnector" ], "summary": "unbind external user login", "parameters": [ { "description": "ExternalLoginUnbindingReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.ExternalLoginUnbindingReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/embed/config": { "get": { "description": "get embed plugin config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Plugin" ], "summary": "get embed plugin config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/plugin.EmbedConfig" } } } } ] } } } } }, "/answer/api/v1/file": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "upload file", "consumes": [ "multipart/form-data" ], "tags": [ "Upload" ], "summary": "upload file", "parameters": [ { "enum": [ "post", "post_attachment", "avatar", "branding" ], "type": "string", "description": "identify the source of the file upload", "name": "source", "in": "formData", "required": true }, { "type": "file", "description": "file", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "string" } } } ] } } } } }, "/answer/api/v1/follow": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "follow object or cancel follow operation", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Activity" ], "summary": "follow object or cancel follow operation", "parameters": [ { "description": "follow", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.FollowReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.FollowResp" } } } ] } } } } }, "/answer/api/v1/follow/tags": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user follow tags", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Activity" ], "summary": "update user follow tags", "parameters": [ { "description": "follow", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateFollowTagsReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/language/config": { "get": { "description": "get language config mapping", "produces": [ "application/json" ], "tags": [ "Lang" ], "summary": "get language config mapping", "parameters": [ { "type": "string", "description": "Accept-Language", "name": "Accept-Language", "in": "header", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/language/options": { "get": { "description": "Get language options", "produces": [ "application/json" ], "tags": [ "Lang" ], "summary": "Get language options", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/meta/reaction": { "get": { "description": "get reaction for an object", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Meta" ], "summary": "get reaction", "parameters": [ { "type": "string", "description": "object_id", "name": "object_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.ReactionRespItem" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update reaction. if not exist, add one", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Meta" ], "summary": "add or update reaction", "parameters": [ { "description": "reaction", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateReactionReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/notification/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get notification list", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Notification" ], "summary": "get notification list", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "enum": [ "inbox", "achievement" ], "type": "string", "description": "type", "name": "type", "in": "query", "required": true }, { "enum": [ "all", "posts", "invites", "votes" ], "type": "string", "description": "inbox_type", "name": "inbox_type", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/notification/read/state": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "ClearUnRead", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Notification" ], "summary": "ClearUnRead", "parameters": [ { "description": "NotificationClearIDRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.NotificationClearIDRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/notification/read/state/all": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "ClearUnRead", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Notification" ], "summary": "ClearUnRead", "parameters": [ { "description": "NotificationClearRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.NotificationClearRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/notification/status": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "GetRedDot", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Notification" ], "summary": "GetRedDot", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "DelRedDot", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Notification" ], "summary": "DelRedDot", "parameters": [ { "description": "NotificationClearRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.NotificationClearRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/permission": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "check user permission", "produces": [ "application/json" ], "tags": [ "Permission" ], "summary": "check user permission", "parameters": [ { "type": "string", "description": "access-token", "name": "Authorization", "in": "header", "required": true }, { "enum": [ "question.add", "question.edit", "question.edit_without_review", "question.delete", "question.close", "question.reopen", "question.vote_up", "question.vote_down", "question.pin", "question.unpin", "question.hide", "question.show", "answer.add", "answer.edit", "answer.edit_without_review", "answer.delete", "answer.accept", "answer.vote_up", "answer.vote_down", "answer.invite_someone_to_answer", "comment.add", "comment.edit", "comment.delete", "comment.vote_up", "comment.vote_down", "report.add", "tag.add", "tag.edit", "tag.edit_slug_name", "tag.edit_without_review", "tag.delete", "tag.synonym", "link.url_limit", "vote.detail", "answer.audit", "question.audit", "tag.audit", "tag.use_reserved_tag" ], "type": "string", "description": "permission key", "name": "action", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": { "type": "boolean" } } } } ] } } } } }, "/answer/api/v1/personal/answer/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "list personal answers", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Personal" ], "summary": "list personal answers", "parameters": [ { "type": "string", "default": "string", "description": "username", "name": "username", "in": "query", "required": true }, { "enum": [ "newest", "score" ], "type": "string", "description": "order", "name": "order", "in": "query", "required": true }, { "type": "string", "default": "0", "description": "page", "name": "page", "in": "query", "required": true }, { "type": "string", "default": "20", "description": "page_size", "name": "page_size", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/personal/collection/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "list personal collections", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Collection" ], "summary": "list personal collections", "parameters": [ { "type": "string", "default": "0", "description": "page", "name": "page", "in": "query", "required": true }, { "type": "string", "default": "20", "description": "page_size", "name": "page_size", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/personal/comment/page": { "get": { "description": "user personal comment list", "produces": [ "application/json" ], "tags": [ "Comment" ], "summary": "user personal comment list", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "username", "name": "username", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetCommentPersonalWithPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/personal/qa/top": { "get": { "description": "UserTop", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "UserTop", "parameters": [ { "type": "string", "default": "string", "description": "username", "name": "username", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/personal/rank/page": { "get": { "description": "user personal rank list", "produces": [ "application/json" ], "tags": [ "Rank" ], "summary": "user personal rank list", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "username", "name": "username", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetRankPersonalPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/personal/user/info": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "GetOtherUserInfoByUsername", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "GetOtherUserInfoByUsername", "parameters": [ { "type": "string", "description": "username", "name": "username", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetOtherUserInfoResp" } } } ] } } } } }, "/answer/api/v1/personal/vote/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user personal votes", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Activity" ], "summary": "get user personal votes", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetVoteWithPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/plugin/status": { "get": { "description": "get all plugins status", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Plugin" ], "summary": "get all plugins status", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetPluginListResp" } } } } ] } } } } }, "/answer/api/v1/post/render": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "render post content", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Upload" ], "summary": "render post content", "parameters": [ { "description": "PostRenderReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.PostRenderReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "update question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionUpdate" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "add question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionAdd" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "delete question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "delete question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RemoveQuestionReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/answer": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add question and answer", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "add question and answer", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionAddByAnswer" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/info": { "get": { "description": "get question details", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "get question details", "parameters": [ { "type": "string", "default": "1", "description": "Question TagID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/question/invite": { "get": { "description": "get question invite user info", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "get question invite user info", "parameters": [ { "type": "string", "default": "1", "description": "Question ID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update question invite user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "update question invite user", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionUpdateInviteUser" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/link": { "get": { "description": "get question link", "tags": [ "Question" ], "summary": "get question link", "parameters": [ { "minimum": 1, "type": "integer", "name": "in_days", "in": "query" }, { "enum": [ "newest", "active", "hot", "score", "unanswered", "recommend", "frequent" ], "type": "string", "name": "order", "in": "query" }, { "minimum": 1, "type": "integer", "name": "page", "in": "query" }, { "maximum": 100, "minimum": 1, "type": "integer", "name": "page_size", "in": "query" }, { "type": "string", "name": "question_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.QuestionPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/question/operation": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "Operation question \\n operation [pin unpin hide show]", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "Operation question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.OperationQuestionReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/page": { "get": { "description": "get questions by page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "get questions by page", "parameters": [ { "description": "QuestionPageReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionPageReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.QuestionPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/question/recommend/page": { "get": { "description": "get recommend questions by page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "get recommend questions by page", "parameters": [ { "description": "QuestionPageReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionPageReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.QuestionPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/question/recover": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "recover deleted question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "recover deleted question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.QuestionRecoverReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/reopen": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "reopen question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "reopen question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.ReopenQuestionReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/similar": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "fuzzy query similar questions based on title", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "fuzzy query similar questions based on title", "parameters": [ { "type": "string", "default": "string", "description": "title", "name": "title", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/similar/tag": { "get": { "description": "Search Similar Question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "Search Similar Question", "parameters": [ { "type": "string", "default": "", "description": "question_id", "name": "question_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/question/status": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "Close question", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Question" ], "summary": "Close question", "parameters": [ { "description": "question", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.CloseQuestionReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/question/tags": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get tag list", "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get tag list", "parameters": [ { "type": "string", "description": "tag", "name": "tag", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetTagBasicResp" } } } } ] } } } } }, "/answer/api/v1/reasons": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get reasons by object type and action", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "reason" ], "summary": "get reasons by object type and action", "parameters": [ { "enum": [ "question", "answer", "comment", "user" ], "type": "string", "description": "object_type", "name": "object_type", "in": "query", "required": true }, { "enum": [ "status", "close", "flag", "review" ], "type": "string", "description": "action", "name": "action", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/render/config": { "get": { "description": "GetRenderConfig", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "PluginRender" ], "summary": "GetRenderConfig", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/plugin.RenderConfig" } } } ] } } } } }, "/answer/api/v1/report": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add report \u003cbr\u003e source (question, answer, comment, user)", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Report" ], "summary": "add report", "parameters": [ { "description": "report", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddReportReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/report/review": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "review report", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Report" ], "summary": "review report", "parameters": [ { "description": "flag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.ReviewReportReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/report/unreviewed/post": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get unreviewed report post page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Report" ], "summary": "get unreviewed report post page", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetReportListPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/review/pending/post": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update review", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Review" ], "summary": "update review", "parameters": [ { "description": "review", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateReviewReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/review/pending/post/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get unreviewed post page", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Review" ], "summary": "get unreviewed post page", "parameters": [ { "type": "integer", "description": "page", "name": "page", "in": "query" }, { "type": "string", "description": "object_id", "name": "object_id", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUnreviewedPostPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/reviewing/type": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get reviewing type", "produces": [ "application/json" ], "tags": [ "Revision" ], "summary": "get reviewing type", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetReviewingTypeResp" } } } } ] } } } } }, "/answer/api/v1/revisions": { "get": { "description": "get revision list", "produces": [ "application/json" ], "tags": [ "Revision" ], "summary": "get revision list", "parameters": [ { "type": "string", "description": "object id", "name": "object_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetRevisionResp" } } } } ] } } } } }, "/answer/api/v1/revisions/audit": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "revision audit operation:approve or reject", "produces": [ "application/json" ], "tags": [ "Revision" ], "summary": "revision audit", "parameters": [ { "description": "audit", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RevisionAuditReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/revisions/edit/check": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "check can update revision", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Revision" ], "summary": "check can update revision", "parameters": [ { "type": "string", "default": "string", "description": "id", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/revisions/unreviewed": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get unreviewed revision list", "produces": [ "application/json" ], "tags": [ "Revision" ], "summary": "get unreviewed revision list", "parameters": [ { "type": "string", "description": "page id", "name": "page", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUnreviewedRevisionResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/search": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "search object", "produces": [ "application/json" ], "tags": [ "Search" ], "summary": "search object", "parameters": [ { "type": "string", "description": "query string", "name": "q", "in": "query", "required": true }, { "enum": [ "newest", "active", "score", "relevance" ], "type": "string", "description": "order", "name": "order", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SearchResp" } } } ] } } } } }, "/answer/api/v1/search/desc": { "get": { "description": "get search description", "produces": [ "application/json" ], "tags": [ "Search" ], "summary": "get search description", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SearchResp" } } } ] } } } } }, "/answer/api/v1/siteinfo": { "get": { "description": "get site info", "produces": [ "application/json" ], "tags": [ "site" ], "summary": "get site info", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.SiteInfoResp" } } } ] } } } } }, "/answer/api/v1/siteinfo/legal": { "get": { "description": "get site legal info", "produces": [ "application/json" ], "tags": [ "site" ], "summary": "get site legal info", "parameters": [ { "enum": [ "tos", "privacy" ], "type": "string", "description": "legal information type", "name": "info_type", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetSiteLegalInfoResp" } } } ] } } } } }, "/answer/api/v1/tag": { "get": { "description": "get tag one", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get tag one", "parameters": [ { "type": "string", "description": "tag id", "name": "tag_id", "in": "query", "required": true }, { "type": "string", "description": "tag name", "name": "tag_name", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetTagResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "update tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateTagReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "add tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddTagReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "description": "delete tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "delete tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RemoveTagReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/tag/merge": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "merge tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "merge tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.AddTagReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/tag/recover": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "recover delete tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "recover delete tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.RecoverTagReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/tag/synonym": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update tag", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "update tag", "parameters": [ { "description": "tag", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateTagSynonymReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/tag/synonyms": { "get": { "description": "get tag synonyms", "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get tag synonyms", "parameters": [ { "type": "integer", "description": "tag id", "name": "tag_id", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetTagSynonymsResp" } } } ] } } } } }, "/answer/api/v1/tags": { "get": { "description": "get tags list by slug name", "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get tags list", "parameters": [ { "type": "array", "items": { "type": "string" }, "collectionFormat": "csv", "description": "string collection", "name": "tags", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetTagBasicResp" } } } } ] } } } } }, "/answer/api/v1/tags/following": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get following tag list", "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get following tag list", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetFollowingTagsResp" } } } } ] } } } } }, "/answer/api/v1/tags/page": { "get": { "description": "get tag page", "produces": [ "application/json" ], "tags": [ "Tag" ], "summary": "get tag page", "parameters": [ { "type": "integer", "description": "page size", "name": "page", "in": "query" }, { "type": "integer", "description": "page size", "name": "page_size", "in": "query" }, { "type": "string", "description": "slug_name", "name": "slug_name", "in": "query" }, { "enum": [ "popular", "name", "newest" ], "type": "string", "description": "query condition", "name": "query_cond", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/pager.PageModel" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/schema.GetTagPageResp" } } } } ] } } } ] } } } } }, "/answer/api/v1/user/action/record": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "ActionRecord", "tags": [ "User" ], "summary": "ActionRecord", "parameters": [ { "enum": [ "login", "e_mail", "find_pass" ], "type": "string", "description": "action", "name": "action", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.ActionRecordResp" } } } ] } } } } }, "/answer/api/v1/user/email": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "user change email verification", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "user change email verification", "parameters": [ { "description": "UserChangeEmailVerifyReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserChangeEmailVerifyReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/email/change/code": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "send email to the user email then change their email", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "send email to the user email then change their email", "parameters": [ { "description": "UserChangeEmailSendCodeReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserChangeEmailSendCodeReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/email/verification": { "post": { "description": "UserVerifyEmail", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserVerifyEmail", "parameters": [ { "type": "string", "default": "", "description": "code", "name": "code", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.UserLoginResp" } } } ] } } } } }, "/answer/api/v1/user/email/verification/send": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "UserVerifyEmailSend", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserVerifyEmailSend", "parameters": [ { "type": "string", "default": "", "description": "captcha_id", "name": "captcha_id", "in": "query" }, { "type": "string", "default": "", "description": "captcha_code", "name": "captcha_code", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/user/info": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user info, if user no login response http code is 200, but user info is null", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "GetUserInfoByUserID", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetCurrentLoginUserInfoResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "UserUpdateInfo update user info", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserUpdateInfo update user info", "parameters": [ { "type": "string", "description": "access-token", "name": "Authorization", "in": "header", "required": true }, { "description": "UpdateInfoRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateInfoRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/info/search": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "SearchUserListByName", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "SearchUserListByName", "parameters": [ { "type": "string", "description": "username", "name": "username", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetOtherUserInfoResp" } } } ] } } } } }, "/answer/api/v1/user/interface": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "UserUpdateInterface update user interface config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserUpdateInterface update user interface config", "parameters": [ { "type": "string", "description": "access-token", "name": "Authorization", "in": "header", "required": true }, { "description": "UpdateInfoRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/login/email": { "post": { "description": "UserEmailLogin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserEmailLogin", "parameters": [ { "description": "UserEmailLogin", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserEmailLoginReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.UserLoginResp" } } } ] } } } } }, "/answer/api/v1/user/logout": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "user logout", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "user logout", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/notification/config": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user's notification config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "update user's notification config", "parameters": [ { "description": "UpdateUserNotificationConfigReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserNotificationConfigReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user's notification config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "get user's notification config", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetUserNotificationConfigResp" } } } ] } } } } }, "/answer/api/v1/user/notification/unsubscribe": { "put": { "description": "unsubscribe notification", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "unsubscribe notification", "parameters": [ { "description": "UserUnsubscribeNotificationReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserUnsubscribeNotificationReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/password": { "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "UserModifyPassWord", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserModifyPassWord", "parameters": [ { "description": "UserModifyPasswordReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserModifyPasswordReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/password/replacement": { "post": { "description": "UseRePassWord", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UseRePassWord", "parameters": [ { "description": "UserRePassWordRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserRePassWordRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/user/password/reset": { "post": { "description": "RetrievePassWord", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "RetrievePassWord", "parameters": [ { "description": "UserRetrievePassWordRequest", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserRetrievePassWordRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/answer/api/v1/user/plugin/config": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get user plugin config", "produces": [ "application/json" ], "tags": [ "UserPlugin" ], "summary": "get user plugin config", "parameters": [ { "type": "string", "description": "plugin_slug_name", "name": "plugin_slug_name", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetPluginConfigResp" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "description": "update user plugin config", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "UserPlugin" ], "summary": "update user plugin config", "parameters": [ { "description": "UpdatePluginConfigReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UpdateUserPluginConfigReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/answer/api/v1/user/plugin/configs": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "get plugin list that used for user.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "UserPlugin" ], "summary": "get plugin list that used for user.", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/schema.GetUserPluginListResp" } } } } ] } } } } }, "/answer/api/v1/user/ranking": { "get": { "description": "get user ranking", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "get user ranking", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.UserRankingResp" } } } ] } } } } }, "/answer/api/v1/user/register/email": { "post": { "description": "UserRegisterByEmail", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "UserRegisterByEmail", "parameters": [ { "description": "UserRegisterReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.UserRegisterReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.UserLoginResp" } } } ] } } } } }, "/answer/api/v1/user/staff": { "get": { "description": "get user staff", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "get user staff", "parameters": [ { "type": "string", "description": "username", "name": "username", "in": "query", "required": true }, { "type": "string", "description": "page_size", "name": "page_size", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.GetUserStaffResp" } } } ] } } } } }, "/answer/api/v1/vote/down": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add vote", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Activity" ], "summary": "vote down", "parameters": [ { "description": "vote", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.VoteReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.VoteResp" } } } ] } } } } }, "/answer/api/v1/vote/up": { "post": { "security": [ { "ApiKeyAuth": [] } ], "description": "add vote", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Activity" ], "summary": "vote up", "parameters": [ { "description": "vote", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schema.VoteReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/schema.VoteResp" } } } ] } } } } }, "/custom.css": { "get": { "description": "get site custom CSS", "produces": [ "text/css" ], "tags": [ "site" ], "summary": "get site custom CSS", "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/installation/base-info": { "post": { "description": "init base info", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "installation" ], "summary": "init base info", "parameters": [ { "description": "InitBaseInfoReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/install.InitBaseInfoReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/installation/config-file/check": { "post": { "description": "check config file if exist when installation", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "installation" ], "summary": "check config file if exist when installation", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/install.CheckConfigFileResp" } } } ] } } } } }, "/installation/db/check": { "post": { "description": "check database if exist when installation", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "installation" ], "summary": "check database if exist when installation", "parameters": [ { "description": "CheckDatabaseReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/install.CheckDatabaseReq" } } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/install.CheckConfigFileResp" } } } ] } } } } }, "/installation/init": { "post": { "description": "init environment", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "installation" ], "summary": "init environment", "parameters": [ { "description": "CheckDatabaseReq", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/install.CheckDatabaseReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/installation/language/config": { "get": { "description": "get installation language config mapping", "produces": [ "application/json" ], "tags": [ "Lang" ], "summary": "get installation language config mapping", "parameters": [ { "type": "string", "description": "installation language", "name": "lang", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/installation/language/options": { "get": { "description": "get installation language options", "produces": [ "application/json" ], "tags": [ "Lang" ], "summary": "get installation language options", "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/handler.RespBody" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/translator.LangOption" } } } } ] } } } } }, "/personal/question/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], "description": "list personal questions", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Personal" ], "summary": "list personal questions", "parameters": [ { "type": "string", "default": "string", "description": "username", "name": "username", "in": "query", "required": true }, { "enum": [ "newest", "score" ], "type": "string", "description": "order", "name": "order", "in": "query", "required": true }, { "type": "string", "default": "0", "description": "page", "name": "page", "in": "query", "required": true }, { "type": "string", "default": "20", "description": "page_size", "name": "page_size", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handler.RespBody" } } } } }, "/robots.txt": { "get": { "description": "get site robots information", "produces": [ "application/json" ], "tags": [ "site" ], "summary": "get site robots information", "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } } }, "definitions": { "constant.NotificationChannelKey": { "type": "string", "enum": [ "email" ], "x-enum-varnames": [ "EmailChannel" ] }, "constant.Privilege": { "type": "object", "properties": { "key": { "type": "string" }, "label": { "type": "string" }, "value": { "type": "integer", "minimum": 1 } } }, "entity.BadgeLevel": { "type": "integer", "enum": [ 1, 2, 3 ], "x-enum-varnames": [ "BadgeLevelBronze", "BadgeLevelSilver", "BadgeLevelGold" ] }, "handler.RespBody": { "type": "object", "properties": { "code": { "description": "http code", "type": "integer" }, "data": { "description": "response data" }, "msg": { "description": "response message", "type": "string" }, "reason": { "description": "reason key", "type": "string" } } }, "install.CheckConfigFileResp": { "type": "object", "properties": { "config_file_exist": { "type": "boolean" }, "db_connection_success": { "type": "boolean" }, "db_table_exist": { "type": "boolean" } } }, "install.CheckDatabaseReq": { "type": "object", "required": [ "db_type" ], "properties": { "db_file": { "type": "string" }, "db_host": { "type": "string" }, "db_name": { "type": "string" }, "db_password": { "type": "string" }, "db_type": { "type": "string", "enum": [ "postgres", "sqlite3", "mysql" ] }, "db_username": { "type": "string" }, "ssl_cert": { "type": "string" }, "ssl_enabled": { "type": "boolean" }, "ssl_key": { "type": "string" }, "ssl_mode": { "type": "string" }, "ssl_root_cert": { "type": "string" } } }, "install.InitBaseInfoReq": { "type": "object", "required": [ "contact_email", "email", "external_content_display", "lang", "name", "password", "site_name", "site_url" ], "properties": { "contact_email": { "type": "string", "maxLength": 500 }, "email": { "type": "string", "maxLength": 500 }, "external_content_display": { "type": "string", "enum": [ "always_display", "ask_before_display" ] }, "lang": { "type": "string", "maxLength": 30 }, "login_required": { "type": "boolean" }, "name": { "type": "string", "maxLength": 30, "minLength": 2 }, "password": { "type": "string", "maxLength": 32, "minLength": 8 }, "site_name": { "type": "string", "maxLength": 30 }, "site_url": { "type": "string", "maxLength": 512 } } }, "pager.PageModel": { "type": "object", "properties": { "count": { "type": "integer" }, "list": {} } }, "plugin.EmbedConfig": { "type": "object", "properties": { "enable": { "type": "boolean" }, "platform": { "type": "string" } } }, "plugin.RenderConfig": { "type": "object", "properties": { "select_theme": { "type": "string" } } }, "schema.AIConversationAdminDeleteReq": { "type": "object", "required": [ "conversation_id" ], "properties": { "conversation_id": { "type": "string" } } }, "schema.AIConversationAdminDetailResp": { "type": "object", "properties": { "conversation_id": { "type": "string" }, "created_at": { "type": "integer" }, "records": { "type": "array", "items": { "$ref": "#/definitions/schema.AIConversationRecord" } }, "topic": { "type": "string" }, "user_info": { "$ref": "#/definitions/schema.AIConversationUserInfo" } } }, "schema.AIConversationAdminListItem": { "type": "object", "properties": { "created_at": { "type": "integer" }, "helpful_count": { "type": "integer" }, "id": { "type": "string" }, "topic": { "type": "string" }, "unhelpful_count": { "type": "integer" }, "user_info": { "$ref": "#/definitions/schema.AIConversationUserInfo" } } }, "schema.AIConversationDetailResp": { "type": "object", "properties": { "conversation_id": { "type": "string" }, "created_at": { "type": "integer" }, "records": { "type": "array", "items": { "$ref": "#/definitions/schema.AIConversationRecord" } }, "topic": { "type": "string" }, "updated_at": { "type": "integer" } } }, "schema.AIConversationListItem": { "type": "object", "properties": { "conversation_id": { "type": "string" }, "created_at": { "type": "integer" }, "topic": { "type": "string" } } }, "schema.AIConversationRecord": { "type": "object", "properties": { "chat_completion_id": { "type": "string" }, "content": { "type": "string" }, "created_at": { "type": "integer" }, "helpful": { "type": "integer" }, "role": { "type": "string" }, "unhelpful": { "type": "integer" } } }, "schema.AIConversationUserInfo": { "type": "object", "properties": { "avatar": { "type": "string" }, "display_name": { "type": "string" }, "id": { "type": "string" }, "rank": { "type": "integer" }, "username": { "type": "string" } } }, "schema.AIConversationVoteReq": { "type": "object", "required": [ "chat_completion_id", "vote_type" ], "properties": { "cancel": { "type": "boolean" }, "chat_completion_id": { "type": "string" }, "vote_type": { "type": "string", "enum": [ "helpful", "unhelpful" ] } } }, "schema.AIPromptConfig": { "type": "object", "properties": { "en_us": { "type": "string" }, "zh_cn": { "type": "string" } } }, "schema.AcceptAnswerReq": { "type": "object", "required": [ "question_id" ], "properties": { "answer_id": { "type": "string" }, "question_id": { "type": "string", "maxLength": 30 } } }, "schema.ActObjectInfo": { "type": "object", "properties": { "answer_id": { "type": "string" }, "display_name": { "type": "string" }, "main_tag_slug_name": { "type": "string" }, "object_type": { "type": "string" }, "question_id": { "type": "string" }, "title": { "type": "string" }, "username": { "type": "string" } } }, "schema.ActObjectTimeline": { "type": "object", "properties": { "activity_id": { "type": "string" }, "activity_type": { "type": "string" }, "cancelled": { "type": "boolean" }, "cancelled_at": { "type": "integer" }, "comment": { "type": "string" }, "created_at": { "type": "integer" }, "object_id": { "type": "string" }, "object_type": { "type": "string" }, "revision_id": { "type": "string" }, "user_info": { "$ref": "#/definitions/schema.UserBasicInfo" } } }, "schema.ActionRecordResp": { "type": "object", "properties": { "captcha_id": { "type": "string" }, "captcha_img": { "type": "string" }, "verify": { "type": "boolean" } } }, "schema.AddAPIKeyReq": { "type": "object", "required": [ "description", "scope" ], "properties": { "description": { "type": "string", "maxLength": 150 }, "scope": { "type": "string", "enum": [ "read-only", "global" ] } } }, "schema.AddAPIKeyResp": { "type": "object", "properties": { "access_key": { "type": "string" } } }, "schema.AddCommentReq": { "type": "object", "required": [ "object_id", "original_text" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "mention_username_list": { "description": "@ user id list", "type": "array", "items": { "type": "string" } }, "object_id": { "description": "object id", "type": "string" }, "original_text": { "description": "original comment content", "type": "string", "maxLength": 600, "minLength": 2 }, "reply_comment_id": { "description": "reply comment id", "type": "string" } } }, "schema.AddReportReq": { "type": "object", "required": [ "object_id", "report_type" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "content": { "description": "report content", "type": "string", "maxLength": 500 }, "object_id": { "description": "object id", "type": "string", "maxLength": 20 }, "report_type": { "description": "report type", "type": "integer" } } }, "schema.AddTagReq": { "type": "object", "required": [ "display_name", "original_text", "slug_name" ], "properties": { "display_name": { "description": "display_name", "type": "string", "maxLength": 35 }, "original_text": { "description": "original text", "type": "string", "maxLength": 65536 }, "slug_name": { "description": "slug_name", "type": "string", "maxLength": 35 } } }, "schema.AddUserReq": { "type": "object", "required": [ "display_name", "email", "password" ], "properties": { "display_name": { "type": "string", "maxLength": 30, "minLength": 2 }, "email": { "type": "string", "maxLength": 500 }, "password": { "type": "string", "maxLength": 32, "minLength": 8 } } }, "schema.AddUsersReq": { "type": "object", "properties": { "users": { "description": "users info line by line", "type": "string" } } }, "schema.AdminUpdateAnswerStatusReq": { "type": "object", "required": [ "answer_id", "status" ], "properties": { "answer_id": { "type": "string" }, "status": { "type": "string", "enum": [ "available", "deleted" ] } } }, "schema.AdminUpdateQuestionStatusReq": { "type": "object", "required": [ "question_id", "status" ], "properties": { "question_id": { "type": "string" }, "status": { "type": "string", "enum": [ "available", "closed", "deleted" ] } } }, "schema.AnswerAddReq": { "type": "object", "required": [ "content" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "content": { "type": "string", "maxLength": 65535, "minLength": 6 }, "question_id": { "type": "string" } } }, "schema.AnswerInfo": { "type": "object", "properties": { "accepted": { "type": "integer" }, "collected": { "type": "boolean" }, "content": { "type": "string" }, "create_time": { "type": "integer" }, "html": { "type": "string" }, "id": { "type": "string" }, "member_actions": { "description": "MemberActions", "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "question_id": { "type": "string" }, "question_info": { "$ref": "#/definitions/schema.QuestionInfoResp" }, "status": { "type": "integer" }, "update_time": { "type": "integer" }, "update_user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "vote_count": { "type": "integer" }, "vote_status": { "type": "string" } } }, "schema.AnswerUpdateReq": { "type": "object", "required": [ "content" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "content": { "type": "string", "maxLength": 65535, "minLength": 6 }, "edit_summary": { "type": "string" }, "id": { "type": "string" }, "title": { "type": "string" } } }, "schema.AvatarInfo": { "type": "object", "properties": { "custom": { "type": "string", "maxLength": 200 }, "gravatar": { "type": "string", "maxLength": 200 }, "type": { "type": "string", "maxLength": 100 } } }, "schema.BadgeListInfo": { "type": "object", "properties": { "award_count": { "description": "badge award count", "type": "integer" }, "earned_count": { "description": "badge earned count", "type": "integer" }, "icon": { "description": "badge icon", "type": "string" }, "id": { "description": "badge id", "type": "string" }, "level": { "description": "badge level", "allOf": [ { "$ref": "#/definitions/entity.BadgeLevel" } ] }, "name": { "description": "badge name", "type": "string" } } }, "schema.BadgeStatus": { "type": "string", "enum": [ "active", "inactive" ], "x-enum-varnames": [ "BadgeStatusActive", "BadgeStatusInactive" ] }, "schema.CloseQuestionReq": { "type": "object", "required": [ "id" ], "properties": { "close_msg": { "description": "close_type", "type": "string" }, "close_type": { "description": "close_type", "type": "integer" }, "id": { "type": "string" } } }, "schema.CollectionSwitchReq": { "type": "object", "required": [ "group_id", "object_id" ], "properties": { "bookmark": { "type": "boolean" }, "group_id": { "type": "string" }, "object_id": { "type": "string" } } }, "schema.CollectionSwitchResp": { "type": "object", "properties": { "object_collection_count": { "type": "integer" } } }, "schema.ConfigField": { "type": "object", "properties": { "description": { "type": "string" }, "name": { "type": "string" }, "options": { "type": "array", "items": { "$ref": "#/definitions/schema.ConfigFieldOption" } }, "required": { "type": "boolean" }, "title": { "type": "string" }, "type": { "type": "string" }, "ui_options": { "$ref": "#/definitions/schema.ConfigFieldUIOptions" }, "value": {} } }, "schema.ConfigFieldOption": { "type": "object", "properties": { "label": { "type": "string" }, "value": { "type": "string" } } }, "schema.ConfigFieldUIOptions": { "type": "object", "properties": { "action": { "$ref": "#/definitions/schema.UIOptionAction" }, "class_name": { "type": "string" }, "field_class_name": { "type": "string" }, "input_type": { "type": "string" }, "label": { "type": "string" }, "placeholder": { "type": "string" }, "rows": { "type": "string" }, "text": { "type": "string" }, "variant": { "type": "string" } } }, "schema.ConnectorInfoResp": { "type": "object", "properties": { "icon": { "type": "string" }, "link": { "type": "string" }, "name": { "type": "string" } } }, "schema.ConnectorUserInfoResp": { "type": "object", "properties": { "binding": { "type": "boolean" }, "external_id": { "type": "string" }, "icon": { "type": "string" }, "link": { "type": "string" }, "name": { "type": "string" } } }, "schema.DeleteAPIKeyReq": { "type": "object", "properties": { "id": { "type": "integer" } } }, "schema.DeletePermanentlyReq": { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "users", "questions", "answers" ] } } }, "schema.EditUserProfileReq": { "type": "object", "required": [ "display_name", "email", "user_id" ], "properties": { "display_name": { "type": "string", "maxLength": 30, "minLength": 2 }, "email": { "type": "string", "maxLength": 500 }, "user_id": { "type": "string" }, "username": { "type": "string", "maxLength": 30, "minLength": 2 } } }, "schema.ExternalLoginBindingUserSendEmailReq": { "type": "object", "required": [ "binding_key", "email" ], "properties": { "binding_key": { "type": "string", "maxLength": 100 }, "email": { "type": "string", "maxLength": 512 }, "must": { "description": "If must is true, whatever email if exists, try to bind user.\nIf must is false, when email exist, will only be prompted with a warning.", "type": "boolean" } } }, "schema.ExternalLoginBindingUserSendEmailResp": { "type": "object", "properties": { "access_token": { "type": "string" }, "email_exist_and_must_be_confirmed": { "type": "boolean" } } }, "schema.ExternalLoginUnbindingReq": { "type": "object", "required": [ "external_id" ], "properties": { "external_id": { "type": "string", "maxLength": 128 } } }, "schema.FollowReq": { "type": "object", "required": [ "object_id" ], "properties": { "is_cancel": { "description": "is cancel", "type": "boolean" }, "object_id": { "description": "object id", "type": "string" } } }, "schema.FollowResp": { "type": "object", "properties": { "follows": { "description": "the followers of object", "type": "integer" }, "is_followed": { "description": "if user is followed object will be true,otherwise false", "type": "boolean" } } }, "schema.GetAIModelResp": { "type": "object", "properties": { "created": { "type": "integer" }, "id": { "type": "string" }, "object": { "type": "string" }, "owned_by": { "type": "string" } } }, "schema.GetAIProviderResp": { "type": "object", "properties": { "default_api_host": { "type": "string" }, "display_name": { "type": "string" }, "name": { "type": "string" } } }, "schema.GetAPIKeyResp": { "type": "object", "properties": { "access_key": { "type": "string" }, "created_at": { "type": "integer" }, "description": { "type": "string" }, "id": { "type": "integer" }, "last_used_at": { "type": "integer" }, "scope": { "type": "string" } } }, "schema.GetAnswerInfoResp": { "type": "object", "properties": { "info": { "$ref": "#/definitions/schema.AnswerInfo" }, "question": { "$ref": "#/definitions/schema.QuestionInfoResp" } } }, "schema.GetBadgeInfoResp": { "type": "object", "properties": { "award_count": { "description": "badge award count", "type": "integer" }, "description": { "description": "badge description", "type": "string" }, "earned_count": { "description": "badge earned count", "type": "integer" }, "icon": { "description": "badge icon", "type": "string" }, "id": { "description": "badge id", "type": "string" }, "is_single": { "description": "badge is single or multiple", "type": "boolean" }, "level": { "description": "badge level", "allOf": [ { "$ref": "#/definitions/entity.BadgeLevel" } ] }, "name": { "description": "badge name", "type": "string" } } }, "schema.GetBadgeListPagedResp": { "type": "object", "properties": { "award_count": { "description": "badge award count", "type": "integer" }, "description": { "description": "badge description", "type": "string" }, "earned": { "description": "badge earned count", "type": "boolean" }, "group_name": { "description": "badge group name", "type": "string" }, "icon": { "description": "badge icon", "type": "string" }, "id": { "description": "badge id", "type": "string" }, "level": { "description": "badge level", "allOf": [ { "$ref": "#/definitions/entity.BadgeLevel" } ] }, "name": { "description": "badge name", "type": "string" }, "status": { "description": "badge status", "allOf": [ { "$ref": "#/definitions/schema.BadgeStatus" } ] } } }, "schema.GetBadgeListResp": { "type": "object", "properties": { "badges": { "description": "badge list info", "type": "array", "items": { "$ref": "#/definitions/schema.BadgeListInfo" } }, "group_name": { "description": "badge group name", "type": "string" } } }, "schema.GetCommentPersonalWithPageResp": { "type": "object", "properties": { "answer_id": { "description": "answer id", "type": "string" }, "comment_id": { "description": "comment id", "type": "string" }, "content": { "description": "content", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "object_id": { "description": "object id", "type": "string" }, "object_type": { "description": "object type", "type": "string", "enum": [ "question", "answer", "tag", "comment" ] }, "question_id": { "description": "question id", "type": "string" }, "title": { "description": "title", "type": "string" }, "url_title": { "description": "url title", "type": "string" } } }, "schema.GetCommentResp": { "type": "object", "properties": { "comment_id": { "description": "comment id", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "is_vote": { "description": "current user if already vote this comment", "type": "boolean" }, "member_actions": { "description": "MemberActions", "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "object_id": { "description": "object id", "type": "string" }, "original_text": { "description": "original comment content", "type": "string" }, "parsed_text": { "description": "parsed comment content", "type": "string" }, "reply_comment_id": { "description": "reply comment id", "type": "string" }, "reply_user_display_name": { "description": "reply user display name", "type": "string" }, "reply_user_id": { "description": "reply user id", "type": "string" }, "reply_user_status": { "description": "reply user status", "type": "string" }, "reply_username": { "description": "reply user username", "type": "string" }, "user_avatar": { "description": "user avatar", "type": "string" }, "user_display_name": { "description": "user display name", "type": "string" }, "user_id": { "description": "user id", "type": "string" }, "user_status": { "description": "user status", "type": "string" }, "username": { "description": "username", "type": "string" }, "vote_count": { "description": "user vote amount", "type": "integer" } } }, "schema.GetCurrentLoginUserInfoResp": { "type": "object", "properties": { "access_token": { "description": "access token", "type": "string" }, "answer_count": { "description": "answer count", "type": "integer" }, "authority_group": { "description": "authority group", "type": "integer" }, "avatar": { "$ref": "#/definitions/schema.AvatarInfo" }, "bio": { "description": "bio markdown", "type": "string" }, "bio_html": { "description": "bio html", "type": "string" }, "color_scheme": { "description": "Color scheme", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "display_name": { "description": "display name", "type": "string" }, "e_mail": { "description": "email", "type": "string" }, "follow_count": { "description": "follow count", "type": "integer" }, "have_password": { "description": "user have password", "type": "boolean" }, "id": { "description": "user id", "type": "string" }, "language": { "description": "language", "type": "string" }, "last_login_date": { "description": "last login date", "type": "integer" }, "location": { "description": "location", "type": "string" }, "mail_status": { "description": "mail status(1 pass 2 to be verified)", "type": "integer" }, "mobile": { "description": "mobile", "type": "string" }, "notice_status": { "description": "notice status(1 on 2off)", "type": "integer" }, "question_count": { "description": "question count", "type": "integer" }, "rank": { "description": "rank", "type": "integer" }, "role_id": { "description": "role id", "type": "integer" }, "status": { "description": "user status", "type": "string" }, "suspended_until": { "description": "suspended until timestamp", "type": "integer" }, "username": { "description": "username", "type": "string" }, "visit_token": { "description": "visit token", "type": "string" }, "website": { "description": "website", "type": "string" } } }, "schema.GetFollowingTagsResp": { "type": "object", "properties": { "display_name": { "description": "display name", "type": "string" }, "main_tag_slug_name": { "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", "type": "string" }, "recommend": { "type": "boolean" }, "reserved": { "type": "boolean" }, "slug_name": { "description": "slug name", "type": "string" }, "tag_id": { "description": "tag id", "type": "string" } } }, "schema.GetObjectTimelineResp": { "type": "object", "properties": { "object_info": { "$ref": "#/definitions/schema.ActObjectInfo" }, "timeline": { "type": "array", "items": { "$ref": "#/definitions/schema.ActObjectTimeline" } } } }, "schema.GetOtherUserInfoByUsernameResp": { "type": "object", "properties": { "answer_count": { "description": "answer count", "type": "integer" }, "avatar": { "description": "avatar", "type": "string" }, "bio": { "description": "bio markdown", "type": "string" }, "bio_html": { "description": "bio html", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "display_name": { "description": "display name", "type": "string" }, "follow_count": { "description": "email\nfollow count", "type": "integer" }, "id": { "description": "user id", "type": "string" }, "last_login_date": { "description": "last login date", "type": "integer" }, "location": { "description": "location", "type": "string" }, "mobile": { "description": "mobile", "type": "string" }, "question_count": { "description": "question count", "type": "integer" }, "rank": { "description": "rank", "type": "integer" }, "status": { "type": "string" }, "status_msg": { "type": "string" }, "suspended_until": { "description": "suspended until timestamp", "type": "integer" }, "username": { "description": "username", "type": "string" }, "website": { "description": "website", "type": "string" } } }, "schema.GetOtherUserInfoResp": { "type": "object", "properties": { "info": { "$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp" } } }, "schema.GetPluginConfigResp": { "type": "object", "properties": { "config_fields": { "type": "array", "items": { "$ref": "#/definitions/schema.ConfigField" } }, "description": { "type": "string" }, "name": { "type": "string" }, "slug_name": { "type": "string" }, "version": { "type": "string" } } }, "schema.GetPluginListResp": { "type": "object", "properties": { "description": { "type": "string" }, "enabled": { "type": "boolean" }, "have_config": { "type": "boolean" }, "link": { "type": "string" }, "name": { "type": "string" }, "slug_name": { "type": "string" }, "version": { "type": "string" } } }, "schema.GetPrivilegesConfigResp": { "type": "object", "properties": { "options": { "type": "array", "items": { "$ref": "#/definitions/schema.PrivilegeOption" } }, "selected_level": { "$ref": "#/definitions/schema.PrivilegeLevel" } } }, "schema.GetRankPersonalPageResp": { "type": "object", "properties": { "answer_id": { "description": "answer id", "type": "string" }, "content": { "description": "content", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "object_id": { "description": "object id", "type": "string" }, "object_type": { "description": "object type", "type": "string", "enum": [ "question", "answer", "tag", "comment" ] }, "question_id": { "description": "question id", "type": "string" }, "rank_type": { "description": "rank type", "type": "string" }, "reputation": { "description": "reputation", "type": "integer" }, "title": { "description": "title", "type": "string" }, "url_title": { "description": "url title", "type": "string" } } }, "schema.GetReportListPageResp": { "type": "object", "properties": { "answer_accepted": { "type": "boolean" }, "answer_count": { "type": "integer" }, "answer_id": { "type": "string" }, "author_user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "comment_id": { "type": "string" }, "created_at": { "type": "integer" }, "flag_id": { "type": "string" }, "object_id": { "type": "string" }, "object_show_status": { "type": "integer" }, "object_status": { "type": "integer" }, "object_type": { "type": "string", "enum": [ "question", "answer", "comment" ] }, "original_text": { "type": "string" }, "parsed_text": { "type": "string" }, "question_id": { "type": "string" }, "reason": { "$ref": "#/definitions/schema.ReasonItem" }, "reason_content": { "type": "string" }, "submit_at": { "type": "integer" }, "submitter_user": { "$ref": "#/definitions/schema.UserBasicInfo" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "url_title": { "type": "string" } } }, "schema.GetReviewingTypeResp": { "type": "object", "properties": { "label": { "type": "string" }, "name": { "type": "string" }, "todo_amount": { "type": "integer" } } }, "schema.GetRevisionResp": { "type": "object", "properties": { "content": {}, "create_at": { "type": "integer" }, "id": { "type": "string" }, "object_id": { "type": "string" }, "reason": { "type": "string" }, "status": { "type": "integer" }, "title": { "type": "string" }, "url_title": { "type": "string" }, "use_id": { "type": "string" }, "user_info": { "$ref": "#/definitions/schema.UserBasicInfo" } } }, "schema.GetRoleResp": { "type": "object", "properties": { "description": { "type": "string" }, "id": { "type": "integer" }, "name": { "type": "string" } } }, "schema.GetSMTPConfigResp": { "type": "object", "properties": { "encryption": { "description": "\"\" SSL TLS", "type": "string" }, "from_email": { "type": "string" }, "from_name": { "type": "string" }, "smtp_authentication": { "type": "boolean" }, "smtp_host": { "type": "string" }, "smtp_password": { "type": "string" }, "smtp_port": { "type": "integer" }, "smtp_username": { "type": "string" } } }, "schema.GetSiteLegalInfoResp": { "type": "object", "properties": { "privacy_policy_original_text": { "type": "string" }, "privacy_policy_parsed_text": { "type": "string" }, "terms_of_service_original_text": { "type": "string" }, "terms_of_service_parsed_text": { "type": "string" } } }, "schema.GetTagBasicResp": { "type": "object", "properties": { "display_name": { "type": "string" }, "recommend": { "type": "boolean" }, "reserved": { "type": "boolean" }, "slug_name": { "type": "string" }, "tag_id": { "type": "string" } } }, "schema.GetTagPageResp": { "type": "object", "properties": { "created_at": { "description": "created time", "type": "integer" }, "description": { "description": "description", "type": "string" }, "display_name": { "description": "display_name", "type": "string" }, "excerpt": { "description": "excerpt", "type": "string" }, "follow_count": { "description": "follower amount", "type": "integer" }, "is_follower": { "description": "is follower", "type": "boolean" }, "original_text": { "description": "original text", "type": "string" }, "parsed_text": { "description": "parsed_text", "type": "string" }, "question_count": { "description": "question amount", "type": "integer" }, "recommend": { "type": "boolean" }, "reserved": { "type": "boolean" }, "slug_name": { "description": "slug_name", "type": "string" }, "tag_id": { "description": "tag_id", "type": "string" }, "updated_at": { "description": "updated time", "type": "integer" } } }, "schema.GetTagResp": { "type": "object", "properties": { "created_at": { "type": "integer" }, "description": { "type": "string" }, "display_name": { "type": "string" }, "excerpt": { "type": "string" }, "follow_count": { "type": "integer" }, "is_follower": { "type": "boolean" }, "main_tag_slug_name": { "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", "type": "string" }, "member_actions": { "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "original_text": { "type": "string" }, "parsed_text": { "type": "string" }, "question_count": { "type": "integer" }, "recommend": { "type": "boolean" }, "reserved": { "type": "boolean" }, "slug_name": { "type": "string" }, "status": { "type": "string" }, "tag_id": { "type": "string" }, "updated_at": { "type": "integer" } } }, "schema.GetTagSynonymsResp": { "type": "object", "properties": { "member_actions": { "description": "MemberActions", "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "synonyms": { "description": "synonyms", "type": "array", "items": { "$ref": "#/definitions/schema.TagSynonym" } } } }, "schema.GetUnreviewedPostPageResp": { "type": "object", "properties": { "answer_id": { "type": "string" }, "author_user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "comment_id": { "type": "string" }, "created_at": { "type": "integer" }, "object_id": { "type": "string" }, "object_show_status": { "type": "integer" }, "object_status": { "type": "integer" }, "object_type": { "type": "string", "enum": [ "question", "answer", "comment" ] }, "original_text": { "type": "string" }, "parsed_text": { "type": "string" }, "question_id": { "type": "string" }, "reason": { "type": "string" }, "review_id": { "type": "integer" }, "submit_at": { "type": "integer" }, "submitter_display_name": { "type": "string" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "url_title": { "type": "string" } } }, "schema.GetUnreviewedRevisionResp": { "type": "object", "properties": { "info": { "$ref": "#/definitions/schema.UnreviewedRevisionInfoInfo" }, "type": { "type": "string" }, "unreviewed_info": { "$ref": "#/definitions/schema.GetRevisionResp" } } }, "schema.GetUserActivationResp": { "type": "object", "properties": { "activation_url": { "type": "string" } } }, "schema.GetUserBadgeAwardListResp": { "type": "object", "properties": { "earned_count": { "description": "badge award count", "type": "integer" }, "icon": { "description": "badge icon", "type": "string" }, "id": { "description": "badge id", "type": "string" }, "level": { "description": "badge level", "allOf": [ { "$ref": "#/definitions/entity.BadgeLevel" } ] }, "name": { "description": "badge name", "type": "string" } } }, "schema.GetUserNotificationConfigResp": { "type": "object", "properties": { "all_new_question": { "$ref": "#/definitions/schema.NotificationChannelConfig" }, "all_new_question_for_following_tags": { "$ref": "#/definitions/schema.NotificationChannelConfig" }, "inbox": { "$ref": "#/definitions/schema.NotificationChannelConfig" } } }, "schema.GetUserPageResp": { "type": "object", "properties": { "avatar": { "description": "avatar", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "deleted_at": { "description": "delete time", "type": "integer" }, "display_name": { "description": "display name", "type": "string" }, "e_mail": { "description": "email", "type": "string" }, "rank": { "description": "rank", "type": "integer" }, "role_id": { "description": "role id", "type": "integer" }, "role_name": { "description": "role name", "type": "string" }, "status": { "description": "user status(normal,suspended,deleted,inactive)", "type": "string" }, "suspended_at": { "description": "suspended time", "type": "integer" }, "suspended_until": { "description": "suspended until time", "type": "integer" }, "user_id": { "description": "user id", "type": "string" }, "username": { "description": "username", "type": "string" } } }, "schema.GetUserPluginListResp": { "type": "object", "properties": { "name": { "type": "string" }, "slug_name": { "type": "string" } } }, "schema.GetUserStaffResp": { "type": "object", "properties": { "avatar": { "description": "avatar", "type": "string" }, "display_name": { "description": "display name", "type": "string" }, "username": { "description": "username", "type": "string" } } }, "schema.GetVoteWithPageResp": { "type": "object", "properties": { "answer_id": { "description": "answer id", "type": "string" }, "content": { "description": "content", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "object_id": { "description": "object id", "type": "string" }, "object_type": { "description": "object type", "type": "string", "enum": [ "question", "answer", "tag", "comment" ] }, "question_id": { "description": "question id", "type": "string" }, "title": { "description": "title", "type": "string" }, "url_title": { "description": "url title", "type": "string" }, "vote_type": { "description": "vote type", "type": "string" } } }, "schema.LoadingAction": { "type": "object", "properties": { "state": { "type": "string" }, "text": { "type": "string" } } }, "schema.NotificationChannelConfig": { "type": "object", "properties": { "enable": { "type": "boolean" }, "key": { "$ref": "#/definitions/constant.NotificationChannelKey" } } }, "schema.NotificationClearIDRequest": { "type": "object", "properties": { "id": { "type": "string" } } }, "schema.NotificationClearRequest": { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "inbox", "achievement" ] } } }, "schema.OnCompleteAction": { "type": "object", "properties": { "refresh_form_config": { "type": "boolean" }, "toast_return_message": { "type": "boolean" } } }, "schema.Operation": { "type": "object", "properties": { "description": { "type": "string" }, "level": { "$ref": "#/definitions/schema.OperationLevel" }, "msg": { "type": "string" }, "time": { "type": "integer" }, "type": { "type": "string" } } }, "schema.OperationLevel": { "type": "string", "enum": [ "info", "danger", "warning", "secondary" ], "x-enum-varnames": [ "OperationLevelInfo", "OperationLevelDanger", "OperationLevelWarning", "OperationLevelSecondary" ] }, "schema.OperationQuestionReq": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string" }, "operation": { "description": "operation [pin unpin hide show]", "type": "string" } } }, "schema.PermissionMemberAction": { "type": "object", "properties": { "action": { "type": "string" }, "name": { "type": "string" }, "type": { "type": "string" } } }, "schema.PostRenderReq": { "type": "object", "properties": { "content": { "type": "string" } } }, "schema.PrivilegeLevel": { "type": "integer", "enum": [ 1, 2, 3, 99 ], "x-enum-varnames": [ "PrivilegeLevel1", "PrivilegeLevel2", "PrivilegeLevel3", "PrivilegeLevelCustom" ] }, "schema.PrivilegeOption": { "type": "object", "properties": { "level": { "$ref": "#/definitions/schema.PrivilegeLevel" }, "level_desc": { "type": "string" }, "privileges": { "type": "array", "items": { "$ref": "#/definitions/constant.Privilege" } } } }, "schema.QuestionAdd": { "type": "object", "required": [ "title" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "content": { "description": "content", "type": "string", "maxLength": 65535, "minLength": 0 }, "tags": { "description": "tags", "type": "array", "items": { "$ref": "#/definitions/schema.TagItem" } }, "title": { "description": "question title", "type": "string", "maxLength": 150, "minLength": 6 } } }, "schema.QuestionAddByAnswer": { "type": "object", "required": [ "answer_content", "title" ], "properties": { "answer_content": { "type": "string", "maxLength": 65535, "minLength": 6 }, "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "content": { "description": "content", "type": "string", "maxLength": 65535, "minLength": 0 }, "mention_username_list": { "type": "array", "items": { "type": "string" } }, "tags": { "description": "tags", "type": "array", "items": { "$ref": "#/definitions/schema.TagItem" } }, "title": { "description": "question title", "type": "string", "maxLength": 150, "minLength": 6 } } }, "schema.QuestionInfoResp": { "type": "object", "properties": { "accepted_answer_id": { "type": "string" }, "answer_count": { "type": "integer" }, "answered": { "type": "boolean" }, "collected": { "type": "boolean" }, "collection_count": { "type": "integer" }, "content": { "type": "string" }, "create_time": { "type": "integer" }, "description": { "type": "string" }, "edit_time": { "type": "integer" }, "extends_actions": { "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "first_answer_id": { "type": "string" }, "follow_count": { "type": "integer" }, "html": { "type": "string" }, "id": { "type": "string" }, "is_followed": { "type": "boolean" }, "last_answer_id": { "type": "string" }, "last_answered_user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "member_actions": { "description": "MemberActions", "type": "array", "items": { "$ref": "#/definitions/schema.PermissionMemberAction" } }, "operation": { "$ref": "#/definitions/schema.Operation" }, "pin": { "type": "integer" }, "show": { "type": "integer" }, "status": { "type": "integer" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "unique_view_count": { "type": "integer" }, "update_time": { "type": "integer" }, "update_user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "url_title": { "type": "string" }, "user_info": { "$ref": "#/definitions/schema.UserBasicInfo" }, "view_count": { "type": "integer" }, "vote_count": { "type": "integer" }, "vote_status": { "type": "string" } } }, "schema.QuestionPageReq": { "type": "object", "properties": { "in_days": { "type": "integer", "minimum": 1 }, "order": { "type": "string", "enum": [ "newest", "active", "hot", "score", "unanswered", "recommend", "frequent" ] }, "page": { "type": "integer", "minimum": 1 }, "page_size": { "type": "integer", "minimum": 1 }, "tag": { "type": "string", "maxLength": 100 }, "username": { "type": "string", "maxLength": 100 } } }, "schema.QuestionPageResp": { "type": "object", "properties": { "accepted_answer_id": { "description": "answer information", "type": "string" }, "answer_count": { "type": "integer" }, "collection_count": { "type": "integer" }, "created_at": { "type": "integer" }, "description": { "type": "string" }, "follow_count": { "type": "integer" }, "id": { "type": "string" }, "last_answer_id": { "type": "string" }, "operated_at": { "description": "operator information", "type": "integer" }, "operation_type": { "type": "string" }, "operator": { "$ref": "#/definitions/schema.QuestionPageRespOperator" }, "pin": { "description": "1: unpin, 2: pin", "type": "integer" }, "show": { "description": "0: show, 1: hide", "type": "integer" }, "status": { "type": "integer" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "unique_view_count": { "type": "integer" }, "url_title": { "type": "string" }, "view_count": { "description": "question statistical information", "type": "integer" }, "vote_count": { "type": "integer" } } }, "schema.QuestionPageRespOperator": { "type": "object", "properties": { "avatar": { "type": "string" }, "display_name": { "type": "string" }, "id": { "type": "string" }, "rank": { "type": "integer" }, "status": { "type": "string" }, "username": { "type": "string" } } }, "schema.QuestionRecoverReq": { "type": "object", "required": [ "question_id" ], "properties": { "question_id": { "type": "string" } } }, "schema.QuestionUpdate": { "type": "object", "required": [ "id", "title" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "content": { "description": "content", "type": "string", "maxLength": 65535, "minLength": 0 }, "edit_summary": { "description": "edit summary", "type": "string" }, "id": { "description": "question id", "type": "string" }, "invite_user": { "type": "array", "items": { "type": "string" } }, "tags": { "description": "tags", "type": "array", "items": { "$ref": "#/definitions/schema.TagItem" } }, "title": { "description": "question title", "type": "string", "maxLength": 150, "minLength": 6 } } }, "schema.QuestionUpdateInviteUser": { "type": "object", "required": [ "id" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "id": { "type": "string" }, "invite_user": { "type": "array", "items": { "type": "string" } } } }, "schema.ReactionRespItem": { "type": "object", "properties": { "count": { "description": "Count is the number of users who reacted", "type": "integer" }, "emoji": { "description": "Emoji is the reaction emoji", "type": "string" }, "is_active": { "description": "IsActive is if current user has reacted", "type": "boolean" }, "tooltip": { "description": "Tooltip is the user's name who reacted", "type": "string" } } }, "schema.ReasonItem": { "type": "object", "properties": { "content_type": { "type": "string" }, "description": { "type": "string" }, "name": { "type": "string" }, "placeholder": { "type": "string" }, "reason_key": { "type": "string" }, "reason_type": { "type": "integer" } } }, "schema.RecoverAnswerReq": { "type": "object", "required": [ "answer_id" ], "properties": { "answer_id": { "type": "string" } } }, "schema.RecoverTagReq": { "type": "object", "required": [ "tag_id" ], "properties": { "tag_id": { "type": "string" } } }, "schema.RemoveAnswerReq": { "type": "object", "required": [ "id" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "id": { "type": "string" } } }, "schema.RemoveCommentReq": { "type": "object", "required": [ "comment_id" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "comment_id": { "description": "comment id", "type": "string" } } }, "schema.RemoveQuestionReq": { "type": "object", "required": [ "id" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "captcha_id", "type": "string" }, "id": { "description": "question id", "type": "string" } } }, "schema.RemoveTagReq": { "type": "object", "required": [ "tag_id" ], "properties": { "tag_id": { "description": "tag_id", "type": "string" } } }, "schema.ReopenQuestionReq": { "type": "object", "properties": { "question_id": { "type": "string" } } }, "schema.ReviewReportReq": { "type": "object", "required": [ "flag_id", "operation_type" ], "properties": { "close_msg": { "type": "string" }, "close_type": { "type": "integer" }, "content": { "type": "string", "maxLength": 65535, "minLength": 6 }, "flag_id": { "type": "string" }, "operation_type": { "type": "string", "enum": [ "edit_post", "close_post", "delete_post", "unlist_post", "ignore_report" ] }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagItem" } }, "title": { "type": "string", "maxLength": 150, "minLength": 6 } } }, "schema.RevisionAuditReq": { "type": "object", "required": [ "id", "operation" ], "properties": { "id": { "description": "object id", "type": "string" }, "operation": { "description": "approve or reject", "type": "string" } } }, "schema.SearchObject": { "type": "object", "properties": { "accepted": { "type": "boolean" }, "answer_count": { "type": "integer" }, "created_at": { "type": "integer" }, "excerpt": { "type": "string" }, "id": { "type": "string" }, "question_id": { "type": "string" }, "status": { "description": "Status", "type": "string" }, "tags": { "description": "tags", "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "url_title": { "type": "string" }, "user_info": { "description": "user info", "allOf": [ { "$ref": "#/definitions/schema.SearchObjectUser" } ] }, "vote_count": { "type": "integer" } } }, "schema.SearchObjectUser": { "type": "object", "properties": { "display_name": { "type": "string" }, "id": { "type": "string" }, "rank": { "type": "integer" }, "status": { "type": "string" }, "username": { "type": "string" } } }, "schema.SearchResp": { "type": "object", "properties": { "count": { "type": "integer" }, "list": { "description": "search response", "type": "array", "items": { "$ref": "#/definitions/schema.SearchResult" } } } }, "schema.SearchResult": { "type": "object", "properties": { "object": { "description": "this object", "allOf": [ { "$ref": "#/definitions/schema.SearchObject" } ] }, "object_type": { "description": "object_type", "type": "string" } } }, "schema.SendUserActivationReq": { "type": "object", "required": [ "user_id" ], "properties": { "user_id": { "type": "string" } } }, "schema.SiteAIProvider": { "type": "object", "properties": { "api_host": { "type": "string", "maxLength": 512 }, "api_key": { "type": "string", "maxLength": 256 }, "model": { "type": "string", "maxLength": 100 }, "provider": { "type": "string", "maxLength": 50 } } }, "schema.SiteAIReq": { "type": "object", "properties": { "ai_providers": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteAIProvider" } }, "chosen_provider": { "type": "string", "maxLength": 50 }, "enabled": { "type": "boolean" }, "prompt_config": { "$ref": "#/definitions/schema.AIPromptConfig" } } }, "schema.SiteAIResp": { "type": "object", "properties": { "ai_providers": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteAIProvider" } }, "chosen_provider": { "type": "string", "maxLength": 50 }, "enabled": { "type": "boolean" }, "prompt_config": { "$ref": "#/definitions/schema.AIPromptConfig" } } }, "schema.SiteAdvancedReq": { "type": "object", "properties": { "authorized_attachment_extensions": { "type": "array", "items": { "type": "string" } }, "authorized_image_extensions": { "type": "array", "items": { "type": "string" } }, "max_attachment_size": { "type": "integer" }, "max_image_megapixel": { "type": "integer" }, "max_image_size": { "type": "integer" } } }, "schema.SiteAdvancedResp": { "type": "object", "properties": { "authorized_attachment_extensions": { "type": "array", "items": { "type": "string" } }, "authorized_image_extensions": { "type": "array", "items": { "type": "string" } }, "max_attachment_size": { "type": "integer" }, "max_image_megapixel": { "type": "integer" }, "max_image_size": { "type": "integer" } } }, "schema.SiteBrandingReq": { "type": "object", "properties": { "favicon": { "type": "string", "maxLength": 512 }, "logo": { "type": "string", "maxLength": 512 }, "mobile_logo": { "type": "string", "maxLength": 512 }, "square_icon": { "type": "string", "maxLength": 512 } } }, "schema.SiteBrandingResp": { "type": "object", "properties": { "favicon": { "type": "string", "maxLength": 512 }, "logo": { "type": "string", "maxLength": 512 }, "mobile_logo": { "type": "string", "maxLength": 512 }, "square_icon": { "type": "string", "maxLength": 512 } } }, "schema.SiteCustomCssHTMLReq": { "type": "object", "properties": { "custom_css": { "type": "string", "maxLength": 65536 }, "custom_footer": { "type": "string", "maxLength": 65536 }, "custom_head": { "type": "string", "maxLength": 65536 }, "custom_header": { "type": "string", "maxLength": 65536 }, "custom_sidebar": { "type": "string", "maxLength": 65536 } } }, "schema.SiteCustomCssHTMLResp": { "type": "object", "properties": { "custom_css": { "type": "string", "maxLength": 65536 }, "custom_footer": { "type": "string", "maxLength": 65536 }, "custom_head": { "type": "string", "maxLength": 65536 }, "custom_header": { "type": "string", "maxLength": 65536 }, "custom_sidebar": { "type": "string", "maxLength": 65536 } } }, "schema.SiteGeneralReq": { "type": "object", "required": [ "contact_email", "name", "site_url" ], "properties": { "contact_email": { "type": "string", "maxLength": 512 }, "description": { "type": "string", "maxLength": 2000 }, "name": { "type": "string", "maxLength": 128 }, "short_description": { "type": "string", "maxLength": 255 }, "site_url": { "type": "string", "maxLength": 512 } } }, "schema.SiteGeneralResp": { "type": "object", "required": [ "contact_email", "name", "site_url" ], "properties": { "contact_email": { "type": "string", "maxLength": 512 }, "description": { "type": "string", "maxLength": 2000 }, "name": { "type": "string", "maxLength": 128 }, "short_description": { "type": "string", "maxLength": 255 }, "site_url": { "type": "string", "maxLength": 512 } } }, "schema.SiteInfoResp": { "type": "object", "properties": { "ai_enabled": { "type": "boolean" }, "branding": { "$ref": "#/definitions/schema.SiteBrandingResp" }, "custom_css_html": { "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" }, "general": { "$ref": "#/definitions/schema.SiteGeneralResp" }, "interface": { "$ref": "#/definitions/schema.SiteInterfaceSettingsResp" }, "login": { "$ref": "#/definitions/schema.SiteLoginResp" }, "mcp_enabled": { "type": "boolean" }, "revision": { "type": "string" }, "site_advanced": { "$ref": "#/definitions/schema.SiteAdvancedResp" }, "site_legal": { "$ref": "#/definitions/schema.SiteLegalSimpleResp" }, "site_questions": { "$ref": "#/definitions/schema.SiteQuestionsResp" }, "site_security": { "$ref": "#/definitions/schema.SiteSecurityResp" }, "site_seo": { "$ref": "#/definitions/schema.SiteSeoResp" }, "site_tags": { "$ref": "#/definitions/schema.SiteTagsResp" }, "site_users": { "$ref": "#/definitions/schema.SiteUsersResp" }, "theme": { "$ref": "#/definitions/schema.SiteThemeResp" }, "users_settings": { "$ref": "#/definitions/schema.SiteUsersSettingsResp" }, "version": { "type": "string" } } }, "schema.SiteInterfaceReq": { "type": "object", "required": [ "language", "time_zone" ], "properties": { "language": { "type": "string", "maxLength": 128 }, "time_zone": { "type": "string", "maxLength": 128 } } }, "schema.SiteInterfaceSettingsResp": { "type": "object", "required": [ "language", "time_zone" ], "properties": { "language": { "type": "string", "maxLength": 128 }, "time_zone": { "type": "string", "maxLength": 128 } } }, "schema.SiteLegalSimpleResp": { "type": "object", "required": [ "external_content_display" ], "properties": { "external_content_display": { "type": "string", "enum": [ "always_display", "ask_before_display" ] } } }, "schema.SiteLoginReq": { "type": "object", "properties": { "allow_email_domains": { "type": "array", "items": { "type": "string" } }, "allow_email_registrations": { "type": "boolean" }, "allow_new_registrations": { "type": "boolean" }, "allow_password_login": { "type": "boolean" } } }, "schema.SiteLoginResp": { "type": "object", "properties": { "allow_email_domains": { "type": "array", "items": { "type": "string" } }, "allow_email_registrations": { "type": "boolean" }, "allow_new_registrations": { "type": "boolean" }, "allow_password_login": { "type": "boolean" } } }, "schema.SiteMCPReq": { "type": "object", "properties": { "enabled": { "type": "boolean" } } }, "schema.SiteMCPResp": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "http_header": { "type": "string" }, "type": { "type": "string" }, "url": { "type": "string" } } }, "schema.SitePoliciesReq": { "type": "object", "properties": { "privacy_policy_original_text": { "type": "string" }, "privacy_policy_parsed_text": { "type": "string" }, "terms_of_service_original_text": { "type": "string" }, "terms_of_service_parsed_text": { "type": "string" } } }, "schema.SitePoliciesResp": { "type": "object", "properties": { "privacy_policy_original_text": { "type": "string" }, "privacy_policy_parsed_text": { "type": "string" }, "terms_of_service_original_text": { "type": "string" }, "terms_of_service_parsed_text": { "type": "string" } } }, "schema.SiteQuestionsReq": { "type": "object", "properties": { "min_content": { "type": "integer", "maximum": 65535, "minimum": 0 }, "min_tags": { "type": "integer", "maximum": 5, "minimum": 0 }, "restrict_answer": { "type": "boolean" } } }, "schema.SiteQuestionsResp": { "type": "object", "properties": { "min_content": { "type": "integer", "maximum": 65535, "minimum": 0 }, "min_tags": { "type": "integer", "maximum": 5, "minimum": 0 }, "restrict_answer": { "type": "boolean" } } }, "schema.SiteSecurityReq": { "type": "object", "required": [ "external_content_display" ], "properties": { "check_update": { "type": "boolean" }, "external_content_display": { "type": "string", "enum": [ "always_display", "ask_before_display" ] }, "login_required": { "type": "boolean" } } }, "schema.SiteSecurityResp": { "type": "object", "required": [ "external_content_display" ], "properties": { "check_update": { "type": "boolean" }, "external_content_display": { "type": "string", "enum": [ "always_display", "ask_before_display" ] }, "login_required": { "type": "boolean" } } }, "schema.SiteSeoReq": { "type": "object", "required": [ "permalink", "robots" ], "properties": { "permalink": { "type": "integer", "maximum": 4, "minimum": 0 }, "robots": { "type": "string" } } }, "schema.SiteSeoResp": { "type": "object", "required": [ "permalink", "robots" ], "properties": { "permalink": { "type": "integer", "maximum": 4, "minimum": 0 }, "robots": { "type": "string" } } }, "schema.SiteTagsReq": { "type": "object", "properties": { "recommend_tags": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { "type": "boolean" }, "reserved_tags": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteWriteTag" } } } }, "schema.SiteTagsResp": { "type": "object", "properties": { "recommend_tags": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { "type": "boolean" }, "reserved_tags": { "type": "array", "items": { "$ref": "#/definitions/schema.SiteWriteTag" } } } }, "schema.SiteThemeReq": { "type": "object", "required": [ "theme" ], "properties": { "color_scheme": { "type": "string", "maxLength": 100 }, "layout": { "type": "string", "enum": [ "Full-width", "Fixed-width" ] }, "theme": { "type": "string", "maxLength": 255 }, "theme_config": { "type": "object", "additionalProperties": {} } } }, "schema.SiteThemeResp": { "type": "object", "properties": { "color_scheme": { "type": "string" }, "layout": { "type": "string" }, "theme": { "type": "string" }, "theme_config": { "type": "object", "additionalProperties": {} }, "theme_options": { "type": "array", "items": { "$ref": "#/definitions/schema.ThemeOption" } } } }, "schema.SiteUsersReq": { "type": "object", "required": [ "default_avatar" ], "properties": { "allow_update_avatar": { "type": "boolean" }, "allow_update_bio": { "type": "boolean" }, "allow_update_display_name": { "type": "boolean" }, "allow_update_location": { "type": "boolean" }, "allow_update_username": { "type": "boolean" }, "allow_update_website": { "type": "boolean" }, "default_avatar": { "type": "string", "enum": [ "system", "gravatar" ] }, "gravatar_base_url": { "type": "string" } } }, "schema.SiteUsersResp": { "type": "object", "required": [ "default_avatar" ], "properties": { "allow_update_avatar": { "type": "boolean" }, "allow_update_bio": { "type": "boolean" }, "allow_update_display_name": { "type": "boolean" }, "allow_update_location": { "type": "boolean" }, "allow_update_username": { "type": "boolean" }, "allow_update_website": { "type": "boolean" }, "default_avatar": { "type": "string", "enum": [ "system", "gravatar" ] }, "gravatar_base_url": { "type": "string" } } }, "schema.SiteUsersSettingsReq": { "type": "object", "required": [ "default_avatar" ], "properties": { "default_avatar": { "type": "string", "enum": [ "system", "gravatar" ] }, "gravatar_base_url": { "type": "string" } } }, "schema.SiteUsersSettingsResp": { "type": "object", "required": [ "default_avatar" ], "properties": { "default_avatar": { "type": "string", "enum": [ "system", "gravatar" ] }, "gravatar_base_url": { "type": "string" } } }, "schema.SiteWriteTag": { "type": "object", "required": [ "slug_name" ], "properties": { "display_name": { "type": "string" }, "slug_name": { "type": "string" } } }, "schema.TagItem": { "type": "object", "properties": { "display_name": { "description": "display_name", "type": "string", "maxLength": 35 }, "original_text": { "description": "original text", "type": "string" }, "slug_name": { "description": "slug_name", "type": "string", "maxLength": 35 } } }, "schema.TagResp": { "type": "object", "properties": { "display_name": { "type": "string" }, "main_tag_slug_name": { "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", "type": "string" }, "recommend": { "type": "boolean" }, "reserved": { "type": "boolean" }, "slug_name": { "type": "string" } } }, "schema.TagSynonym": { "type": "object", "properties": { "display_name": { "description": "display name", "type": "string" }, "main_tag_slug_name": { "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", "type": "string" }, "slug_name": { "description": "slug name", "type": "string" }, "tag_id": { "description": "tag id", "type": "string" } } }, "schema.ThemeOption": { "type": "object", "properties": { "label": { "type": "string" }, "value": { "type": "string" } } }, "schema.UIOptionAction": { "type": "object", "properties": { "loading": { "$ref": "#/definitions/schema.LoadingAction" }, "method": { "type": "string" }, "on_complete": { "$ref": "#/definitions/schema.OnCompleteAction" }, "url": { "type": "string" } } }, "schema.UnreviewedRevisionInfoInfo": { "type": "object", "properties": { "answer_accepted": { "type": "boolean" }, "answer_count": { "type": "integer" }, "answer_id": { "type": "string" }, "comment_id": { "type": "string" }, "content": { "type": "string" }, "created_at": { "type": "integer" }, "html": { "type": "string" }, "object_creator_user_id": { "type": "string" }, "object_id": { "type": "string" }, "object_type": { "type": "string" }, "question_id": { "type": "string" }, "show_status": { "type": "integer" }, "status": { "type": "integer" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/schema.TagResp" } }, "title": { "type": "string" }, "url_title": { "type": "string" } } }, "schema.UpdateAPIKeyReq": { "type": "object", "required": [ "description", "id" ], "properties": { "description": { "type": "string", "maxLength": 150 }, "id": { "type": "integer" } } }, "schema.UpdateBadgeStatusReq": { "type": "object", "required": [ "id", "status" ], "properties": { "id": { "description": "badge id", "type": "string" }, "status": { "description": "badge status", "allOf": [ { "$ref": "#/definitions/schema.BadgeStatus" } ] } } }, "schema.UpdateCommentReq": { "type": "object", "required": [ "comment_id", "original_text" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "description": "whether user can delete it", "type": "string" }, "comment_id": { "description": "comment id", "type": "string" }, "original_text": { "description": "original comment content", "type": "string", "maxLength": 600, "minLength": 2 } } }, "schema.UpdateFollowTagsReq": { "type": "object", "properties": { "slug_name_list": { "description": "tag slug name list", "type": "array", "items": { "type": "string" } } } }, "schema.UpdateInfoRequest": { "type": "object", "properties": { "avatar": { "$ref": "#/definitions/schema.AvatarInfo" }, "bio": { "type": "string", "maxLength": 4096 }, "display_name": { "type": "string", "maxLength": 30, "minLength": 2 }, "location": { "type": "string", "maxLength": 100 }, "username": { "type": "string", "maxLength": 30, "minLength": 2 }, "website": { "type": "string", "maxLength": 500 } } }, "schema.UpdatePluginConfigReq": { "type": "object", "required": [ "plugin_slug_name" ], "properties": { "config_fields": { "type": "object", "additionalProperties": {} }, "plugin_slug_name": { "type": "string", "maxLength": 100 } } }, "schema.UpdatePluginStatusReq": { "type": "object", "required": [ "plugin_slug_name" ], "properties": { "enabled": { "type": "boolean" }, "plugin_slug_name": { "type": "string", "maxLength": 100 } } }, "schema.UpdatePrivilegesConfigReq": { "type": "object", "required": [ "level" ], "properties": { "custom_privileges": { "type": "array", "items": { "$ref": "#/definitions/constant.Privilege" } }, "level": { "minimum": 1, "allOf": [ { "$ref": "#/definitions/schema.PrivilegeLevel" } ] } } }, "schema.UpdateReactionReq": { "type": "object", "required": [ "emoji", "object_id", "reaction" ], "properties": { "emoji": { "type": "string", "enum": [ "heart", "smile", "frown" ] }, "object_id": { "type": "string" }, "reaction": { "type": "string", "enum": [ "activate", "deactivate" ] } } }, "schema.UpdateReviewReq": { "type": "object", "required": [ "review_id", "status" ], "properties": { "review_id": { "type": "integer" }, "status": { "type": "string", "enum": [ "approve", "reject" ] } } }, "schema.UpdateSMTPConfigReq": { "type": "object", "properties": { "encryption": { "description": "\"\" SSL TLS", "type": "string", "enum": [ "SSL", "TLS" ] }, "from_email": { "type": "string", "maxLength": 256 }, "from_name": { "type": "string", "maxLength": 256 }, "smtp_authentication": { "type": "boolean" }, "smtp_host": { "type": "string", "maxLength": 256 }, "smtp_password": { "type": "string", "maxLength": 256 }, "smtp_port": { "type": "integer", "maximum": 65535, "minimum": 1 }, "smtp_username": { "type": "string", "maxLength": 256 }, "test_email_recipient": { "type": "string" } } }, "schema.UpdateTagReq": { "type": "object", "required": [ "tag_id" ], "properties": { "display_name": { "description": "display_name", "type": "string", "maxLength": 35 }, "edit_summary": { "description": "edit summary", "type": "string" }, "original_text": { "description": "original text", "type": "string" }, "slug_name": { "description": "slug_name", "type": "string", "maxLength": 35 }, "tag_id": { "description": "tag_id", "type": "string" } } }, "schema.UpdateTagSynonymReq": { "type": "object", "required": [ "synonym_tag_list", "tag_id" ], "properties": { "synonym_tag_list": { "description": "synonym tag list", "type": "array", "items": { "$ref": "#/definitions/schema.TagItem" } }, "tag_id": { "description": "tag_id", "type": "string" } } }, "schema.UpdateUserInterfaceRequest": { "type": "object", "required": [ "color_scheme", "language" ], "properties": { "color_scheme": { "description": "Color scheme", "type": "string", "maxLength": 100 }, "language": { "description": "language", "type": "string", "maxLength": 100 } } }, "schema.UpdateUserNotificationConfigReq": { "type": "object", "properties": { "all_new_question": { "$ref": "#/definitions/schema.NotificationChannelConfig" }, "all_new_question_for_following_tags": { "$ref": "#/definitions/schema.NotificationChannelConfig" }, "inbox": { "$ref": "#/definitions/schema.NotificationChannelConfig" } } }, "schema.UpdateUserPasswordReq": { "type": "object", "required": [ "password", "user_id" ], "properties": { "password": { "type": "string", "maxLength": 32, "minLength": 8 }, "user_id": { "type": "string" } } }, "schema.UpdateUserPluginConfigReq": { "type": "object", "required": [ "plugin_slug_name" ], "properties": { "config_fields": { "type": "object", "additionalProperties": {} }, "plugin_slug_name": { "type": "string", "maxLength": 100 } } }, "schema.UpdateUserRoleReq": { "type": "object", "required": [ "role_id", "user_id" ], "properties": { "role_id": { "description": "role id", "type": "integer" }, "user_id": { "description": "user id", "type": "string" } } }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ "status", "user_id" ], "properties": { "remove_all_content": { "type": "boolean" }, "status": { "type": "string", "enum": [ "normal", "suspended", "deleted", "inactive" ] }, "suspend_duration": { "type": "string", "enum": [ "24h", "48h", "72h", "7d", "14d", "1m", "2m", "3m", "6m", "1y", "forever" ] }, "user_id": { "type": "string" } } }, "schema.UserBasicInfo": { "type": "object", "properties": { "avatar": { "type": "string" }, "display_name": { "type": "string" }, "id": { "type": "string" }, "language": { "type": "string" }, "location": { "type": "string" }, "rank": { "type": "integer" }, "status": { "type": "string" }, "suspended_until": { "type": "integer" }, "username": { "type": "string" }, "website": { "type": "string" } } }, "schema.UserChangeEmailSendCodeReq": { "type": "object", "required": [ "e_mail" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "e_mail": { "type": "string", "maxLength": 500 }, "pass": { "type": "string", "maxLength": 32, "minLength": 8 } } }, "schema.UserChangeEmailVerifyReq": { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "maxLength": 500 } } }, "schema.UserEmailLoginReq": { "type": "object", "required": [ "e_mail", "pass" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "e_mail": { "type": "string", "maxLength": 500 }, "pass": { "type": "string", "maxLength": 32, "minLength": 8 } } }, "schema.UserLoginResp": { "type": "object", "properties": { "access_token": { "description": "access token", "type": "string" }, "answer_count": { "description": "answer count", "type": "integer" }, "authority_group": { "description": "authority group", "type": "integer" }, "avatar": { "description": "avatar", "type": "string" }, "bio": { "description": "bio markdown", "type": "string" }, "bio_html": { "description": "bio html", "type": "string" }, "color_scheme": { "description": "Color scheme", "type": "string" }, "created_at": { "description": "create time", "type": "integer" }, "display_name": { "description": "display name", "type": "string" }, "e_mail": { "description": "email", "type": "string" }, "follow_count": { "description": "follow count", "type": "integer" }, "have_password": { "description": "user have password", "type": "boolean" }, "id": { "description": "user id", "type": "string" }, "language": { "description": "language", "type": "string" }, "last_login_date": { "description": "last login date", "type": "integer" }, "location": { "description": "location", "type": "string" }, "mail_status": { "description": "mail status(1 pass 2 to be verified)", "type": "integer" }, "mobile": { "description": "mobile", "type": "string" }, "notice_status": { "description": "notice status(1 on 2off)", "type": "integer" }, "question_count": { "description": "question count", "type": "integer" }, "rank": { "description": "rank", "type": "integer" }, "role_id": { "description": "role id", "type": "integer" }, "status": { "description": "user status", "type": "string" }, "suspended_until": { "description": "suspended until timestamp", "type": "integer" }, "username": { "description": "username", "type": "string" }, "visit_token": { "description": "visit token", "type": "string" }, "website": { "description": "website", "type": "string" } } }, "schema.UserModifyPasswordReq": { "type": "object", "required": [ "pass" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "old_pass": { "type": "string", "maxLength": 32, "minLength": 8 }, "pass": { "type": "string", "maxLength": 32, "minLength": 8 } } }, "schema.UserRankingResp": { "type": "object", "properties": { "staffs": { "type": "array", "items": { "$ref": "#/definitions/schema.UserRankingSimpleInfo" } }, "users_with_the_most_reputation": { "type": "array", "items": { "$ref": "#/definitions/schema.UserRankingSimpleInfo" } }, "users_with_the_most_vote": { "type": "array", "items": { "$ref": "#/definitions/schema.UserRankingSimpleInfo" } } } }, "schema.UserRankingSimpleInfo": { "type": "object", "properties": { "avatar": { "description": "avatar", "type": "string" }, "display_name": { "description": "display name", "type": "string" }, "rank": { "description": "rank", "type": "integer" }, "username": { "description": "username", "type": "string" }, "vote_count": { "description": "vote", "type": "integer" } } }, "schema.UserRePassWordRequest": { "type": "object", "required": [ "code", "pass" ], "properties": { "code": { "type": "string", "maxLength": 100 }, "pass": { "type": "string", "maxLength": 32 } } }, "schema.UserRegisterReq": { "type": "object", "required": [ "e_mail", "name", "pass" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "e_mail": { "type": "string", "maxLength": 500 }, "name": { "type": "string", "maxLength": 30, "minLength": 2 }, "pass": { "type": "string", "maxLength": 32, "minLength": 8 } } }, "schema.UserRetrievePassWordRequest": { "type": "object", "required": [ "e_mail" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "e_mail": { "type": "string", "maxLength": 500 } } }, "schema.UserUnsubscribeNotificationReq": { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "maxLength": 500 } } }, "schema.VoteReq": { "type": "object", "required": [ "object_id" ], "properties": { "captcha_code": { "type": "string" }, "captcha_id": { "type": "string" }, "is_cancel": { "type": "boolean" }, "object_id": { "type": "string" } } }, "schema.VoteResp": { "type": "object", "properties": { "down_votes": { "type": "integer" }, "up_votes": { "type": "integer" }, "vote_status": { "type": "string" }, "votes": { "type": "integer" } } }, "translator.LangOption": { "type": "object", "properties": { "label": { "type": "string" }, "progress": { "description": "Translation completion percentage", "type": "integer" }, "value": { "type": "string" } } } }, "securityDefinitions": { "ApiKeyAuth": { "type": "apiKey", "name": "Authorization", "in": "header" } } } ================================================ FILE: docs/swagger.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. basePath: / definitions: constant.NotificationChannelKey: enum: - email type: string x-enum-varnames: - EmailChannel constant.Privilege: properties: key: type: string label: type: string value: minimum: 1 type: integer type: object entity.BadgeLevel: enum: - 1 - 2 - 3 type: integer x-enum-varnames: - BadgeLevelBronze - BadgeLevelSilver - BadgeLevelGold handler.RespBody: properties: code: description: http code type: integer data: description: response data msg: description: response message type: string reason: description: reason key type: string type: object install.CheckConfigFileResp: properties: config_file_exist: type: boolean db_connection_success: type: boolean db_table_exist: type: boolean type: object install.CheckDatabaseReq: properties: db_file: type: string db_host: type: string db_name: type: string db_password: type: string db_type: enum: - postgres - sqlite3 - mysql type: string db_username: type: string ssl_cert: type: string ssl_enabled: type: boolean ssl_key: type: string ssl_mode: type: string ssl_root_cert: type: string required: - db_type type: object install.InitBaseInfoReq: properties: contact_email: maxLength: 500 type: string email: maxLength: 500 type: string external_content_display: enum: - always_display - ask_before_display type: string lang: maxLength: 30 type: string login_required: type: boolean name: maxLength: 30 minLength: 2 type: string password: maxLength: 32 minLength: 8 type: string site_name: maxLength: 30 type: string site_url: maxLength: 512 type: string required: - contact_email - email - external_content_display - lang - name - password - site_name - site_url type: object pager.PageModel: properties: count: type: integer list: {} type: object plugin.EmbedConfig: properties: enable: type: boolean platform: type: string type: object plugin.RenderConfig: properties: select_theme: type: string type: object schema.AIConversationAdminDeleteReq: properties: conversation_id: type: string required: - conversation_id type: object schema.AIConversationAdminDetailResp: properties: conversation_id: type: string created_at: type: integer records: items: $ref: '#/definitions/schema.AIConversationRecord' type: array topic: type: string user_info: $ref: '#/definitions/schema.AIConversationUserInfo' type: object schema.AIConversationAdminListItem: properties: created_at: type: integer helpful_count: type: integer id: type: string topic: type: string unhelpful_count: type: integer user_info: $ref: '#/definitions/schema.AIConversationUserInfo' type: object schema.AIConversationDetailResp: properties: conversation_id: type: string created_at: type: integer records: items: $ref: '#/definitions/schema.AIConversationRecord' type: array topic: type: string updated_at: type: integer type: object schema.AIConversationListItem: properties: conversation_id: type: string created_at: type: integer topic: type: string type: object schema.AIConversationRecord: properties: chat_completion_id: type: string content: type: string created_at: type: integer helpful: type: integer role: type: string unhelpful: type: integer type: object schema.AIConversationUserInfo: properties: avatar: type: string display_name: type: string id: type: string rank: type: integer username: type: string type: object schema.AIConversationVoteReq: properties: cancel: type: boolean chat_completion_id: type: string vote_type: enum: - helpful - unhelpful type: string required: - chat_completion_id - vote_type type: object schema.AIPromptConfig: properties: en_us: type: string zh_cn: type: string type: object schema.AcceptAnswerReq: properties: answer_id: type: string question_id: maxLength: 30 type: string required: - question_id type: object schema.ActObjectInfo: properties: answer_id: type: string display_name: type: string main_tag_slug_name: type: string object_type: type: string question_id: type: string title: type: string username: type: string type: object schema.ActObjectTimeline: properties: activity_id: type: string activity_type: type: string cancelled: type: boolean cancelled_at: type: integer comment: type: string created_at: type: integer object_id: type: string object_type: type: string revision_id: type: string user_info: $ref: '#/definitions/schema.UserBasicInfo' type: object schema.ActionRecordResp: properties: captcha_id: type: string captcha_img: type: string verify: type: boolean type: object schema.AddAPIKeyReq: properties: description: maxLength: 150 type: string scope: enum: - read-only - global type: string required: - description - scope type: object schema.AddAPIKeyResp: properties: access_key: type: string type: object schema.AddCommentReq: properties: captcha_code: type: string captcha_id: type: string mention_username_list: description: '@ user id list' items: type: string type: array object_id: description: object id type: string original_text: description: original comment content maxLength: 600 minLength: 2 type: string reply_comment_id: description: reply comment id type: string required: - object_id - original_text type: object schema.AddReportReq: properties: captcha_code: type: string captcha_id: description: captcha_id type: string content: description: report content maxLength: 500 type: string object_id: description: object id maxLength: 20 type: string report_type: description: report type type: integer required: - object_id - report_type type: object schema.AddTagReq: properties: display_name: description: display_name maxLength: 35 type: string original_text: description: original text maxLength: 65536 type: string slug_name: description: slug_name maxLength: 35 type: string required: - display_name - original_text - slug_name type: object schema.AddUserReq: properties: display_name: maxLength: 30 minLength: 2 type: string email: maxLength: 500 type: string password: maxLength: 32 minLength: 8 type: string required: - display_name - email - password type: object schema.AddUsersReq: properties: users: description: users info line by line type: string type: object schema.AdminUpdateAnswerStatusReq: properties: answer_id: type: string status: enum: - available - deleted type: string required: - answer_id - status type: object schema.AdminUpdateQuestionStatusReq: properties: question_id: type: string status: enum: - available - closed - deleted type: string required: - question_id - status type: object schema.AnswerAddReq: properties: captcha_code: type: string captcha_id: type: string content: maxLength: 65535 minLength: 6 type: string question_id: type: string required: - content type: object schema.AnswerInfo: properties: accepted: type: integer collected: type: boolean content: type: string create_time: type: integer html: type: string id: type: string member_actions: description: MemberActions items: $ref: '#/definitions/schema.PermissionMemberAction' type: array question_id: type: string question_info: $ref: '#/definitions/schema.QuestionInfoResp' status: type: integer update_time: type: integer update_user_info: $ref: '#/definitions/schema.UserBasicInfo' user_info: $ref: '#/definitions/schema.UserBasicInfo' vote_count: type: integer vote_status: type: string type: object schema.AnswerUpdateReq: properties: captcha_code: type: string captcha_id: type: string content: maxLength: 65535 minLength: 6 type: string edit_summary: type: string id: type: string title: type: string required: - content type: object schema.AvatarInfo: properties: custom: maxLength: 200 type: string gravatar: maxLength: 200 type: string type: maxLength: 100 type: string type: object schema.BadgeListInfo: properties: award_count: description: badge award count type: integer earned_count: description: badge earned count type: integer icon: description: badge icon type: string id: description: badge id type: string level: allOf: - $ref: '#/definitions/entity.BadgeLevel' description: badge level name: description: badge name type: string type: object schema.BadgeStatus: enum: - active - inactive type: string x-enum-varnames: - BadgeStatusActive - BadgeStatusInactive schema.CloseQuestionReq: properties: close_msg: description: close_type type: string close_type: description: close_type type: integer id: type: string required: - id type: object schema.CollectionSwitchReq: properties: bookmark: type: boolean group_id: type: string object_id: type: string required: - group_id - object_id type: object schema.CollectionSwitchResp: properties: object_collection_count: type: integer type: object schema.ConfigField: properties: description: type: string name: type: string options: items: $ref: '#/definitions/schema.ConfigFieldOption' type: array required: type: boolean title: type: string type: type: string ui_options: $ref: '#/definitions/schema.ConfigFieldUIOptions' value: {} type: object schema.ConfigFieldOption: properties: label: type: string value: type: string type: object schema.ConfigFieldUIOptions: properties: action: $ref: '#/definitions/schema.UIOptionAction' class_name: type: string field_class_name: type: string input_type: type: string label: type: string placeholder: type: string rows: type: string text: type: string variant: type: string type: object schema.ConnectorInfoResp: properties: icon: type: string link: type: string name: type: string type: object schema.ConnectorUserInfoResp: properties: binding: type: boolean external_id: type: string icon: type: string link: type: string name: type: string type: object schema.DeleteAPIKeyReq: properties: id: type: integer type: object schema.DeletePermanentlyReq: properties: type: enum: - users - questions - answers type: string required: - type type: object schema.EditUserProfileReq: properties: display_name: maxLength: 30 minLength: 2 type: string email: maxLength: 500 type: string user_id: type: string username: maxLength: 30 minLength: 2 type: string required: - display_name - email - user_id type: object schema.ExternalLoginBindingUserSendEmailReq: properties: binding_key: maxLength: 100 type: string email: maxLength: 512 type: string must: description: |- If must is true, whatever email if exists, try to bind user. If must is false, when email exist, will only be prompted with a warning. type: boolean required: - binding_key - email type: object schema.ExternalLoginBindingUserSendEmailResp: properties: access_token: type: string email_exist_and_must_be_confirmed: type: boolean type: object schema.ExternalLoginUnbindingReq: properties: external_id: maxLength: 128 type: string required: - external_id type: object schema.FollowReq: properties: is_cancel: description: is cancel type: boolean object_id: description: object id type: string required: - object_id type: object schema.FollowResp: properties: follows: description: the followers of object type: integer is_followed: description: if user is followed object will be true,otherwise false type: boolean type: object schema.GetAIModelResp: properties: created: type: integer id: type: string object: type: string owned_by: type: string type: object schema.GetAIProviderResp: properties: default_api_host: type: string display_name: type: string name: type: string type: object schema.GetAPIKeyResp: properties: access_key: type: string created_at: type: integer description: type: string id: type: integer last_used_at: type: integer scope: type: string type: object schema.GetAnswerInfoResp: properties: info: $ref: '#/definitions/schema.AnswerInfo' question: $ref: '#/definitions/schema.QuestionInfoResp' type: object schema.GetBadgeInfoResp: properties: award_count: description: badge award count type: integer description: description: badge description type: string earned_count: description: badge earned count type: integer icon: description: badge icon type: string id: description: badge id type: string is_single: description: badge is single or multiple type: boolean level: allOf: - $ref: '#/definitions/entity.BadgeLevel' description: badge level name: description: badge name type: string type: object schema.GetBadgeListPagedResp: properties: award_count: description: badge award count type: integer description: description: badge description type: string earned: description: badge earned count type: boolean group_name: description: badge group name type: string icon: description: badge icon type: string id: description: badge id type: string level: allOf: - $ref: '#/definitions/entity.BadgeLevel' description: badge level name: description: badge name type: string status: allOf: - $ref: '#/definitions/schema.BadgeStatus' description: badge status type: object schema.GetBadgeListResp: properties: badges: description: badge list info items: $ref: '#/definitions/schema.BadgeListInfo' type: array group_name: description: badge group name type: string type: object schema.GetCommentPersonalWithPageResp: properties: answer_id: description: answer id type: string comment_id: description: comment id type: string content: description: content type: string created_at: description: create time type: integer object_id: description: object id type: string object_type: description: object type enum: - question - answer - tag - comment type: string question_id: description: question id type: string title: description: title type: string url_title: description: url title type: string type: object schema.GetCommentResp: properties: comment_id: description: comment id type: string created_at: description: create time type: integer is_vote: description: current user if already vote this comment type: boolean member_actions: description: MemberActions items: $ref: '#/definitions/schema.PermissionMemberAction' type: array object_id: description: object id type: string original_text: description: original comment content type: string parsed_text: description: parsed comment content type: string reply_comment_id: description: reply comment id type: string reply_user_display_name: description: reply user display name type: string reply_user_id: description: reply user id type: string reply_user_status: description: reply user status type: string reply_username: description: reply user username type: string user_avatar: description: user avatar type: string user_display_name: description: user display name type: string user_id: description: user id type: string user_status: description: user status type: string username: description: username type: string vote_count: description: user vote amount type: integer type: object schema.GetCurrentLoginUserInfoResp: properties: access_token: description: access token type: string answer_count: description: answer count type: integer authority_group: description: authority group type: integer avatar: $ref: '#/definitions/schema.AvatarInfo' bio: description: bio markdown type: string bio_html: description: bio html type: string color_scheme: description: Color scheme type: string created_at: description: create time type: integer display_name: description: display name type: string e_mail: description: email type: string follow_count: description: follow count type: integer have_password: description: user have password type: boolean id: description: user id type: string language: description: language type: string last_login_date: description: last login date type: integer location: description: location type: string mail_status: description: mail status(1 pass 2 to be verified) type: integer mobile: description: mobile type: string notice_status: description: notice status(1 on 2off) type: integer question_count: description: question count type: integer rank: description: rank type: integer role_id: description: role id type: integer status: description: user status type: string suspended_until: description: suspended until timestamp type: integer username: description: username type: string visit_token: description: visit token type: string website: description: website type: string type: object schema.GetFollowingTagsResp: properties: display_name: description: display name type: string main_tag_slug_name: description: if main tag slug name is not empty, this tag is synonymous with the main tag type: string recommend: type: boolean reserved: type: boolean slug_name: description: slug name type: string tag_id: description: tag id type: string type: object schema.GetObjectTimelineResp: properties: object_info: $ref: '#/definitions/schema.ActObjectInfo' timeline: items: $ref: '#/definitions/schema.ActObjectTimeline' type: array type: object schema.GetOtherUserInfoByUsernameResp: properties: answer_count: description: answer count type: integer avatar: description: avatar type: string bio: description: bio markdown type: string bio_html: description: bio html type: string created_at: description: create time type: integer display_name: description: display name type: string follow_count: description: |- email follow count type: integer id: description: user id type: string last_login_date: description: last login date type: integer location: description: location type: string mobile: description: mobile type: string question_count: description: question count type: integer rank: description: rank type: integer status: type: string status_msg: type: string suspended_until: description: suspended until timestamp type: integer username: description: username type: string website: description: website type: string type: object schema.GetOtherUserInfoResp: properties: info: $ref: '#/definitions/schema.GetOtherUserInfoByUsernameResp' type: object schema.GetPluginConfigResp: properties: config_fields: items: $ref: '#/definitions/schema.ConfigField' type: array description: type: string name: type: string slug_name: type: string version: type: string type: object schema.GetPluginListResp: properties: description: type: string enabled: type: boolean have_config: type: boolean link: type: string name: type: string slug_name: type: string version: type: string type: object schema.GetPrivilegesConfigResp: properties: options: items: $ref: '#/definitions/schema.PrivilegeOption' type: array selected_level: $ref: '#/definitions/schema.PrivilegeLevel' type: object schema.GetRankPersonalPageResp: properties: answer_id: description: answer id type: string content: description: content type: string created_at: description: create time type: integer object_id: description: object id type: string object_type: description: object type enum: - question - answer - tag - comment type: string question_id: description: question id type: string rank_type: description: rank type type: string reputation: description: reputation type: integer title: description: title type: string url_title: description: url title type: string type: object schema.GetReportListPageResp: properties: answer_accepted: type: boolean answer_count: type: integer answer_id: type: string author_user_info: $ref: '#/definitions/schema.UserBasicInfo' comment_id: type: string created_at: type: integer flag_id: type: string object_id: type: string object_show_status: type: integer object_status: type: integer object_type: enum: - question - answer - comment type: string original_text: type: string parsed_text: type: string question_id: type: string reason: $ref: '#/definitions/schema.ReasonItem' reason_content: type: string submit_at: type: integer submitter_user: $ref: '#/definitions/schema.UserBasicInfo' tags: items: $ref: '#/definitions/schema.TagResp' type: array title: type: string url_title: type: string type: object schema.GetReviewingTypeResp: properties: label: type: string name: type: string todo_amount: type: integer type: object schema.GetRevisionResp: properties: content: {} create_at: type: integer id: type: string object_id: type: string reason: type: string status: type: integer title: type: string url_title: type: string use_id: type: string user_info: $ref: '#/definitions/schema.UserBasicInfo' type: object schema.GetRoleResp: properties: description: type: string id: type: integer name: type: string type: object schema.GetSMTPConfigResp: properties: encryption: description: '"" SSL TLS' type: string from_email: type: string from_name: type: string smtp_authentication: type: boolean smtp_host: type: string smtp_password: type: string smtp_port: type: integer smtp_username: type: string type: object schema.GetSiteLegalInfoResp: properties: privacy_policy_original_text: type: string privacy_policy_parsed_text: type: string terms_of_service_original_text: type: string terms_of_service_parsed_text: type: string type: object schema.GetTagBasicResp: properties: display_name: type: string recommend: type: boolean reserved: type: boolean slug_name: type: string tag_id: type: string type: object schema.GetTagPageResp: properties: created_at: description: created time type: integer description: description: description type: string display_name: description: display_name type: string excerpt: description: excerpt type: string follow_count: description: follower amount type: integer is_follower: description: is follower type: boolean original_text: description: original text type: string parsed_text: description: parsed_text type: string question_count: description: question amount type: integer recommend: type: boolean reserved: type: boolean slug_name: description: slug_name type: string tag_id: description: tag_id type: string updated_at: description: updated time type: integer type: object schema.GetTagResp: properties: created_at: type: integer description: type: string display_name: type: string excerpt: type: string follow_count: type: integer is_follower: type: boolean main_tag_slug_name: description: if main tag slug name is not empty, this tag is synonymous with the main tag type: string member_actions: items: $ref: '#/definitions/schema.PermissionMemberAction' type: array original_text: type: string parsed_text: type: string question_count: type: integer recommend: type: boolean reserved: type: boolean slug_name: type: string status: type: string tag_id: type: string updated_at: type: integer type: object schema.GetTagSynonymsResp: properties: member_actions: description: MemberActions items: $ref: '#/definitions/schema.PermissionMemberAction' type: array synonyms: description: synonyms items: $ref: '#/definitions/schema.TagSynonym' type: array type: object schema.GetUnreviewedPostPageResp: properties: answer_id: type: string author_user_info: $ref: '#/definitions/schema.UserBasicInfo' comment_id: type: string created_at: type: integer object_id: type: string object_show_status: type: integer object_status: type: integer object_type: enum: - question - answer - comment type: string original_text: type: string parsed_text: type: string question_id: type: string reason: type: string review_id: type: integer submit_at: type: integer submitter_display_name: type: string tags: items: $ref: '#/definitions/schema.TagResp' type: array title: type: string url_title: type: string type: object schema.GetUnreviewedRevisionResp: properties: info: $ref: '#/definitions/schema.UnreviewedRevisionInfoInfo' type: type: string unreviewed_info: $ref: '#/definitions/schema.GetRevisionResp' type: object schema.GetUserActivationResp: properties: activation_url: type: string type: object schema.GetUserBadgeAwardListResp: properties: earned_count: description: badge award count type: integer icon: description: badge icon type: string id: description: badge id type: string level: allOf: - $ref: '#/definitions/entity.BadgeLevel' description: badge level name: description: badge name type: string type: object schema.GetUserNotificationConfigResp: properties: all_new_question: $ref: '#/definitions/schema.NotificationChannelConfig' all_new_question_for_following_tags: $ref: '#/definitions/schema.NotificationChannelConfig' inbox: $ref: '#/definitions/schema.NotificationChannelConfig' type: object schema.GetUserPageResp: properties: avatar: description: avatar type: string created_at: description: create time type: integer deleted_at: description: delete time type: integer display_name: description: display name type: string e_mail: description: email type: string rank: description: rank type: integer role_id: description: role id type: integer role_name: description: role name type: string status: description: user status(normal,suspended,deleted,inactive) type: string suspended_at: description: suspended time type: integer suspended_until: description: suspended until time type: integer user_id: description: user id type: string username: description: username type: string type: object schema.GetUserPluginListResp: properties: name: type: string slug_name: type: string type: object schema.GetUserStaffResp: properties: avatar: description: avatar type: string display_name: description: display name type: string username: description: username type: string type: object schema.GetVoteWithPageResp: properties: answer_id: description: answer id type: string content: description: content type: string created_at: description: create time type: integer object_id: description: object id type: string object_type: description: object type enum: - question - answer - tag - comment type: string question_id: description: question id type: string title: description: title type: string url_title: description: url title type: string vote_type: description: vote type type: string type: object schema.LoadingAction: properties: state: type: string text: type: string type: object schema.NotificationChannelConfig: properties: enable: type: boolean key: $ref: '#/definitions/constant.NotificationChannelKey' type: object schema.NotificationClearIDRequest: properties: id: type: string type: object schema.NotificationClearRequest: properties: type: enum: - inbox - achievement type: string required: - type type: object schema.OnCompleteAction: properties: refresh_form_config: type: boolean toast_return_message: type: boolean type: object schema.Operation: properties: description: type: string level: $ref: '#/definitions/schema.OperationLevel' msg: type: string time: type: integer type: type: string type: object schema.OperationLevel: enum: - info - danger - warning - secondary type: string x-enum-varnames: - OperationLevelInfo - OperationLevelDanger - OperationLevelWarning - OperationLevelSecondary schema.OperationQuestionReq: properties: id: type: string operation: description: operation [pin unpin hide show] type: string required: - id type: object schema.PermissionMemberAction: properties: action: type: string name: type: string type: type: string type: object schema.PostRenderReq: properties: content: type: string type: object schema.PrivilegeLevel: enum: - 1 - 2 - 3 - 99 type: integer x-enum-varnames: - PrivilegeLevel1 - PrivilegeLevel2 - PrivilegeLevel3 - PrivilegeLevelCustom schema.PrivilegeOption: properties: level: $ref: '#/definitions/schema.PrivilegeLevel' level_desc: type: string privileges: items: $ref: '#/definitions/constant.Privilege' type: array type: object schema.QuestionAdd: properties: captcha_code: type: string captcha_id: description: captcha_id type: string content: description: content maxLength: 65535 minLength: 0 type: string tags: description: tags items: $ref: '#/definitions/schema.TagItem' type: array title: description: question title maxLength: 150 minLength: 6 type: string required: - title type: object schema.QuestionAddByAnswer: properties: answer_content: maxLength: 65535 minLength: 6 type: string captcha_code: type: string captcha_id: description: captcha_id type: string content: description: content maxLength: 65535 minLength: 0 type: string mention_username_list: items: type: string type: array tags: description: tags items: $ref: '#/definitions/schema.TagItem' type: array title: description: question title maxLength: 150 minLength: 6 type: string required: - answer_content - title type: object schema.QuestionInfoResp: properties: accepted_answer_id: type: string answer_count: type: integer answered: type: boolean collected: type: boolean collection_count: type: integer content: type: string create_time: type: integer description: type: string edit_time: type: integer extends_actions: items: $ref: '#/definitions/schema.PermissionMemberAction' type: array first_answer_id: type: string follow_count: type: integer html: type: string id: type: string is_followed: type: boolean last_answer_id: type: string last_answered_user_info: $ref: '#/definitions/schema.UserBasicInfo' member_actions: description: MemberActions items: $ref: '#/definitions/schema.PermissionMemberAction' type: array operation: $ref: '#/definitions/schema.Operation' pin: type: integer show: type: integer status: type: integer tags: items: $ref: '#/definitions/schema.TagResp' type: array title: type: string unique_view_count: type: integer update_time: type: integer update_user_info: $ref: '#/definitions/schema.UserBasicInfo' url_title: type: string user_info: $ref: '#/definitions/schema.UserBasicInfo' view_count: type: integer vote_count: type: integer vote_status: type: string type: object schema.QuestionPageReq: properties: in_days: minimum: 1 type: integer order: enum: - newest - active - hot - score - unanswered - recommend - frequent type: string page: minimum: 1 type: integer page_size: minimum: 1 type: integer tag: maxLength: 100 type: string username: maxLength: 100 type: string type: object schema.QuestionPageResp: properties: accepted_answer_id: description: answer information type: string answer_count: type: integer collection_count: type: integer created_at: type: integer description: type: string follow_count: type: integer id: type: string last_answer_id: type: string operated_at: description: operator information type: integer operation_type: type: string operator: $ref: '#/definitions/schema.QuestionPageRespOperator' pin: description: '1: unpin, 2: pin' type: integer show: description: '0: show, 1: hide' type: integer status: type: integer tags: items: $ref: '#/definitions/schema.TagResp' type: array title: type: string unique_view_count: type: integer url_title: type: string view_count: description: question statistical information type: integer vote_count: type: integer type: object schema.QuestionPageRespOperator: properties: avatar: type: string display_name: type: string id: type: string rank: type: integer status: type: string username: type: string type: object schema.QuestionRecoverReq: properties: question_id: type: string required: - question_id type: object schema.QuestionUpdate: properties: captcha_code: type: string captcha_id: description: captcha_id type: string content: description: content maxLength: 65535 minLength: 0 type: string edit_summary: description: edit summary type: string id: description: question id type: string invite_user: items: type: string type: array tags: description: tags items: $ref: '#/definitions/schema.TagItem' type: array title: description: question title maxLength: 150 minLength: 6 type: string required: - id - title type: object schema.QuestionUpdateInviteUser: properties: captcha_code: type: string captcha_id: description: captcha_id type: string id: type: string invite_user: items: type: string type: array required: - id type: object schema.ReactionRespItem: properties: count: description: Count is the number of users who reacted type: integer emoji: description: Emoji is the reaction emoji type: string is_active: description: IsActive is if current user has reacted type: boolean tooltip: description: Tooltip is the user's name who reacted type: string type: object schema.ReasonItem: properties: content_type: type: string description: type: string name: type: string placeholder: type: string reason_key: type: string reason_type: type: integer type: object schema.RecoverAnswerReq: properties: answer_id: type: string required: - answer_id type: object schema.RecoverTagReq: properties: tag_id: type: string required: - tag_id type: object schema.RemoveAnswerReq: properties: captcha_code: type: string captcha_id: type: string id: type: string required: - id type: object schema.RemoveCommentReq: properties: captcha_code: type: string captcha_id: type: string comment_id: description: comment id type: string required: - comment_id type: object schema.RemoveQuestionReq: properties: captcha_code: type: string captcha_id: description: captcha_id type: string id: description: question id type: string required: - id type: object schema.RemoveTagReq: properties: tag_id: description: tag_id type: string required: - tag_id type: object schema.ReopenQuestionReq: properties: question_id: type: string type: object schema.ReviewReportReq: properties: close_msg: type: string close_type: type: integer content: maxLength: 65535 minLength: 6 type: string flag_id: type: string operation_type: enum: - edit_post - close_post - delete_post - unlist_post - ignore_report type: string tags: items: $ref: '#/definitions/schema.TagItem' type: array title: maxLength: 150 minLength: 6 type: string required: - flag_id - operation_type type: object schema.RevisionAuditReq: properties: id: description: object id type: string operation: description: approve or reject type: string required: - id - operation type: object schema.SearchObject: properties: accepted: type: boolean answer_count: type: integer created_at: type: integer excerpt: type: string id: type: string question_id: type: string status: description: Status type: string tags: description: tags items: $ref: '#/definitions/schema.TagResp' type: array title: type: string url_title: type: string user_info: allOf: - $ref: '#/definitions/schema.SearchObjectUser' description: user info vote_count: type: integer type: object schema.SearchObjectUser: properties: display_name: type: string id: type: string rank: type: integer status: type: string username: type: string type: object schema.SearchResp: properties: count: type: integer list: description: search response items: $ref: '#/definitions/schema.SearchResult' type: array type: object schema.SearchResult: properties: object: allOf: - $ref: '#/definitions/schema.SearchObject' description: this object object_type: description: object_type type: string type: object schema.SendUserActivationReq: properties: user_id: type: string required: - user_id type: object schema.SiteAIProvider: properties: api_host: maxLength: 512 type: string api_key: maxLength: 256 type: string model: maxLength: 100 type: string provider: maxLength: 50 type: string type: object schema.SiteAIReq: properties: ai_providers: items: $ref: '#/definitions/schema.SiteAIProvider' type: array chosen_provider: maxLength: 50 type: string enabled: type: boolean prompt_config: $ref: '#/definitions/schema.AIPromptConfig' type: object schema.SiteAIResp: properties: ai_providers: items: $ref: '#/definitions/schema.SiteAIProvider' type: array chosen_provider: maxLength: 50 type: string enabled: type: boolean prompt_config: $ref: '#/definitions/schema.AIPromptConfig' type: object schema.SiteAdvancedReq: properties: authorized_attachment_extensions: items: type: string type: array authorized_image_extensions: items: type: string type: array max_attachment_size: type: integer max_image_megapixel: type: integer max_image_size: type: integer type: object schema.SiteAdvancedResp: properties: authorized_attachment_extensions: items: type: string type: array authorized_image_extensions: items: type: string type: array max_attachment_size: type: integer max_image_megapixel: type: integer max_image_size: type: integer type: object schema.SiteBrandingReq: properties: favicon: maxLength: 512 type: string logo: maxLength: 512 type: string mobile_logo: maxLength: 512 type: string square_icon: maxLength: 512 type: string type: object schema.SiteBrandingResp: properties: favicon: maxLength: 512 type: string logo: maxLength: 512 type: string mobile_logo: maxLength: 512 type: string square_icon: maxLength: 512 type: string type: object schema.SiteCustomCssHTMLReq: properties: custom_css: maxLength: 65536 type: string custom_footer: maxLength: 65536 type: string custom_head: maxLength: 65536 type: string custom_header: maxLength: 65536 type: string custom_sidebar: maxLength: 65536 type: string type: object schema.SiteCustomCssHTMLResp: properties: custom_css: maxLength: 65536 type: string custom_footer: maxLength: 65536 type: string custom_head: maxLength: 65536 type: string custom_header: maxLength: 65536 type: string custom_sidebar: maxLength: 65536 type: string type: object schema.SiteGeneralReq: properties: contact_email: maxLength: 512 type: string description: maxLength: 2000 type: string name: maxLength: 128 type: string short_description: maxLength: 255 type: string site_url: maxLength: 512 type: string required: - contact_email - name - site_url type: object schema.SiteGeneralResp: properties: contact_email: maxLength: 512 type: string description: maxLength: 2000 type: string name: maxLength: 128 type: string short_description: maxLength: 255 type: string site_url: maxLength: 512 type: string required: - contact_email - name - site_url type: object schema.SiteInfoResp: properties: ai_enabled: type: boolean branding: $ref: '#/definitions/schema.SiteBrandingResp' custom_css_html: $ref: '#/definitions/schema.SiteCustomCssHTMLResp' general: $ref: '#/definitions/schema.SiteGeneralResp' interface: $ref: '#/definitions/schema.SiteInterfaceSettingsResp' login: $ref: '#/definitions/schema.SiteLoginResp' mcp_enabled: type: boolean revision: type: string site_advanced: $ref: '#/definitions/schema.SiteAdvancedResp' site_legal: $ref: '#/definitions/schema.SiteLegalSimpleResp' site_questions: $ref: '#/definitions/schema.SiteQuestionsResp' site_security: $ref: '#/definitions/schema.SiteSecurityResp' site_seo: $ref: '#/definitions/schema.SiteSeoResp' site_tags: $ref: '#/definitions/schema.SiteTagsResp' site_users: $ref: '#/definitions/schema.SiteUsersResp' theme: $ref: '#/definitions/schema.SiteThemeResp' users_settings: $ref: '#/definitions/schema.SiteUsersSettingsResp' version: type: string type: object schema.SiteInterfaceReq: properties: language: maxLength: 128 type: string time_zone: maxLength: 128 type: string required: - language - time_zone type: object schema.SiteInterfaceSettingsResp: properties: language: maxLength: 128 type: string time_zone: maxLength: 128 type: string required: - language - time_zone type: object schema.SiteLegalSimpleResp: properties: external_content_display: enum: - always_display - ask_before_display type: string required: - external_content_display type: object schema.SiteLoginReq: properties: allow_email_domains: items: type: string type: array allow_email_registrations: type: boolean allow_new_registrations: type: boolean allow_password_login: type: boolean type: object schema.SiteLoginResp: properties: allow_email_domains: items: type: string type: array allow_email_registrations: type: boolean allow_new_registrations: type: boolean allow_password_login: type: boolean type: object schema.SiteMCPReq: properties: enabled: type: boolean type: object schema.SiteMCPResp: properties: enabled: type: boolean http_header: type: string type: type: string url: type: string type: object schema.SitePoliciesReq: properties: privacy_policy_original_text: type: string privacy_policy_parsed_text: type: string terms_of_service_original_text: type: string terms_of_service_parsed_text: type: string type: object schema.SitePoliciesResp: properties: privacy_policy_original_text: type: string privacy_policy_parsed_text: type: string terms_of_service_original_text: type: string terms_of_service_parsed_text: type: string type: object schema.SiteQuestionsReq: properties: min_content: maximum: 65535 minimum: 0 type: integer min_tags: maximum: 5 minimum: 0 type: integer restrict_answer: type: boolean type: object schema.SiteQuestionsResp: properties: min_content: maximum: 65535 minimum: 0 type: integer min_tags: maximum: 5 minimum: 0 type: integer restrict_answer: type: boolean type: object schema.SiteSecurityReq: properties: check_update: type: boolean external_content_display: enum: - always_display - ask_before_display type: string login_required: type: boolean required: - external_content_display type: object schema.SiteSecurityResp: properties: check_update: type: boolean external_content_display: enum: - always_display - ask_before_display type: string login_required: type: boolean required: - external_content_display type: object schema.SiteSeoReq: properties: permalink: maximum: 4 minimum: 0 type: integer robots: type: string required: - permalink - robots type: object schema.SiteSeoResp: properties: permalink: maximum: 4 minimum: 0 type: integer robots: type: string required: - permalink - robots type: object schema.SiteTagsReq: properties: recommend_tags: items: $ref: '#/definitions/schema.SiteWriteTag' type: array required_tag: type: boolean reserved_tags: items: $ref: '#/definitions/schema.SiteWriteTag' type: array type: object schema.SiteTagsResp: properties: recommend_tags: items: $ref: '#/definitions/schema.SiteWriteTag' type: array required_tag: type: boolean reserved_tags: items: $ref: '#/definitions/schema.SiteWriteTag' type: array type: object schema.SiteThemeReq: properties: color_scheme: maxLength: 100 type: string layout: enum: - Full-width - Fixed-width type: string theme: maxLength: 255 type: string theme_config: additionalProperties: {} type: object required: - theme type: object schema.SiteThemeResp: properties: color_scheme: type: string layout: type: string theme: type: string theme_config: additionalProperties: {} type: object theme_options: items: $ref: '#/definitions/schema.ThemeOption' type: array type: object schema.SiteUsersReq: properties: allow_update_avatar: type: boolean allow_update_bio: type: boolean allow_update_display_name: type: boolean allow_update_location: type: boolean allow_update_username: type: boolean allow_update_website: type: boolean default_avatar: enum: - system - gravatar type: string gravatar_base_url: type: string required: - default_avatar type: object schema.SiteUsersResp: properties: allow_update_avatar: type: boolean allow_update_bio: type: boolean allow_update_display_name: type: boolean allow_update_location: type: boolean allow_update_username: type: boolean allow_update_website: type: boolean default_avatar: enum: - system - gravatar type: string gravatar_base_url: type: string required: - default_avatar type: object schema.SiteUsersSettingsReq: properties: default_avatar: enum: - system - gravatar type: string gravatar_base_url: type: string required: - default_avatar type: object schema.SiteUsersSettingsResp: properties: default_avatar: enum: - system - gravatar type: string gravatar_base_url: type: string required: - default_avatar type: object schema.SiteWriteTag: properties: display_name: type: string slug_name: type: string required: - slug_name type: object schema.TagItem: properties: display_name: description: display_name maxLength: 35 type: string original_text: description: original text type: string slug_name: description: slug_name maxLength: 35 type: string type: object schema.TagResp: properties: display_name: type: string main_tag_slug_name: description: if main tag slug name is not empty, this tag is synonymous with the main tag type: string recommend: type: boolean reserved: type: boolean slug_name: type: string type: object schema.TagSynonym: properties: display_name: description: display name type: string main_tag_slug_name: description: if main tag slug name is not empty, this tag is synonymous with the main tag type: string slug_name: description: slug name type: string tag_id: description: tag id type: string type: object schema.ThemeOption: properties: label: type: string value: type: string type: object schema.UIOptionAction: properties: loading: $ref: '#/definitions/schema.LoadingAction' method: type: string on_complete: $ref: '#/definitions/schema.OnCompleteAction' url: type: string type: object schema.UnreviewedRevisionInfoInfo: properties: answer_accepted: type: boolean answer_count: type: integer answer_id: type: string comment_id: type: string content: type: string created_at: type: integer html: type: string object_creator_user_id: type: string object_id: type: string object_type: type: string question_id: type: string show_status: type: integer status: type: integer tags: items: $ref: '#/definitions/schema.TagResp' type: array title: type: string url_title: type: string type: object schema.UpdateAPIKeyReq: properties: description: maxLength: 150 type: string id: type: integer required: - description - id type: object schema.UpdateBadgeStatusReq: properties: id: description: badge id type: string status: allOf: - $ref: '#/definitions/schema.BadgeStatus' description: badge status required: - id - status type: object schema.UpdateCommentReq: properties: captcha_code: type: string captcha_id: description: whether user can delete it type: string comment_id: description: comment id type: string original_text: description: original comment content maxLength: 600 minLength: 2 type: string required: - comment_id - original_text type: object schema.UpdateFollowTagsReq: properties: slug_name_list: description: tag slug name list items: type: string type: array type: object schema.UpdateInfoRequest: properties: avatar: $ref: '#/definitions/schema.AvatarInfo' bio: maxLength: 4096 type: string display_name: maxLength: 30 minLength: 2 type: string location: maxLength: 100 type: string username: maxLength: 30 minLength: 2 type: string website: maxLength: 500 type: string type: object schema.UpdatePluginConfigReq: properties: config_fields: additionalProperties: {} type: object plugin_slug_name: maxLength: 100 type: string required: - plugin_slug_name type: object schema.UpdatePluginStatusReq: properties: enabled: type: boolean plugin_slug_name: maxLength: 100 type: string required: - plugin_slug_name type: object schema.UpdatePrivilegesConfigReq: properties: custom_privileges: items: $ref: '#/definitions/constant.Privilege' type: array level: allOf: - $ref: '#/definitions/schema.PrivilegeLevel' minimum: 1 required: - level type: object schema.UpdateReactionReq: properties: emoji: enum: - heart - smile - frown type: string object_id: type: string reaction: enum: - activate - deactivate type: string required: - emoji - object_id - reaction type: object schema.UpdateReviewReq: properties: review_id: type: integer status: enum: - approve - reject type: string required: - review_id - status type: object schema.UpdateSMTPConfigReq: properties: encryption: description: '"" SSL TLS' enum: - SSL - TLS type: string from_email: maxLength: 256 type: string from_name: maxLength: 256 type: string smtp_authentication: type: boolean smtp_host: maxLength: 256 type: string smtp_password: maxLength: 256 type: string smtp_port: maximum: 65535 minimum: 1 type: integer smtp_username: maxLength: 256 type: string test_email_recipient: type: string type: object schema.UpdateTagReq: properties: display_name: description: display_name maxLength: 35 type: string edit_summary: description: edit summary type: string original_text: description: original text type: string slug_name: description: slug_name maxLength: 35 type: string tag_id: description: tag_id type: string required: - tag_id type: object schema.UpdateTagSynonymReq: properties: synonym_tag_list: description: synonym tag list items: $ref: '#/definitions/schema.TagItem' type: array tag_id: description: tag_id type: string required: - synonym_tag_list - tag_id type: object schema.UpdateUserInterfaceRequest: properties: color_scheme: description: Color scheme maxLength: 100 type: string language: description: language maxLength: 100 type: string required: - color_scheme - language type: object schema.UpdateUserNotificationConfigReq: properties: all_new_question: $ref: '#/definitions/schema.NotificationChannelConfig' all_new_question_for_following_tags: $ref: '#/definitions/schema.NotificationChannelConfig' inbox: $ref: '#/definitions/schema.NotificationChannelConfig' type: object schema.UpdateUserPasswordReq: properties: password: maxLength: 32 minLength: 8 type: string user_id: type: string required: - password - user_id type: object schema.UpdateUserPluginConfigReq: properties: config_fields: additionalProperties: {} type: object plugin_slug_name: maxLength: 100 type: string required: - plugin_slug_name type: object schema.UpdateUserRoleReq: properties: role_id: description: role id type: integer user_id: description: user id type: string required: - role_id - user_id type: object schema.UpdateUserStatusReq: properties: remove_all_content: type: boolean status: enum: - normal - suspended - deleted - inactive type: string suspend_duration: enum: - 24h - 48h - 72h - 7d - 14d - 1m - 2m - 3m - 6m - 1y - forever type: string user_id: type: string required: - status - user_id type: object schema.UserBasicInfo: properties: avatar: type: string display_name: type: string id: type: string language: type: string location: type: string rank: type: integer status: type: string suspended_until: type: integer username: type: string website: type: string type: object schema.UserChangeEmailSendCodeReq: properties: captcha_code: type: string captcha_id: type: string e_mail: maxLength: 500 type: string pass: maxLength: 32 minLength: 8 type: string required: - e_mail type: object schema.UserChangeEmailVerifyReq: properties: code: maxLength: 500 type: string required: - code type: object schema.UserEmailLoginReq: properties: captcha_code: type: string captcha_id: type: string e_mail: maxLength: 500 type: string pass: maxLength: 32 minLength: 8 type: string required: - e_mail - pass type: object schema.UserLoginResp: properties: access_token: description: access token type: string answer_count: description: answer count type: integer authority_group: description: authority group type: integer avatar: description: avatar type: string bio: description: bio markdown type: string bio_html: description: bio html type: string color_scheme: description: Color scheme type: string created_at: description: create time type: integer display_name: description: display name type: string e_mail: description: email type: string follow_count: description: follow count type: integer have_password: description: user have password type: boolean id: description: user id type: string language: description: language type: string last_login_date: description: last login date type: integer location: description: location type: string mail_status: description: mail status(1 pass 2 to be verified) type: integer mobile: description: mobile type: string notice_status: description: notice status(1 on 2off) type: integer question_count: description: question count type: integer rank: description: rank type: integer role_id: description: role id type: integer status: description: user status type: string suspended_until: description: suspended until timestamp type: integer username: description: username type: string visit_token: description: visit token type: string website: description: website type: string type: object schema.UserModifyPasswordReq: properties: captcha_code: type: string captcha_id: type: string old_pass: maxLength: 32 minLength: 8 type: string pass: maxLength: 32 minLength: 8 type: string required: - pass type: object schema.UserRankingResp: properties: staffs: items: $ref: '#/definitions/schema.UserRankingSimpleInfo' type: array users_with_the_most_reputation: items: $ref: '#/definitions/schema.UserRankingSimpleInfo' type: array users_with_the_most_vote: items: $ref: '#/definitions/schema.UserRankingSimpleInfo' type: array type: object schema.UserRankingSimpleInfo: properties: avatar: description: avatar type: string display_name: description: display name type: string rank: description: rank type: integer username: description: username type: string vote_count: description: vote type: integer type: object schema.UserRePassWordRequest: properties: code: maxLength: 100 type: string pass: maxLength: 32 type: string required: - code - pass type: object schema.UserRegisterReq: properties: captcha_code: type: string captcha_id: type: string e_mail: maxLength: 500 type: string name: maxLength: 30 minLength: 2 type: string pass: maxLength: 32 minLength: 8 type: string required: - e_mail - name - pass type: object schema.UserRetrievePassWordRequest: properties: captcha_code: type: string captcha_id: type: string e_mail: maxLength: 500 type: string required: - e_mail type: object schema.UserUnsubscribeNotificationReq: properties: code: maxLength: 500 type: string required: - code type: object schema.VoteReq: properties: captcha_code: type: string captcha_id: type: string is_cancel: type: boolean object_id: type: string required: - object_id type: object schema.VoteResp: properties: down_votes: type: integer up_votes: type: integer vote_status: type: string votes: type: integer type: object translator.LangOption: properties: label: type: string progress: description: Translation completion percentage type: integer value: type: string type: object info: contact: {} description: Apache Answer API title: Apache Answer paths: /: get: consumes: - application/json description: if config file not exist try to redirect to install page produces: - application/json responses: {} summary: if config file not exist try to redirect to install page tags: - installation /answer/admin/api/ai-config: get: description: get AI configuration produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteAIResp' type: object security: - ApiKeyAuth: [] summary: get AI configuration tags: - admin put: description: update AI configuration parameters: - description: AI config in: body name: data required: true schema: $ref: '#/definitions/schema.SiteAIReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update AI configuration tags: - admin /answer/admin/api/ai-models: post: description: get AI models produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetAIModelResp' type: array type: object security: - ApiKeyAuth: [] summary: get AI models tags: - admin /answer/admin/api/ai-provider: get: description: get AI provider configuration produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetAIProviderResp' type: array type: object security: - ApiKeyAuth: [] summary: get AI provider configuration tags: - admin /answer/admin/api/ai/conversation: delete: consumes: - application/json description: delete conversation and its related records for admin parameters: - description: apikey in: body name: data required: true schema: $ref: '#/definitions/schema.AIConversationAdminDeleteReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' summary: delete conversation for admin tags: - ai-conversation-admin get: consumes: - application/json description: get conversation detail for admin parameters: - description: conversation id in: query name: conversation_id required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.AIConversationAdminDetailResp' type: object summary: get conversation detail for admin tags: - ai-conversation-admin /answer/admin/api/ai/conversation/page: get: consumes: - application/json description: get conversation list for admin parameters: - description: page in: query name: page type: integer - description: page size in: query name: page_size type: integer produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.AIConversationAdminListItem' type: array type: object type: object summary: get conversation list for admin tags: - ai-conversation-admin /answer/admin/api/answer/page: get: consumes: - application/json description: Status:[available,deleted,pending] parameters: - description: page size in: query name: page type: integer - description: page size in: query name: page_size type: integer - description: user status enum: - available - deleted - pending in: query name: status type: string - description: answer id or question title in: query name: query type: string - description: question id in: query name: question_id type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: AdminAnswerPage admin answer page tags: - admin /answer/admin/api/answer/status: put: consumes: - application/json description: update answer status parameters: - description: AdminUpdateAnswerStatusReq in: body name: data required: true schema: $ref: '#/definitions/schema.AdminUpdateAnswerStatusReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update answer status tags: - admin /answer/admin/api/api-key: delete: description: delete apikey parameters: - description: apikey in: body name: data required: true schema: $ref: '#/definitions/schema.DeleteAPIKeyReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: delete apikey tags: - admin post: description: add apikey parameters: - description: apikey in: body name: data required: true schema: $ref: '#/definitions/schema.AddAPIKeyReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.AddAPIKeyResp' type: object security: - ApiKeyAuth: [] summary: add apikey tags: - admin put: description: update apikey parameters: - description: apikey in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateAPIKeyReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update apikey tags: - admin /answer/admin/api/api-key/all: get: description: get all api keys produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetAPIKeyResp' type: array type: object security: - ApiKeyAuth: [] summary: get all api keys tags: - admin /answer/admin/api/badge/status: put: consumes: - application/json description: update badge status parameters: - description: UpdateBadgeStatusReq in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateBadgeStatusReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update badge status tags: - AdminBadge /answer/admin/api/badges: get: consumes: - application/json description: list all badges by page parameters: - description: page in: query name: page type: integer - description: page size in: query name: page_size type: integer - description: badge status enum: - "" - active - inactive in: query name: status type: string - description: search param in: query name: q type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetBadgeListPagedResp' type: array type: object security: - ApiKeyAuth: [] summary: list all badges by page tags: - AdminBadge /answer/admin/api/dashboard: get: consumes: - application/json description: DashboardInfo produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: DashboardInfo tags: - admin /answer/admin/api/delete/permanently: delete: consumes: - application/json description: delete permanently parameters: - description: DeletePermanentlyReq in: body name: data required: true schema: $ref: '#/definitions/schema.DeletePermanentlyReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: delete permanently tags: - admin /answer/admin/api/language/options: get: description: Get language options produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: Get language options tags: - Lang /answer/admin/api/mcp-config: get: description: get MCP configuration produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteMCPResp' type: object security: - ApiKeyAuth: [] summary: get MCP configuration tags: - admin put: description: update MCP configuration parameters: - description: MCP config in: body name: data required: true schema: $ref: '#/definitions/schema.SiteMCPReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update MCP configuration tags: - admin /answer/admin/api/plugin/config: get: description: get plugin config parameters: - description: plugin_slug_name in: query name: plugin_slug_name required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetPluginConfigResp' type: object security: - ApiKeyAuth: [] summary: get plugin config tags: - AdminPlugin put: consumes: - application/json description: update plugin config parameters: - description: UpdatePluginConfigReq in: body name: data required: true schema: $ref: '#/definitions/schema.UpdatePluginConfigReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update plugin config tags: - AdminPlugin /answer/admin/api/plugin/status: put: consumes: - application/json description: update plugin status parameters: - description: UpdatePluginStatusReq in: body name: data required: true schema: $ref: '#/definitions/schema.UpdatePluginStatusReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update plugin status tags: - AdminPlugin /answer/admin/api/plugins: get: consumes: - application/json description: get plugin list parameters: - description: 'status: active/inactive' in: query name: status type: string - description: have config in: query name: have_config type: boolean produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetPluginListResp' type: array type: object security: - ApiKeyAuth: [] summary: get plugin list tags: - AdminPlugin /answer/admin/api/question/page: get: consumes: - application/json description: Status:[available,closed,deleted,pending] parameters: - description: page size in: query name: page type: integer - description: page size in: query name: page_size type: integer - description: user status enum: - available - closed - deleted - pending in: query name: status type: string - description: question id or title in: query name: query type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: AdminQuestionPage admin question page tags: - admin /answer/admin/api/question/status: put: consumes: - application/json description: update question status parameters: - description: AdminUpdateQuestionStatusReq in: body name: data required: true schema: $ref: '#/definitions/schema.AdminUpdateQuestionStatusReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update question status tags: - admin /answer/admin/api/reasons: get: consumes: - application/json description: get reasons by object type and action parameters: - description: object_type enum: - question - answer - comment - user in: query name: object_type required: true type: string - description: action enum: - status - close - flag - review in: query name: action required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: get reasons by object type and action tags: - reason /answer/admin/api/roles: get: description: get role list produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetRoleResp' type: array type: object security: - ApiKeyAuth: [] summary: get role list tags: - admin /answer/admin/api/setting/privileges: get: description: GetPrivilegesConfig get privileges config produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetPrivilegesConfigResp' type: object security: - ApiKeyAuth: [] summary: GetPrivilegesConfig get privileges config tags: - admin put: description: update privileges config parameters: - description: config in: body name: data required: true schema: $ref: '#/definitions/schema.UpdatePrivilegesConfigReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update privileges config tags: - admin /answer/admin/api/setting/smtp: get: description: GetSMTPConfig get smtp config produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetSMTPConfigResp' type: object security: - ApiKeyAuth: [] summary: GetSMTPConfig get smtp config tags: - admin put: description: update smtp config parameters: - description: smtp config in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateSMTPConfigReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update smtp config tags: - admin /answer/admin/api/siteinfo/advanced: get: description: get site advanced setting produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteAdvancedResp' type: object security: - ApiKeyAuth: [] summary: get site advanced setting tags: - admin put: description: update site advanced info parameters: - description: advanced settings in: body name: data required: true schema: $ref: '#/definitions/schema.SiteAdvancedReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site advanced info tags: - admin /answer/admin/api/siteinfo/branding: get: description: get site interface produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteBrandingResp' type: object security: - ApiKeyAuth: [] summary: get site interface tags: - admin put: description: update site info branding parameters: - description: branding info in: body name: data required: true schema: $ref: '#/definitions/schema.SiteBrandingReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site info branding tags: - admin /answer/admin/api/siteinfo/custom-css-html: get: description: get site info custom html css config produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteCustomCssHTMLResp' type: object security: - ApiKeyAuth: [] summary: get site info custom html css config tags: - admin put: description: update site custom css html config parameters: - description: login info in: body name: data required: true schema: $ref: '#/definitions/schema.SiteCustomCssHTMLReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site custom css html config tags: - admin /answer/admin/api/siteinfo/general: get: description: get site general information produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteGeneralResp' type: object security: - ApiKeyAuth: [] summary: get site general information tags: - admin put: description: update site general information parameters: - description: general in: body name: data required: true schema: $ref: '#/definitions/schema.SiteGeneralReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site general information tags: - admin /answer/admin/api/siteinfo/interface: get: description: get site interface produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteInterfaceSettingsResp' type: object security: - ApiKeyAuth: [] summary: get site interface tags: - admin put: description: update site info interface parameters: - description: general in: body name: data required: true schema: $ref: '#/definitions/schema.SiteInterfaceReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site info interface tags: - admin /answer/admin/api/siteinfo/login: get: description: get site info login config produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteLoginResp' type: object security: - ApiKeyAuth: [] summary: get site info login config tags: - admin put: description: update site login parameters: - description: login info in: body name: data required: true schema: $ref: '#/definitions/schema.SiteLoginReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site login tags: - admin /answer/admin/api/siteinfo/polices: get: description: Get the policies information for the site produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SitePoliciesResp' type: object security: - ApiKeyAuth: [] summary: Get the policies information for the site tags: - admin put: description: update site policies configuration parameters: - description: write info in: body name: data required: true schema: $ref: '#/definitions/schema.SitePoliciesReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site policies configuration tags: - admin /answer/admin/api/siteinfo/question: get: description: get site questions setting produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteQuestionsResp' type: object security: - ApiKeyAuth: [] summary: get site questions setting tags: - admin put: description: update site question settings parameters: - description: questions settings in: body name: data required: true schema: $ref: '#/definitions/schema.SiteQuestionsReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site question settings tags: - admin /answer/admin/api/siteinfo/security: get: description: Get the security information for the site produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteSecurityResp' type: object security: - ApiKeyAuth: [] summary: Get the security information for the site tags: - admin put: description: update site security configuration parameters: - description: write info in: body name: data required: true schema: $ref: '#/definitions/schema.SiteSecurityReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site security configuration tags: - admin /answer/admin/api/siteinfo/seo: get: description: get site seo information produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteSeoResp' type: object security: - ApiKeyAuth: [] summary: get site seo information tags: - admin put: description: update site seo information parameters: - description: seo in: body name: data required: true schema: $ref: '#/definitions/schema.SiteSeoReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site seo information tags: - admin /answer/admin/api/siteinfo/tag: get: description: get site tags setting produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteTagsResp' type: object security: - ApiKeyAuth: [] summary: get site tags setting tags: - admin put: description: update site tag settings parameters: - description: tags settings in: body name: data required: true schema: $ref: '#/definitions/schema.SiteTagsReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site tag settings tags: - admin /answer/admin/api/siteinfo/theme: get: description: get site info theme config produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteThemeResp' type: object security: - ApiKeyAuth: [] summary: get site info theme config tags: - admin put: description: update site custom css html config parameters: - description: login info in: body name: data required: true schema: $ref: '#/definitions/schema.SiteThemeReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site custom css html config tags: - admin /answer/admin/api/siteinfo/users: get: description: get site user config produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteUsersResp' type: object security: - ApiKeyAuth: [] summary: get site user config tags: - admin put: description: update site info config about users parameters: - description: users info in: body name: data required: true schema: $ref: '#/definitions/schema.SiteUsersReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site info config about users tags: - admin /answer/admin/api/siteinfo/users-settings: get: description: get site interface produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteUsersSettingsResp' type: object security: - ApiKeyAuth: [] summary: get site interface tags: - admin put: description: update site info users settings parameters: - description: general in: body name: data required: true schema: $ref: '#/definitions/schema.SiteUsersSettingsReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update site info users settings tags: - admin /answer/admin/api/theme/options: get: description: Get theme options produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: Get theme options tags: - admin /answer/admin/api/user: post: consumes: - application/json description: add user parameters: - description: user in: body name: data required: true schema: $ref: '#/definitions/schema.AddUserReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: add user tags: - admin /answer/admin/api/user/activation: get: description: get user activation parameters: - description: user id in: query name: user_id required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetUserActivationResp' type: object security: - ApiKeyAuth: [] summary: get user activation tags: - admin /answer/admin/api/user/password: put: consumes: - application/json description: update user password parameters: - description: user in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateUserPasswordReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update user password tags: - admin /answer/admin/api/user/profile: put: consumes: - application/json description: edit user profile parameters: - description: user in: body name: data required: true schema: $ref: '#/definitions/schema.EditUserProfileReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: edit user profile tags: - admin /answer/admin/api/user/role: put: consumes: - application/json description: update user role parameters: - description: user in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateUserRoleReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update user role tags: - admin /answer/admin/api/user/status: put: consumes: - application/json description: update user parameters: - description: user in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateUserStatusReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update user tags: - admin /answer/admin/api/users: post: consumes: - application/json description: add users parameters: - description: user in: body name: data required: true schema: $ref: '#/definitions/schema.AddUsersReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: add users tags: - admin /answer/admin/api/users/activation: post: description: send user activation parameters: - description: SendUserActivationReq in: body name: data required: true schema: $ref: '#/definitions/schema.SendUserActivationReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: send user activation tags: - admin /answer/admin/api/users/page: get: description: get user page parameters: - description: page size in: query name: page type: integer - description: page size in: query name: page_size type: integer - description: 'search query: email, username or id:[id]' in: query name: query type: string - description: staff user in: query name: staff type: boolean - description: user status enum: - suspended - deleted - inactive in: query name: status type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: records: items: $ref: '#/definitions/schema.GetUserPageResp' type: array type: object type: object security: - ApiKeyAuth: [] summary: get user page tags: - admin /answer/api/v1/activity/timeline: get: description: get object timeline parameters: - description: object id in: query name: object_id type: string - description: tag slug name in: query name: tag_slug_name type: string - description: object type enum: - question - answer - tag in: query name: object_type type: string - description: is show vote in: query name: show_vote type: boolean produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetObjectTimelineResp' type: object summary: get object timeline tags: - Comment /answer/api/v1/activity/timeline/detail: get: description: get object timeline detail parameters: - description: revision id in: query name: revision_id required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetObjectTimelineResp' type: object summary: get object timeline detail tags: - Comment /answer/api/v1/ai/conversation: get: consumes: - application/json description: get conversation detail parameters: - description: conversation id in: query name: conversation_id required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.AIConversationDetailResp' type: object summary: get conversation detail tags: - ai-conversation /answer/api/v1/ai/conversation/page: get: consumes: - application/json description: get conversation list parameters: - description: page in: query name: page type: integer - description: page size in: query name: page_size type: integer produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.AIConversationListItem' type: array type: object type: object summary: get conversation list tags: - ai-conversation /answer/api/v1/ai/conversation/vote: post: consumes: - application/json description: vote record parameters: - description: vote request in: body name: data required: true schema: $ref: '#/definitions/schema.AIConversationVoteReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' summary: vote record tags: - ai-conversation /answer/api/v1/answer: delete: consumes: - application/json description: delete answer parameters: - description: answer in: body name: data required: true schema: $ref: '#/definitions/schema.RemoveAnswerReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: delete answer tags: - Answer post: consumes: - application/json description: add answer parameters: - description: add answer request in: body name: data required: true schema: $ref: '#/definitions/schema.AnswerAddReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: Add Answer tags: - Answer put: consumes: - application/json description: Update Answer parameters: - description: AnswerUpdateReq in: body name: data required: true schema: $ref: '#/definitions/schema.AnswerUpdateReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: Update Answer tags: - Answer /answer/api/v1/answer/acceptance: post: consumes: - application/json description: Accept Answer parameters: - description: AcceptAnswerReq in: body name: data required: true schema: $ref: '#/definitions/schema.AcceptAnswerReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: Accept Answer tags: - Answer /answer/api/v1/answer/info: get: consumes: - application/json description: Get Answer Detail parameters: - description: id in: query name: id required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetAnswerInfoResp' type: object summary: Get Answer Detail tags: - Answer /answer/api/v1/answer/page: get: consumes: - application/json description: AnswerList
order (default or updated) parameters: - description: question_id in: query name: question_id required: true type: string - description: order in: query name: order required: true type: string - description: page in: query name: page required: true type: string - description: page_size in: query name: page_size required: true type: string produces: - application/json responses: "200": description: OK schema: type: string summary: AnswerList tags: - Answer /answer/api/v1/answer/recover: post: consumes: - application/json description: recover the deleted answer parameters: - description: answer in: body name: data required: true schema: $ref: '#/definitions/schema.RecoverAnswerReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: recover answer tags: - Answer /answer/api/v1/badge: get: consumes: - application/json description: get badge info parameters: - default: string description: id in: query name: id required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetBadgeInfoResp' type: object summary: get badge info tags: - api-badge /answer/api/v1/badge/awards/page: get: consumes: - application/json description: get badge award list parameters: - description: page in: query name: page type: integer - description: page size in: query name: page_size type: integer - description: badge id in: query name: badge_id required: true type: string - description: only list the award by username in: query name: username type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetBadgeInfoResp' type: object summary: get badge award list tags: - api-badge /answer/api/v1/badge/user/awards: get: consumes: - application/json description: get user badge award list parameters: - description: user name in: query name: username required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetUserBadgeAwardListResp' type: array type: object summary: get user badge award list tags: - api-badge /answer/api/v1/badge/user/awards/recent: get: consumes: - application/json description: get user badge award list parameters: - description: user name in: query name: username required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetUserBadgeAwardListResp' type: array type: object summary: get user badge award list tags: - api-badge /answer/api/v1/badges: get: consumes: - application/json description: list all badges group by group produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetBadgeListResp' type: array type: object summary: list all badges group by group tags: - api-badge /answer/api/v1/collection/switch: post: consumes: - application/json description: add collection parameters: - description: collection in: body name: data required: true schema: $ref: '#/definitions/schema.CollectionSwitchReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.CollectionSwitchResp' type: object security: - ApiKeyAuth: [] summary: add collection tags: - Collection /answer/api/v1/comment: delete: consumes: - application/json description: remove comment parameters: - description: comment in: body name: data required: true schema: $ref: '#/definitions/schema.RemoveCommentReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: remove comment tags: - Comment get: description: get comment by id parameters: - description: id in: query name: id required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.GetCommentResp' type: array type: object type: object summary: get comment by id tags: - Comment post: consumes: - application/json description: add comment parameters: - description: comment in: body name: data required: true schema: $ref: '#/definitions/schema.AddCommentReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetCommentResp' type: object security: - ApiKeyAuth: [] summary: add comment tags: - Comment put: consumes: - application/json description: update comment parameters: - description: comment in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateCommentReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update comment tags: - Comment /answer/api/v1/comment/page: get: description: get comment page parameters: - description: page in: query name: page type: integer - description: page size in: query name: page_size type: integer - description: object id in: query name: object_id required: true type: string - description: query condition enum: - vote in: query name: query_cond type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.GetCommentResp' type: array type: object type: object summary: get comment page tags: - Comment /answer/api/v1/connector/binding/email: post: consumes: - application/json description: external login binding user send email parameters: - description: external login binding user send email in: body name: data required: true schema: $ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailResp' type: object summary: external login binding user send email tags: - PluginConnector /answer/api/v1/connector/info: get: description: get all enabled connectors produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.ConnectorInfoResp' type: array type: object security: - ApiKeyAuth: [] summary: get all enabled connectors tags: - PluginConnector /answer/api/v1/connector/user/info: get: description: get all connectors info about user produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.ConnectorUserInfoResp' type: array type: object security: - ApiKeyAuth: [] summary: get all connectors info about user tags: - PluginConnector /answer/api/v1/connector/user/unbinding: delete: consumes: - application/json description: unbind external user login parameters: - description: ExternalLoginUnbindingReq in: body name: data required: true schema: $ref: '#/definitions/schema.ExternalLoginUnbindingReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: unbind external user login tags: - PluginConnector /answer/api/v1/embed/config: get: consumes: - application/json description: get embed plugin config produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/plugin.EmbedConfig' type: array type: object summary: get embed plugin config tags: - Plugin /answer/api/v1/file: post: consumes: - multipart/form-data description: upload file parameters: - description: identify the source of the file upload enum: - post - post_attachment - avatar - branding in: formData name: source required: true type: string - description: file in: formData name: file required: true type: file responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: type: string type: object security: - ApiKeyAuth: [] summary: upload file tags: - Upload /answer/api/v1/follow: post: consumes: - application/json description: follow object or cancel follow operation parameters: - description: follow in: body name: data required: true schema: $ref: '#/definitions/schema.FollowReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.FollowResp' type: object security: - ApiKeyAuth: [] summary: follow object or cancel follow operation tags: - Activity /answer/api/v1/follow/tags: put: consumes: - application/json description: update user follow tags parameters: - description: follow in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateFollowTagsReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update user follow tags tags: - Activity /answer/api/v1/language/config: get: description: get language config mapping parameters: - description: Accept-Language in: header name: Accept-Language required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' summary: get language config mapping tags: - Lang /answer/api/v1/language/options: get: description: Get language options produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' summary: Get language options tags: - Lang /answer/api/v1/meta/reaction: get: consumes: - application/json description: get reaction for an object parameters: - description: object_id in: query name: object_id required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.ReactionRespItem' type: object summary: get reaction tags: - Meta put: consumes: - application/json description: update reaction. if not exist, add one parameters: - description: reaction in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateReactionReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: add or update reaction tags: - Meta /answer/api/v1/notification/page: get: consumes: - application/json description: get notification list parameters: - description: page size in: query name: page type: integer - description: page size in: query name: page_size type: integer - description: type enum: - inbox - achievement in: query name: type required: true type: string - description: inbox_type enum: - all - posts - invites - votes in: query name: inbox_type required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: get notification list tags: - Notification /answer/api/v1/notification/read/state: put: consumes: - application/json description: ClearUnRead parameters: - description: NotificationClearIDRequest in: body name: data required: true schema: $ref: '#/definitions/schema.NotificationClearIDRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: ClearUnRead tags: - Notification /answer/api/v1/notification/read/state/all: put: consumes: - application/json description: ClearUnRead parameters: - description: NotificationClearRequest in: body name: data required: true schema: $ref: '#/definitions/schema.NotificationClearRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: ClearUnRead tags: - Notification /answer/api/v1/notification/status: get: consumes: - application/json description: GetRedDot produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: GetRedDot tags: - Notification put: consumes: - application/json description: DelRedDot parameters: - description: NotificationClearRequest in: body name: data required: true schema: $ref: '#/definitions/schema.NotificationClearRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: DelRedDot tags: - Notification /answer/api/v1/permission: get: description: check user permission parameters: - description: access-token in: header name: Authorization required: true type: string - description: permission key enum: - question.add - question.edit - question.edit_without_review - question.delete - question.close - question.reopen - question.vote_up - question.vote_down - question.pin - question.unpin - question.hide - question.show - answer.add - answer.edit - answer.edit_without_review - answer.delete - answer.accept - answer.vote_up - answer.vote_down - answer.invite_someone_to_answer - comment.add - comment.edit - comment.delete - comment.vote_up - comment.vote_down - report.add - tag.add - tag.edit - tag.edit_slug_name - tag.edit_without_review - tag.delete - tag.synonym - link.url_limit - vote.detail - answer.audit - question.audit - tag.audit - tag.use_reserved_tag in: query name: action required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: additionalProperties: type: boolean type: object type: object security: - ApiKeyAuth: [] summary: check user permission tags: - Permission /answer/api/v1/personal/answer/page: get: consumes: - application/json description: list personal answers parameters: - default: string description: username in: query name: username required: true type: string - description: order enum: - newest - score in: query name: order required: true type: string - default: "0" description: page in: query name: page required: true type: string - default: "20" description: page_size in: query name: page_size required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: list personal answers tags: - Personal /answer/api/v1/personal/collection/page: get: consumes: - application/json description: list personal collections parameters: - default: "0" description: page in: query name: page required: true type: string - default: "20" description: page_size in: query name: page_size required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: list personal collections tags: - Collection /answer/api/v1/personal/comment/page: get: description: user personal comment list parameters: - description: page in: query name: page type: integer - description: page size in: query name: page_size type: integer - description: username in: query name: username type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.GetCommentPersonalWithPageResp' type: array type: object type: object summary: user personal comment list tags: - Comment /answer/api/v1/personal/qa/top: get: consumes: - application/json description: UserTop parameters: - default: string description: username in: query name: username required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' summary: UserTop tags: - Question /answer/api/v1/personal/rank/page: get: description: user personal rank list parameters: - description: page in: query name: page type: integer - description: page size in: query name: page_size type: integer - description: username in: query name: username type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.GetRankPersonalPageResp' type: array type: object type: object summary: user personal rank list tags: - Rank /answer/api/v1/personal/user/info: get: consumes: - application/json description: GetOtherUserInfoByUsername parameters: - description: username in: query name: username required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetOtherUserInfoResp' type: object security: - ApiKeyAuth: [] summary: GetOtherUserInfoByUsername tags: - User /answer/api/v1/personal/vote/page: get: consumes: - application/json description: get user personal votes parameters: - description: page size in: query name: page type: integer - description: page size in: query name: page_size type: integer produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.GetVoteWithPageResp' type: array type: object type: object security: - ApiKeyAuth: [] summary: get user personal votes tags: - Activity /answer/api/v1/plugin/status: get: consumes: - application/json description: get all plugins status produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetPluginListResp' type: array type: object summary: get all plugins status tags: - Plugin /answer/api/v1/post/render: post: consumes: - application/json description: render post content parameters: - description: PostRenderReq in: body name: data required: true schema: $ref: '#/definitions/schema.PostRenderReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: render post content tags: - Upload /answer/api/v1/question: delete: consumes: - application/json description: delete question parameters: - description: question in: body name: data required: true schema: $ref: '#/definitions/schema.RemoveQuestionReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: delete question tags: - Question post: consumes: - application/json description: add question parameters: - description: question in: body name: data required: true schema: $ref: '#/definitions/schema.QuestionAdd' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: add question tags: - Question put: consumes: - application/json description: update question parameters: - description: question in: body name: data required: true schema: $ref: '#/definitions/schema.QuestionUpdate' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update question tags: - Question /answer/api/v1/question/answer: post: consumes: - application/json description: add question and answer parameters: - description: question in: body name: data required: true schema: $ref: '#/definitions/schema.QuestionAddByAnswer' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: add question and answer tags: - Question /answer/api/v1/question/info: get: consumes: - application/json description: get question details parameters: - default: "1" description: Question TagID in: query name: id required: true type: string produces: - application/json responses: "200": description: OK schema: type: string summary: get question details tags: - Question /answer/api/v1/question/invite: get: consumes: - application/json description: get question invite user info parameters: - default: "1" description: Question ID in: query name: id required: true type: string produces: - application/json responses: "200": description: OK schema: type: string summary: get question invite user info tags: - Question put: consumes: - application/json description: update question invite user parameters: - description: question in: body name: data required: true schema: $ref: '#/definitions/schema.QuestionUpdateInviteUser' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update question invite user tags: - Question /answer/api/v1/question/link: get: description: get question link parameters: - in: query minimum: 1 name: in_days type: integer - enum: - newest - active - hot - score - unanswered - recommend - frequent in: query name: order type: string - in: query minimum: 1 name: page type: integer - in: query maximum: 100 minimum: 1 name: page_size type: integer - in: query name: question_id required: true type: string responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.QuestionPageResp' type: array type: object type: object summary: get question link tags: - Question /answer/api/v1/question/operation: put: consumes: - application/json description: Operation question \n operation [pin unpin hide show] parameters: - description: question in: body name: data required: true schema: $ref: '#/definitions/schema.OperationQuestionReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: Operation question tags: - Question /answer/api/v1/question/page: get: consumes: - application/json description: get questions by page parameters: - description: QuestionPageReq in: body name: data required: true schema: $ref: '#/definitions/schema.QuestionPageReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.QuestionPageResp' type: array type: object type: object summary: get questions by page tags: - Question /answer/api/v1/question/recommend/page: get: consumes: - application/json description: get recommend questions by page parameters: - description: QuestionPageReq in: body name: data required: true schema: $ref: '#/definitions/schema.QuestionPageReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.QuestionPageResp' type: array type: object type: object summary: get recommend questions by page tags: - Question /answer/api/v1/question/recover: post: consumes: - application/json description: recover deleted question parameters: - description: question in: body name: data required: true schema: $ref: '#/definitions/schema.QuestionRecoverReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: recover deleted question tags: - Question /answer/api/v1/question/reopen: put: consumes: - application/json description: reopen question parameters: - description: question in: body name: data required: true schema: $ref: '#/definitions/schema.ReopenQuestionReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: reopen question tags: - Question /answer/api/v1/question/similar: get: consumes: - application/json description: fuzzy query similar questions based on title parameters: - default: string description: title in: query name: title required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: fuzzy query similar questions based on title tags: - Question /answer/api/v1/question/similar/tag: get: consumes: - application/json description: Search Similar Question parameters: - default: "" description: question_id in: query name: question_id required: true type: string produces: - application/json responses: "200": description: OK schema: type: string summary: Search Similar Question tags: - Question /answer/api/v1/question/status: put: consumes: - application/json description: Close question parameters: - description: question in: body name: data required: true schema: $ref: '#/definitions/schema.CloseQuestionReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: Close question tags: - Question /answer/api/v1/question/tags: get: description: get tag list parameters: - description: tag in: query name: tag type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetTagBasicResp' type: array type: object security: - ApiKeyAuth: [] summary: get tag list tags: - Tag /answer/api/v1/reasons: get: consumes: - application/json description: get reasons by object type and action parameters: - description: object_type enum: - question - answer - comment - user in: query name: object_type required: true type: string - description: action enum: - status - close - flag - review in: query name: action required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: get reasons by object type and action tags: - reason /answer/api/v1/render/config: get: consumes: - application/json description: GetRenderConfig produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/plugin.RenderConfig' type: object summary: GetRenderConfig tags: - PluginRender /answer/api/v1/report: post: consumes: - application/json description: add report
source (question, answer, comment, user) parameters: - description: report in: body name: data required: true schema: $ref: '#/definitions/schema.AddReportReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: add report tags: - Report /answer/api/v1/report/review: put: consumes: - application/json description: review report parameters: - description: flag in: body name: data required: true schema: $ref: '#/definitions/schema.ReviewReportReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: review report tags: - Report /answer/api/v1/report/unreviewed/post: get: consumes: - application/json description: get unreviewed report post page parameters: - description: page in: query name: page type: integer produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.GetReportListPageResp' type: array type: object type: object security: - ApiKeyAuth: [] summary: get unreviewed report post page tags: - Report /answer/api/v1/review/pending/post: put: consumes: - application/json description: update review parameters: - description: review in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateReviewReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update review tags: - Review /answer/api/v1/review/pending/post/page: get: consumes: - application/json description: get unreviewed post page parameters: - description: page in: query name: page type: integer - description: object_id in: query name: object_id type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.GetUnreviewedPostPageResp' type: array type: object type: object security: - ApiKeyAuth: [] summary: get unreviewed post page tags: - Review /answer/api/v1/reviewing/type: get: description: get reviewing type produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetReviewingTypeResp' type: array type: object security: - ApiKeyAuth: [] summary: get reviewing type tags: - Revision /answer/api/v1/revisions: get: description: get revision list parameters: - description: object id in: query name: object_id required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetRevisionResp' type: array type: object summary: get revision list tags: - Revision /answer/api/v1/revisions/audit: put: description: revision audit operation:approve or reject parameters: - description: audit in: body name: data required: true schema: $ref: '#/definitions/schema.RevisionAuditReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: revision audit tags: - Revision /answer/api/v1/revisions/edit/check: get: consumes: - application/json description: check can update revision parameters: - default: string description: id in: query name: id required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: check can update revision tags: - Revision /answer/api/v1/revisions/unreviewed: get: description: get unreviewed revision list parameters: - description: page id in: query name: page required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.GetUnreviewedRevisionResp' type: array type: object type: object security: - ApiKeyAuth: [] summary: get unreviewed revision list tags: - Revision /answer/api/v1/search: get: description: search object parameters: - description: query string in: query name: q required: true type: string - description: order enum: - newest - active - score - relevance in: query name: order required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SearchResp' type: object security: - ApiKeyAuth: [] summary: search object tags: - Search /answer/api/v1/search/desc: get: description: get search description produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SearchResp' type: object summary: get search description tags: - Search /answer/api/v1/siteinfo: get: description: get site info produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.SiteInfoResp' type: object summary: get site info tags: - site /answer/api/v1/siteinfo/legal: get: description: get site legal info parameters: - description: legal information type enum: - tos - privacy in: query name: info_type required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetSiteLegalInfoResp' type: object summary: get site legal info tags: - site /answer/api/v1/tag: delete: consumes: - application/json description: delete tag parameters: - description: tag in: body name: data required: true schema: $ref: '#/definitions/schema.RemoveTagReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: delete tag tags: - Tag get: consumes: - application/json description: get tag one parameters: - description: tag id in: query name: tag_id required: true type: string - description: tag name in: query name: tag_name required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetTagResp' type: object summary: get tag one tags: - Tag post: consumes: - application/json description: add tag parameters: - description: tag in: body name: data required: true schema: $ref: '#/definitions/schema.AddTagReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: add tag tags: - Tag put: consumes: - application/json description: update tag parameters: - description: tag in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateTagReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update tag tags: - Tag /answer/api/v1/tag/merge: post: consumes: - application/json description: merge tag parameters: - description: tag in: body name: data required: true schema: $ref: '#/definitions/schema.AddTagReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: merge tag tags: - Tag /answer/api/v1/tag/recover: post: consumes: - application/json description: recover delete tag parameters: - description: tag in: body name: data required: true schema: $ref: '#/definitions/schema.RecoverTagReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: recover delete tag tags: - Tag /answer/api/v1/tag/synonym: put: consumes: - application/json description: update tag parameters: - description: tag in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateTagSynonymReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update tag tags: - Tag /answer/api/v1/tag/synonyms: get: description: get tag synonyms parameters: - description: tag id in: query name: tag_id required: true type: integer produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetTagSynonymsResp' type: object summary: get tag synonyms tags: - Tag /answer/api/v1/tags: get: description: get tags list by slug name parameters: - collectionFormat: csv description: string collection in: query items: type: string name: tags type: array produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetTagBasicResp' type: array type: object summary: get tags list tags: - Tag /answer/api/v1/tags/following: get: description: get following tag list produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetFollowingTagsResp' type: array type: object security: - ApiKeyAuth: [] summary: get following tag list tags: - Tag /answer/api/v1/tags/page: get: description: get tag page parameters: - description: page size in: query name: page type: integer - description: page size in: query name: page_size type: integer - description: slug_name in: query name: slug_name type: string - description: query condition enum: - popular - name - newest in: query name: query_cond type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: allOf: - $ref: '#/definitions/pager.PageModel' - properties: list: items: $ref: '#/definitions/schema.GetTagPageResp' type: array type: object type: object summary: get tag page tags: - Tag /answer/api/v1/user/action/record: get: description: ActionRecord parameters: - description: action enum: - login - e_mail - find_pass in: query name: action required: true type: string responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.ActionRecordResp' type: object security: - ApiKeyAuth: [] summary: ActionRecord tags: - User /answer/api/v1/user/email: put: consumes: - application/json description: user change email verification parameters: - description: UserChangeEmailVerifyReq in: body name: data required: true schema: $ref: '#/definitions/schema.UserChangeEmailVerifyReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: user change email verification tags: - User /answer/api/v1/user/email/change/code: post: consumes: - application/json description: send email to the user email then change their email parameters: - description: UserChangeEmailSendCodeReq in: body name: data required: true schema: $ref: '#/definitions/schema.UserChangeEmailSendCodeReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: send email to the user email then change their email tags: - User /answer/api/v1/user/email/verification: post: consumes: - application/json description: UserVerifyEmail parameters: - default: "" description: code in: query name: code required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.UserLoginResp' type: object summary: UserVerifyEmail tags: - User /answer/api/v1/user/email/verification/send: post: consumes: - application/json description: UserVerifyEmailSend parameters: - default: "" description: captcha_id in: query name: captcha_id type: string - default: "" description: captcha_code in: query name: captcha_code type: string produces: - application/json responses: "200": description: OK schema: type: string security: - ApiKeyAuth: [] summary: UserVerifyEmailSend tags: - User /answer/api/v1/user/info: get: consumes: - application/json description: get user info, if user no login response http code is 200, but user info is null produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetCurrentLoginUserInfoResp' type: object security: - ApiKeyAuth: [] summary: GetUserInfoByUserID tags: - User put: consumes: - application/json description: UserUpdateInfo update user info parameters: - description: access-token in: header name: Authorization required: true type: string - description: UpdateInfoRequest in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateInfoRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: UserUpdateInfo update user info tags: - User /answer/api/v1/user/info/search: get: consumes: - application/json description: SearchUserListByName parameters: - description: username in: query name: username required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetOtherUserInfoResp' type: object security: - ApiKeyAuth: [] summary: SearchUserListByName tags: - User /answer/api/v1/user/interface: put: consumes: - application/json description: UserUpdateInterface update user interface config parameters: - description: access-token in: header name: Authorization required: true type: string - description: UpdateInfoRequest in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateUserInterfaceRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: UserUpdateInterface update user interface config tags: - User /answer/api/v1/user/login/email: post: consumes: - application/json description: UserEmailLogin parameters: - description: UserEmailLogin in: body name: data required: true schema: $ref: '#/definitions/schema.UserEmailLoginReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.UserLoginResp' type: object summary: UserEmailLogin tags: - User /answer/api/v1/user/logout: get: consumes: - application/json description: user logout produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: user logout tags: - User /answer/api/v1/user/notification/config: post: consumes: - application/json description: get user's notification config produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetUserNotificationConfigResp' type: object security: - ApiKeyAuth: [] summary: get user's notification config tags: - User put: consumes: - application/json description: update user's notification config parameters: - description: UpdateUserNotificationConfigReq in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateUserNotificationConfigReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update user's notification config tags: - User /answer/api/v1/user/notification/unsubscribe: put: consumes: - application/json description: unsubscribe notification parameters: - description: UserUnsubscribeNotificationReq in: body name: data required: true schema: $ref: '#/definitions/schema.UserUnsubscribeNotificationReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' summary: unsubscribe notification tags: - User /answer/api/v1/user/password: put: consumes: - application/json description: UserModifyPassWord parameters: - description: UserModifyPasswordReq in: body name: data required: true schema: $ref: '#/definitions/schema.UserModifyPasswordReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: UserModifyPassWord tags: - User /answer/api/v1/user/password/replacement: post: consumes: - application/json description: UseRePassWord parameters: - description: UserRePassWordRequest in: body name: data required: true schema: $ref: '#/definitions/schema.UserRePassWordRequest' produces: - application/json responses: "200": description: OK schema: type: string summary: UseRePassWord tags: - User /answer/api/v1/user/password/reset: post: consumes: - application/json description: RetrievePassWord parameters: - description: UserRetrievePassWordRequest in: body name: data required: true schema: $ref: '#/definitions/schema.UserRetrievePassWordRequest' produces: - application/json responses: "200": description: OK schema: type: string summary: RetrievePassWord tags: - User /answer/api/v1/user/plugin/config: get: description: get user plugin config parameters: - description: plugin_slug_name in: query name: plugin_slug_name required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetPluginConfigResp' type: object security: - ApiKeyAuth: [] summary: get user plugin config tags: - UserPlugin put: consumes: - application/json description: update user plugin config parameters: - description: UpdatePluginConfigReq in: body name: data required: true schema: $ref: '#/definitions/schema.UpdateUserPluginConfigReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: update user plugin config tags: - UserPlugin /answer/api/v1/user/plugin/configs: get: consumes: - application/json description: get plugin list that used for user. produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/schema.GetUserPluginListResp' type: array type: object security: - ApiKeyAuth: [] summary: get plugin list that used for user. tags: - UserPlugin /answer/api/v1/user/ranking: get: consumes: - application/json description: get user ranking produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.UserRankingResp' type: object summary: get user ranking tags: - User /answer/api/v1/user/register/email: post: consumes: - application/json description: UserRegisterByEmail parameters: - description: UserRegisterReq in: body name: data required: true schema: $ref: '#/definitions/schema.UserRegisterReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.UserLoginResp' type: object summary: UserRegisterByEmail tags: - User /answer/api/v1/user/staff: get: consumes: - application/json description: get user staff parameters: - description: username in: query name: username required: true type: string - description: page_size in: query name: page_size required: true type: string produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.GetUserStaffResp' type: object summary: get user staff tags: - User /answer/api/v1/vote/down: post: consumes: - application/json description: add vote parameters: - description: vote in: body name: data required: true schema: $ref: '#/definitions/schema.VoteReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.VoteResp' type: object security: - ApiKeyAuth: [] summary: vote down tags: - Activity /answer/api/v1/vote/up: post: consumes: - application/json description: add vote parameters: - description: vote in: body name: data required: true schema: $ref: '#/definitions/schema.VoteReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/schema.VoteResp' type: object security: - ApiKeyAuth: [] summary: vote up tags: - Activity /custom.css: get: description: get site custom CSS produces: - text/css responses: "200": description: OK schema: type: string summary: get site custom CSS tags: - site /installation/base-info: post: consumes: - application/json description: init base info parameters: - description: InitBaseInfoReq in: body name: data required: true schema: $ref: '#/definitions/install.InitBaseInfoReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' summary: init base info tags: - installation /installation/config-file/check: post: consumes: - application/json description: check config file if exist when installation produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/install.CheckConfigFileResp' type: object summary: check config file if exist when installation tags: - installation /installation/db/check: post: consumes: - application/json description: check database if exist when installation parameters: - description: CheckDatabaseReq in: body name: data required: true schema: $ref: '#/definitions/install.CheckDatabaseReq' produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: $ref: '#/definitions/install.CheckConfigFileResp' type: object summary: check database if exist when installation tags: - installation /installation/init: post: consumes: - application/json description: init environment parameters: - description: CheckDatabaseReq in: body name: data required: true schema: $ref: '#/definitions/install.CheckDatabaseReq' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' summary: init environment tags: - installation /installation/language/config: get: description: get installation language config mapping parameters: - description: installation language in: query name: lang required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' summary: get installation language config mapping tags: - Lang /installation/language/options: get: description: get installation language options produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/handler.RespBody' - properties: data: items: $ref: '#/definitions/translator.LangOption' type: array type: object summary: get installation language options tags: - Lang /personal/question/page: get: consumes: - application/json description: list personal questions parameters: - default: string description: username in: query name: username required: true type: string - description: order enum: - newest - score in: query name: order required: true type: string - default: "0" description: page in: query name: page required: true type: string - default: "20" description: page_size in: query name: page_size required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] summary: list personal questions tags: - Personal /robots.txt: get: description: get site robots information produces: - application/json responses: "200": description: OK schema: type: string summary: get site robots information tags: - site securityDefinitions: ApiKeyAuth: in: header name: Authorization type: apiKey swagger: "2.0" ================================================ FILE: go.mod ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. module github.com/apache/answer go 1.24.0 require ( github.com/Machiel/slugify v1.0.1 github.com/Masterminds/semver/v3 v3.3.0 github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/bwmarrin/snowflake v0.3.0 github.com/disintegration/imaging v1.6.2 github.com/gin-gonic/gin v1.10.0 github.com/go-playground/locales v0.14.1 github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/validator/v10 v10.22.1 github.com/go-resty/resty/v2 v2.17.1 github.com/go-sql-driver/mysql v1.8.1 github.com/goccy/go-json v0.10.3 github.com/google/uuid v1.6.0 github.com/google/wire v0.5.0 github.com/grokify/html-strip-tags-go v0.1.0 github.com/jinzhu/copier v0.4.0 github.com/jinzhu/now v1.1.5 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 github.com/mark3labs/mcp-go v0.43.2 github.com/microcosm-cc/bluemonday v1.0.27 github.com/mozillazg/go-pinyin v0.20.0 github.com/ory/dockertest/v3 v3.11.0 github.com/robfig/cron/v3 v3.0.1 github.com/sashabaranov/go-openai v1.41.2 github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405 github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f github.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.3 github.com/tidwall/gjson v1.17.3 github.com/yuin/goldmark v1.7.4 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.41.0 golang.org/x/image v0.20.0 golang.org/x/net v0.43.0 golang.org/x/term v0.34.0 golang.org/x/text v0.28.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.33.0 xorm.io/builder v0.3.13 xorm.io/xorm v1.3.2 ) require ( dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/sonic v1.12.2 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/containerd/continuity v0.4.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v27.2.1+incompatible // indirect github.com/docker/docker v27.2.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 // indirect github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.5 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-viper/mapstructure/v2 v2.1.0 // indirect github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect github.com/lestrrat-go/strftime v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.1.14 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.10.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/tools v0.36.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect modernc.org/strutil v1.2.0 // indirect modernc.org/token v1.1.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) replace lukechampine.com/uint128 v1.1.1 => github.com/aichy126/uint128 v1.1.1 replace modernc.org/cc/v3 v3.40.0 => gitlab.com/cznic/cc/v3 v3.40.0 replace github.com/lyft/protoc-gen-validate v0.0.13 => github.com/LinkinStars/protoc-gen-validate v0.0.0-20251030022322-3fddbbe5a0e6 ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= github.com/LinkinStars/protoc-gen-validate v0.0.0-20251030022322-3fddbbe5a0e6/go.mod h1:Lu7LbM9PBAPmasRqVew2kylj56Z1vH/UUM2REVkLh7k= github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E= github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/aichy126/uint128 v1.1.1/go.mod h1:Hke/MPGXUxOl0OXHoNcVesBL4N+XalHEJ9e1jaIbl8o= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267 h1:vDHsaEcs/Q0dwetADENtwus6W1ccaZ9h3KBTm0d2X0g= github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267/go.mod h1:Yj3yPP/vi87JjwylUTCMyd6FrOfGqP1AHk0305hDm2o= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 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/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIrnyWmSJI70= github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 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/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f/go.mod h1:DmMpU91/Ax6BAwoRkjgRCr2rmgEgS4tsmatfV7M+U+c= github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d h1:ygcRCGNKuEiA98k7X35hknEN8RIRUF1jrz7k1rZCvsk= github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs= github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs= github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc= github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8= github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8= github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= github.com/dsoprea/go-jpeg-image-structure v0.0.0-20190422055009-d6f9ba25cf48/go.mod h1:H1hAaFyv9cRV1ywoHvaqVoNSThBvWZ0JarRBcV+FSnE= github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 h1:P1dsxzctGkmG6Zf7gH2xrZhNXWP5/FuLDI7xbCGsWTo= github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102/go.mod h1:6+tQXZ+I62x13UZ+hemLVoZIuq/usVzvau7bqwUo9P0= github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA= github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg= github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d h1:dg6UMHa50VI01WuPWXPbNJpO8QSyvIF5T5n2IZiqX3A= github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= github.com/dsoprea/go-png-image-structure v0.0.0-20190624104353-c9b28dcdc5c8/go.mod h1:Bf0nmcDFFRQBjZwr9qY6c0zTxKQa+Q8YWZmlYxXGxY0= github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA= github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw= github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 h1:/py11NlxDaOxkT9OKN+gXgT+QOH5xj1ZRoyusfRIlo4= github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo= github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/geo v0.0.0-20190812012225-f41920e961ce/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE= github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc= github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= github.com/lestrrat-go/strftime v1.1.0 h1:gMESpZy44/4pXLO/m+sL0yBd1W6LjgjrrD4a68Gapyg= github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/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.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ= github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/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.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM= github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405 h1:2ieGkj4z/YPXVyQ2ayZUg3GwE1pYWd5f1RB6DzAOXKM= github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405/go.mod h1:rIxVzVLKlBwLxO+lC+k/I4HJfRQcemg/f/76Xmmzsec= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f h1:1KHe0uN6p798E7XJZPhZkgm/hXk5CTjisCvFMqaZSKI= github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f h1:/nA4C3UfWw+3XYVBkgVMY1p3nX3uhl22hL2LW3FNcVs= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f h1:xia6AXJor4UV4T6htmHlfN7CGXZ04vlWwybVtFKJ/mA= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f h1:0mrzVRrQ+mz5MWQSdC1y6dwKWiewYKkpRDqNf3nOhmk= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f h1:2gjiRmSj3J/F3A1A22UU1BzO4gQypEZx/4D7c7Ue4Ag= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/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/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4= modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M= modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8= modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk= modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM= xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= ================================================ FILE: i18n/af_ZA.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. email: other: Email password: other: Password email_or_password_wrong_error: other: Email and password do not match. error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: not_found: other: Tag not found. recommend_tag_not_found: other: Recommend Tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: other: The From Name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to Revision. user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. report: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude: name: other: rude or abusive desc: other: A reasonable person would find this content inappropriate for respectful discourse. duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. not_answer: name: other: not an answer desc: other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. not_need: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. other: name: other: something else desc: other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comments tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to {{site_name}} btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to {{site_name}} success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as <link> head: label: Head text: This will insert before </head> header: label: Header text: This will insert after <body> footer: label: Footer text: This will insert before </body>. login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/ar_SA.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. email: other: Email password: other: Password email_or_password_wrong_error: other: Email and password do not match. error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: not_found: other: Tag not found. recommend_tag_not_found: other: Recommend Tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: other: The From Name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to Revision. user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. report: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude: name: other: rude or abusive desc: other: A reasonable person would find this content inappropriate for respectful discourse. duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. not_answer: name: other: not an answer desc: other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. not_need: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. other: name: other: something else desc: other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comments tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to {{site_name}} btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to {{site_name}} success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/az_AZ.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: "Success." unknown: other: "Unknown error." request_format_error: other: "Request format is not valid." unauthorized_error: other: "Unauthorized." database_error: other: "Data server error." role: name: user: other: "User" admin: other: "Admin" moderator: other: "Moderator" description: user: other: "Default with no special access." admin: other: "Have the full power to access the site." moderator: other: "Has access to all posts except admin settings." email: other: "Email" password: other: "Password" email_or_password_wrong_error: other: "Email and password do not match." error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: "Answer do not found." cannot_deleted: other: "No permission to delete." cannot_update: other: "No permission to update." comment: edit_without_permission: other: "Comment are not allowed to edit." not_found: other: "Comment not found." email: duplicate: other: "Email already exists." need_to_be_verified: other: "Email should be verified." verify_url_expired: other: "Email verified URL has expired, please resend the email." lang: not_found: other: "Language file not found." object: captcha_verification_failed: other: "Captcha wrong." disallow_follow: other: "You are not allowed to follow." disallow_vote: other: "You are not allowed to vote." disallow_vote_your_self: other: "You can't vote for your own post." not_found: other: "Object not found." verification_failed: other: "Verification failed." email_or_password_incorrect: other: "Email and password do not match." old_password_verification_failed: other: "The old password verification failed" new_password_same_as_previous_setting: other: "The new password is the same as the previous one." question: not_found: other: "Question not found." cannot_deleted: other: "No permission to delete." cannot_close: other: "No permission to close." cannot_update: other: "No permission to update." rank: fail_to_meet_the_condition: other: "Rank fail to meet the condition." report: handle_failed: other: "Report handle failed." not_found: other: "Report not found." tag: not_found: other: "Tag not found." recommend_tag_not_found: other: "Recommend Tag is not exist." recommend_tag_enter: other: "Please enter at least one required tag." not_contain_synonym_tags: other: "Should not contain synonym tags." cannot_update: other: "No permission to update." cannot_set_synonym_as_itself: other: "You cannot set the synonym of the current tag as itself." smtp: config_from_name_cannot_be_email: other: "The From Name cannot be a email address." theme: not_found: other: "Theme not found." revision: review_underway: other: "Can't edit currently, there is a version in the review queue." no_permission: other: "No permission to Revision." user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: "User not found." suspended: other: "User has been suspended." username_invalid: other: "Username is invalid." username_duplicate: other: "Username is already in use." set_avatar: other: "Avatar set failed." cannot_update_your_role: other: "You cannot modify your role." not_allowed_registration: other: "Currently the site is not open for registration" config: read_config_failed: other: "Read config failed" database: connection_failed: other: "Database connection failed" create_table_failed: other: "Create table failed" install: create_config_failed: other: "Can't create the config.yaml file." report: spam: name: other: "spam" desc: other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." rude: name: other: "rude or abusive" desc: other: "A reasonable person would find this content inappropriate for respectful discourse." duplicate: name: other: "a duplicate" desc: other: "This question has been asked before and already has an answer." not_answer: name: other: "not an answer" desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." not_need: name: other: "no longer needed" desc: other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" desc: other: "This post requires staff attention for another reason not listed above." question: close: duplicate: name: other: "spam" desc: other: "This question has been asked before and already has an answer." guideline: name: other: "a community-specific reason" desc: other: "This question doesn't meet a community guideline." multiple: name: other: "needs details or clarity" desc: other: "This question currently includes multiple questions in one. It should focus on one problem only." other: name: other: "something else" desc: other: "This post requires another reason not listed above." operation_type: asked: other: "asked" answered: other: "answered" modified: other: "modified" notification: action: update_question: other: "updated question" answer_the_question: other: "answered question" update_answer: other: "updated answer" accept_answer: other: "accepted answer" comment_question: other: "commented question" comment_answer: other: "commented answer" reply_to_you: other: "replied to you" mention_you: other: "mentioned you" your_question_is_closed: other: "Your question has been closed" your_question_was_deleted: other: "Your question has been deleted" your_answer_was_deleted: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comment tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to Answer btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name up to 30 characters username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username up to 30 characters character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to Answer success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: "After you've done that, click “Next” button." site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: display_name must be at 2 - 30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/bal_BA.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: "Success." unknown: other: "Unknown error." request_format_error: other: "Request format is not valid." unauthorized_error: other: "Unauthorized." database_error: other: "Data server error." role: name: user: other: "User" admin: other: "Admin" moderator: other: "Moderator" description: user: other: "Default with no special access." admin: other: "Have the full power to access the site." moderator: other: "Has access to all posts except admin settings." email: other: "Email" password: other: "Password" email_or_password_wrong_error: other: "Email and password do not match." error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: "Answer do not found." cannot_deleted: other: "No permission to delete." cannot_update: other: "No permission to update." comment: edit_without_permission: other: "Comment are not allowed to edit." not_found: other: "Comment not found." email: duplicate: other: "Email already exists." need_to_be_verified: other: "Email should be verified." verify_url_expired: other: "Email verified URL has expired, please resend the email." lang: not_found: other: "Language file not found." object: captcha_verification_failed: other: "Captcha wrong." disallow_follow: other: "You are not allowed to follow." disallow_vote: other: "You are not allowed to vote." disallow_vote_your_self: other: "You can't vote for your own post." not_found: other: "Object not found." verification_failed: other: "Verification failed." email_or_password_incorrect: other: "Email and password do not match." old_password_verification_failed: other: "The old password verification failed" new_password_same_as_previous_setting: other: "The new password is the same as the previous one." question: not_found: other: "Question not found." cannot_deleted: other: "No permission to delete." cannot_close: other: "No permission to close." cannot_update: other: "No permission to update." rank: fail_to_meet_the_condition: other: "Rank fail to meet the condition." report: handle_failed: other: "Report handle failed." not_found: other: "Report not found." tag: not_found: other: "Tag not found." recommend_tag_not_found: other: "Recommend Tag is not exist." recommend_tag_enter: other: "Please enter at least one required tag." not_contain_synonym_tags: other: "Should not contain synonym tags." cannot_update: other: "No permission to update." cannot_set_synonym_as_itself: other: "You cannot set the synonym of the current tag as itself." smtp: config_from_name_cannot_be_email: other: "The From Name cannot be a email address." theme: not_found: other: "Theme not found." revision: review_underway: other: "Can't edit currently, there is a version in the review queue." no_permission: other: "No permission to Revision." user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: "User not found." suspended: other: "User has been suspended." username_invalid: other: "Username is invalid." username_duplicate: other: "Username is already in use." set_avatar: other: "Avatar set failed." cannot_update_your_role: other: "You cannot modify your role." not_allowed_registration: other: "Currently the site is not open for registration" config: read_config_failed: other: "Read config failed" database: connection_failed: other: "Database connection failed" create_table_failed: other: "Create table failed" install: create_config_failed: other: "Can't create the config.yaml file." report: spam: name: other: "spam" desc: other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." rude: name: other: "rude or abusive" desc: other: "A reasonable person would find this content inappropriate for respectful discourse." duplicate: name: other: "a duplicate" desc: other: "This question has been asked before and already has an answer." not_answer: name: other: "not an answer" desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." not_need: name: other: "no longer needed" desc: other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" desc: other: "This post requires staff attention for another reason not listed above." question: close: duplicate: name: other: "spam" desc: other: "This question has been asked before and already has an answer." guideline: name: other: "a community-specific reason" desc: other: "This question doesn't meet a community guideline." multiple: name: other: "needs details or clarity" desc: other: "This question currently includes multiple questions in one. It should focus on one problem only." other: name: other: "something else" desc: other: "This post requires another reason not listed above." operation_type: asked: other: "asked" answered: other: "answered" modified: other: "modified" notification: action: update_question: other: "updated question" answer_the_question: other: "answered question" update_answer: other: "updated answer" accept_answer: other: "accepted answer" comment_question: other: "commented question" comment_answer: other: "commented answer" reply_to_you: other: "replied to you" mention_you: other: "mentioned you" your_question_is_closed: other: "Your question has been closed" your_question_was_deleted: other: "Your question has been deleted" your_answer_was_deleted: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comment tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to Answer btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name up to 30 characters username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username up to 30 characters character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to Answer success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: "After you've done that, click “Next” button." site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: display_name must be at 2 - 30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/ban_ID.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: "Success." unknown: other: "Unknown error." request_format_error: other: "Request format is not valid." unauthorized_error: other: "Unauthorized." database_error: other: "Data server error." role: name: user: other: "User" admin: other: "Admin" moderator: other: "Moderator" description: user: other: "Default with no special access." admin: other: "Have the full power to access the site." moderator: other: "Has access to all posts except admin settings." email: other: "Email" password: other: "Password" email_or_password_wrong_error: other: "Email and password do not match." error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: "Answer do not found." cannot_deleted: other: "No permission to delete." cannot_update: other: "No permission to update." comment: edit_without_permission: other: "Comment are not allowed to edit." not_found: other: "Comment not found." email: duplicate: other: "Email already exists." need_to_be_verified: other: "Email should be verified." verify_url_expired: other: "Email verified URL has expired, please resend the email." lang: not_found: other: "Language file not found." object: captcha_verification_failed: other: "Captcha wrong." disallow_follow: other: "You are not allowed to follow." disallow_vote: other: "You are not allowed to vote." disallow_vote_your_self: other: "You can't vote for your own post." not_found: other: "Object not found." verification_failed: other: "Verification failed." email_or_password_incorrect: other: "Email and password do not match." old_password_verification_failed: other: "The old password verification failed" new_password_same_as_previous_setting: other: "The new password is the same as the previous one." question: not_found: other: "Question not found." cannot_deleted: other: "No permission to delete." cannot_close: other: "No permission to close." cannot_update: other: "No permission to update." rank: fail_to_meet_the_condition: other: "Rank fail to meet the condition." report: handle_failed: other: "Report handle failed." not_found: other: "Report not found." tag: not_found: other: "Tag not found." recommend_tag_not_found: other: "Recommend Tag is not exist." recommend_tag_enter: other: "Please enter at least one required tag." not_contain_synonym_tags: other: "Should not contain synonym tags." cannot_update: other: "No permission to update." cannot_set_synonym_as_itself: other: "You cannot set the synonym of the current tag as itself." smtp: config_from_name_cannot_be_email: other: "The From Name cannot be a email address." theme: not_found: other: "Theme not found." revision: review_underway: other: "Can't edit currently, there is a version in the review queue." no_permission: other: "No permission to Revision." user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: "User not found." suspended: other: "User has been suspended." username_invalid: other: "Username is invalid." username_duplicate: other: "Username is already in use." set_avatar: other: "Avatar set failed." cannot_update_your_role: other: "You cannot modify your role." not_allowed_registration: other: "Currently the site is not open for registration" config: read_config_failed: other: "Read config failed" database: connection_failed: other: "Database connection failed" create_table_failed: other: "Create table failed" install: create_config_failed: other: "Can't create the config.yaml file." report: spam: name: other: "spam" desc: other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." rude: name: other: "rude or abusive" desc: other: "A reasonable person would find this content inappropriate for respectful discourse." duplicate: name: other: "a duplicate" desc: other: "This question has been asked before and already has an answer." not_answer: name: other: "not an answer" desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." not_need: name: other: "no longer needed" desc: other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" desc: other: "This post requires staff attention for another reason not listed above." question: close: duplicate: name: other: "spam" desc: other: "This question has been asked before and already has an answer." guideline: name: other: "a community-specific reason" desc: other: "This question doesn't meet a community guideline." multiple: name: other: "needs details or clarity" desc: other: "This question currently includes multiple questions in one. It should focus on one problem only." other: name: other: "something else" desc: other: "This post requires another reason not listed above." operation_type: asked: other: "asked" answered: other: "answered" modified: other: "modified" notification: action: update_question: other: "updated question" answer_the_question: other: "answered question" update_answer: other: "updated answer" accept_answer: other: "accepted answer" comment_question: other: "commented question" comment_answer: other: "commented answer" reply_to_you: other: "replied to you" mention_you: other: "mentioned you" your_question_is_closed: other: "Your question has been closed" your_question_was_deleted: other: "Your question has been deleted" your_answer_was_deleted: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comment tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to Answer btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name up to 30 characters username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username up to 30 characters character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to Answer success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: "After you've done that, click “Next” button." site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: display_name must be at 2 - 30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/bn_BD.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: "Success." unknown: other: "Unknown error." request_format_error: other: "Request format is not valid." unauthorized_error: other: "Unauthorized." database_error: other: "Data server error." role: name: user: other: "User" admin: other: "Admin" moderator: other: "Moderator" description: user: other: "Default with no special access." admin: other: "Have the full power to access the site." moderator: other: "Has access to all posts except admin settings." email: other: "Email" password: other: "Password" email_or_password_wrong_error: other: "Email and password do not match." error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: "Answer do not found." cannot_deleted: other: "No permission to delete." cannot_update: other: "No permission to update." comment: edit_without_permission: other: "Comment are not allowed to edit." not_found: other: "Comment not found." email: duplicate: other: "Email already exists." need_to_be_verified: other: "Email should be verified." verify_url_expired: other: "Email verified URL has expired, please resend the email." lang: not_found: other: "Language file not found." object: captcha_verification_failed: other: "Captcha wrong." disallow_follow: other: "You are not allowed to follow." disallow_vote: other: "You are not allowed to vote." disallow_vote_your_self: other: "You can't vote for your own post." not_found: other: "Object not found." verification_failed: other: "Verification failed." email_or_password_incorrect: other: "Email and password do not match." old_password_verification_failed: other: "The old password verification failed" new_password_same_as_previous_setting: other: "The new password is the same as the previous one." question: not_found: other: "Question not found." cannot_deleted: other: "No permission to delete." cannot_close: other: "No permission to close." cannot_update: other: "No permission to update." rank: fail_to_meet_the_condition: other: "Rank fail to meet the condition." report: handle_failed: other: "Report handle failed." not_found: other: "Report not found." tag: not_found: other: "Tag not found." recommend_tag_not_found: other: "Recommend Tag is not exist." recommend_tag_enter: other: "Please enter at least one required tag." not_contain_synonym_tags: other: "Should not contain synonym tags." cannot_update: other: "No permission to update." cannot_set_synonym_as_itself: other: "You cannot set the synonym of the current tag as itself." smtp: config_from_name_cannot_be_email: other: "The From Name cannot be a email address." theme: not_found: other: "Theme not found." revision: review_underway: other: "Can't edit currently, there is a version in the review queue." no_permission: other: "No permission to Revision." user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: "User not found." suspended: other: "User has been suspended." username_invalid: other: "Username is invalid." username_duplicate: other: "Username is already in use." set_avatar: other: "Avatar set failed." cannot_update_your_role: other: "You cannot modify your role." not_allowed_registration: other: "Currently the site is not open for registration" config: read_config_failed: other: "Read config failed" database: connection_failed: other: "Database connection failed" create_table_failed: other: "Create table failed" install: create_config_failed: other: "Can't create the config.yaml file." report: spam: name: other: "spam" desc: other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." rude: name: other: "rude or abusive" desc: other: "A reasonable person would find this content inappropriate for respectful discourse." duplicate: name: other: "a duplicate" desc: other: "This question has been asked before and already has an answer." not_answer: name: other: "not an answer" desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." not_need: name: other: "no longer needed" desc: other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" desc: other: "This post requires staff attention for another reason not listed above." question: close: duplicate: name: other: "spam" desc: other: "This question has been asked before and already has an answer." guideline: name: other: "a community-specific reason" desc: other: "This question doesn't meet a community guideline." multiple: name: other: "needs details or clarity" desc: other: "This question currently includes multiple questions in one. It should focus on one problem only." other: name: other: "something else" desc: other: "This post requires another reason not listed above." operation_type: asked: other: "asked" answered: other: "answered" modified: other: "modified" notification: action: update_question: other: "updated question" answer_the_question: other: "answered question" update_answer: other: "updated answer" accept_answer: other: "accepted answer" comment_question: other: "commented question" comment_answer: other: "commented answer" reply_to_you: other: "replied to you" mention_you: other: "mentioned you" your_question_is_closed: other: "Your question has been closed" your_question_was_deleted: other: "Your question has been deleted" your_answer_was_deleted: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comment tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to Answer btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name up to 30 characters username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username up to 30 characters character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to Answer success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: "After you've done that, click “Next” button." site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: display_name must be at 2 - 30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/bs_BA.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: "Success." unknown: other: "Unknown error." request_format_error: other: "Request format is not valid." unauthorized_error: other: "Unauthorized." database_error: other: "Data server error." role: name: user: other: "User" admin: other: "Admin" moderator: other: "Moderator" description: user: other: "Default with no special access." admin: other: "Have the full power to access the site." moderator: other: "Has access to all posts except admin settings." email: other: "Email" password: other: "Password" email_or_password_wrong_error: other: "Email and password do not match." error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: "Answer do not found." cannot_deleted: other: "No permission to delete." cannot_update: other: "No permission to update." comment: edit_without_permission: other: "Comment are not allowed to edit." not_found: other: "Comment not found." email: duplicate: other: "Email already exists." need_to_be_verified: other: "Email should be verified." verify_url_expired: other: "Email verified URL has expired, please resend the email." lang: not_found: other: "Language file not found." object: captcha_verification_failed: other: "Captcha wrong." disallow_follow: other: "You are not allowed to follow." disallow_vote: other: "You are not allowed to vote." disallow_vote_your_self: other: "You can't vote for your own post." not_found: other: "Object not found." verification_failed: other: "Verification failed." email_or_password_incorrect: other: "Email and password do not match." old_password_verification_failed: other: "The old password verification failed" new_password_same_as_previous_setting: other: "The new password is the same as the previous one." question: not_found: other: "Question not found." cannot_deleted: other: "No permission to delete." cannot_close: other: "No permission to close." cannot_update: other: "No permission to update." rank: fail_to_meet_the_condition: other: "Rank fail to meet the condition." report: handle_failed: other: "Report handle failed." not_found: other: "Report not found." tag: not_found: other: "Tag not found." recommend_tag_not_found: other: "Recommend Tag is not exist." recommend_tag_enter: other: "Please enter at least one required tag." not_contain_synonym_tags: other: "Should not contain synonym tags." cannot_update: other: "No permission to update." cannot_set_synonym_as_itself: other: "You cannot set the synonym of the current tag as itself." smtp: config_from_name_cannot_be_email: other: "The From Name cannot be a email address." theme: not_found: other: "Theme not found." revision: review_underway: other: "Can't edit currently, there is a version in the review queue." no_permission: other: "No permission to Revision." user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: "User not found." suspended: other: "User has been suspended." username_invalid: other: "Username is invalid." username_duplicate: other: "Username is already in use." set_avatar: other: "Avatar set failed." cannot_update_your_role: other: "You cannot modify your role." not_allowed_registration: other: "Currently the site is not open for registration" config: read_config_failed: other: "Read config failed" database: connection_failed: other: "Database connection failed" create_table_failed: other: "Create table failed" install: create_config_failed: other: "Can't create the config.yaml file." report: spam: name: other: "spam" desc: other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." rude: name: other: "rude or abusive" desc: other: "A reasonable person would find this content inappropriate for respectful discourse." duplicate: name: other: "a duplicate" desc: other: "This question has been asked before and already has an answer." not_answer: name: other: "not an answer" desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." not_need: name: other: "no longer needed" desc: other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" desc: other: "This post requires staff attention for another reason not listed above." question: close: duplicate: name: other: "spam" desc: other: "This question has been asked before and already has an answer." guideline: name: other: "a community-specific reason" desc: other: "This question doesn't meet a community guideline." multiple: name: other: "needs details or clarity" desc: other: "This question currently includes multiple questions in one. It should focus on one problem only." other: name: other: "something else" desc: other: "This post requires another reason not listed above." operation_type: asked: other: "asked" answered: other: "answered" modified: other: "modified" notification: action: update_question: other: "updated question" answer_the_question: other: "answered question" update_answer: other: "updated answer" accept_answer: other: "accepted answer" comment_question: other: "commented question" comment_answer: other: "commented answer" reply_to_you: other: "replied to you" mention_you: other: "mentioned you" your_question_is_closed: other: "Your question has been closed" your_question_was_deleted: other: "Your question has been deleted" your_answer_was_deleted: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comment tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to Answer btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name up to 30 characters username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username up to 30 characters character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to Answer success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: "After you've done that, click “Next” button." site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: display_name must be at 2 - 30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/ca_ES.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. email: other: Email password: other: Password email_or_password_wrong_error: other: Email and password do not match. error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: not_found: other: Tag not found. recommend_tag_not_found: other: Recommend Tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: other: The From Name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to Revision. user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. report: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude: name: other: rude or abusive desc: other: A reasonable person would find this content inappropriate for respectful discourse. duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. not_answer: name: other: not an answer desc: other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. not_need: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. other: name: other: something else desc: other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comments tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to {{site_name}} btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to {{site_name}} success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/cs_CZ.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Úspěch. unknown: other: Neznámá chyba. request_format_error: other: Formát požadavku není platný. unauthorized_error: other: Neautorizováno. database_error: other: Chyba datového serveru. forbidden_error: other: Zakázáno. duplicate_request_error: other: Duplicitní odeslání. action: report: other: Nahlásit edit: other: Upravit delete: other: Smazat close: other: Zavřít reopen: other: Znovu otevřít forbidden_error: other: Zakázáno. pin: other: Připnout hide: other: Skrýt unpin: other: Odepnout show: other: Zobrazit invite_someone_to_answer: other: Upravit undelete: other: Obnovit merge: other: Sloučit role: name: user: other: Uživatel admin: other: Administrátor moderator: other: Moderátor description: user: other: Výchozí bez zvláštního přístupu. admin: other: Má plnou kontrolu nad stránkou. moderator: other: Má přístup ke všem příspěvkům kromě admin nastavení. privilege: level_1: description: other: Úroveň 1 (méně reputace je vyžadováno pro soukromý tým, skupinu) level_2: description: other: Úroveň 2 (nízká reputace je vyžadována pro startovací komunitu) level_3: description: other: Úroveň 3 (vysoká reputace je vyžadována pro vyspělou komunitu) level_custom: description: other: Vlastní úroveň rank_question_add_label: other: Položit dotaz rank_answer_add_label: other: Napsat odpověď rank_comment_add_label: other: Napsat komentář rank_report_add_label: other: Nahlásit rank_comment_vote_up_label: other: Hlasovat pro komentář rank_link_url_limit_label: other: Zveřejnit více než 2 odkazy najednou rank_question_vote_up_label: other: Hlasovat pro dotaz rank_answer_vote_up_label: other: Hlasovat pro odpověď rank_question_vote_down_label: other: Hlasovat proti otázce rank_answer_vote_down_label: other: Hlasovat proti odpovědi rank_invite_someone_to_answer_label: other: Pozvěte někoho, aby odpověděl rank_tag_add_label: other: Vytvořit nový štítek rank_tag_edit_label: other: Upravit popis štítku (vyžaduje kontrolu) rank_question_edit_label: other: Upravit dotaz někoho jiného (vyžaduje kontrolu) rank_answer_edit_label: other: Upravit odpověď někoho jiného (vyžaduje kontrolu) rank_question_edit_without_review_label: other: Upravit dotaz někoho jiného (bez kontroly) rank_answer_edit_without_review_label: other: Upravit odpověď někoho jiného (bez kontroly) rank_question_audit_label: other: Zkontrolovat úpravy dotazu rank_answer_audit_label: other: Zkontrolovat úpravy odpovědí rank_tag_audit_label: other: Zkontrolovat úpravy štítků rank_tag_edit_without_review_label: other: Upravit popis štítku (bez kontroly) rank_tag_synonym_label: other: Správa synonym štítků email: other: Email e_mail: other: Email password: other: Heslo pass: other: Heslo old_pass: other: Current password original_text: other: Tento příspěvek email_or_password_wrong_error: other: Email a heslo nesouhlasí. error: common: invalid_url: other: Neplatná URL. status_invalid: other: Neplatný stav. password: space_invalid: other: Heslo nesmí obsahovat mezery. admin: cannot_update_their_password: other: Nemůžete změnit své heslo. cannot_edit_their_profile: other: Nemůžete upravovat svůj profil. cannot_modify_self_status: other: Nemůžete změnit svůj stav. email_or_password_wrong: other: Email a heslo nesouhlasí. answer: not_found: other: Odpověď nebyla nalezena. cannot_deleted: other: Nemáte právo mazat. cannot_update: other: Nemáte právo aktualizovat. question_closed_cannot_add: other: Dotazy jsou uzavřené a není možno je přidávat. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Nejsou povoleny úpravy komentáře. not_found: other: Komentář nebyl nalezen. cannot_edit_after_deadline: other: Tento komentář byl pro úpravy příliš dlouhý. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: Email už existuje. need_to_be_verified: other: Email musí být ověřen. verify_url_expired: other: Platnost ověřovacího URL vypršela, pošlete si ověřovací email znovu. illegal_email_domain_error: other: Email z této domény není povolen. Použijte jinou doménu. lang: not_found: other: Jazykový soubor nenalezen. object: captcha_verification_failed: other: Nesprávně vyplněná Captcha. disallow_follow: other: Nemáte oprávnění sledovat. disallow_vote: other: Nemáte oprávnění hlasovat. disallow_vote_your_self: other: Nemůžete hlasovat pro svůj vlastní příspěvek. not_found: other: Objekt nenalezen. verification_failed: other: Ověření se nezdařilo. email_or_password_incorrect: other: Email a heslo nesouhlasí. old_password_verification_failed: other: Ověření starého hesla selhalo new_password_same_as_previous_setting: other: Nové heslo je stejné jako předchozí. already_deleted: other: Tento příspěvek byl odstraněn. meta: object_not_found: other: Meta objekt nenalezen question: already_deleted: other: Tento příspěvek byl odstraněn. under_review: other: Váš příspěvek čeká na kontrolu. Bude viditelný po jeho schválení. not_found: other: Dotaz nenalezen. cannot_deleted: other: Nemáte oprávnění k mazání. cannot_close: other: Nemáte oprávnění k uzavření. cannot_update: other: Nemáte oprávnění pro aktualizaci. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Hodnost reputace nesplňuje podmínku. vote_fail_to_meet_the_condition: other: Děkujeme za zpětnou vazbu. Potřebujete alespoň úroveň {{.Rank}}, abyste mohli hlasovat. no_enough_rank_to_operate: other: Potřebujete alespoň úroveň {{.Rank}} k provedení této akce. report: handle_failed: other: Report selhal. not_found: other: Report nebyl nalezen. tag: already_exist: other: Štítek již existuje. not_found: other: Štítek nebyl nalezen. recommend_tag_not_found: other: Doporučený štítek nebyl nalezen. recommend_tag_enter: other: Zadejte prosím alespoň jeden povinný štítek. not_contain_synonym_tags: other: Nemělo by obsahovat synonyma štítků. cannot_update: other: Nemáte oprávnění pro aktualizaci. is_used_cannot_delete: other: Nemůžete odstranit štítek, který se používá. cannot_set_synonym_as_itself: other: Aktuální štítek nelze jako synonymum stejného štítku. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: Jméno odesílatele nemůže být emailová adresa. theme: not_found: other: Motiv nebyl nalezen. revision: review_underway: other: V současné době nelze upravit, čeká na kontrolu. no_permission: other: Nemáte oprávnění k revizi. user: external_login_missing_user_id: other: Platforma třetí strany neposkytuje unikátní UserID, takže se nemůžete přihlásit, kontaktujte prosím správce webových stránek. external_login_unbinding_forbidden: other: Před odebráním tohoto typu přihlášení nastavte přihlašovací heslo pro svůj účet. email_or_password_wrong: other: other: Email a heslo nesouhlasí. not_found: other: Uživatel nebyl nalezen. suspended: other: Uživatelský účet byl pozastaven. username_invalid: other: Uživatelské jméno je neplatné. username_duplicate: other: Uživatelské jméno je již použito. set_avatar: other: Nastavení avataru se nezdařilo. cannot_update_your_role: other: Nemůžete upravovat svoji roli. not_allowed_registration: other: Registrace nejsou povolené. not_allowed_login_via_password: other: Přihlášení přes heslo není povolené. access_denied: other: Přístup zamítnut page_access_denied: other: Nemáte přístup k této stránce. add_bulk_users_format_error: other: "Chyba formátu pole {{.Field}} poblíž '{{.Content}}' na řádku {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Počet uživatelů, které přidáte najednou, by měl být v rozsahu 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Načtení konfigurace selhalo database: connection_failed: other: Spojení s databází selhalo create_table_failed: other: Vytvoření tabulky selhalo install: create_config_failed: other: Soubor config.yaml nelze vytvořit. upload: unsupported_file_format: other: Nepodporovaný formát souboru. site_info: config_not_found: other: Konfigurace webu nebyla nalezena. badge: object_not_found: other: Objekt odznaku nebyl nalezen reason: spam: name: other: spam desc: other: Tento příspěvek je reklama nebo vandalismus. Není užitečný ani relevantní pro aktuální téma. rude_or_abusive: name: other: hrubý nebo zneužívající desc: other: "Rozumný člověk by tento obsah považoval za nevhodný pro slušnou konverzaci." a_duplicate: name: other: duplicita desc: other: Tento dotaz byl položen dříve a již má odpověď. placeholder: other: Zadejte existující odkaz na dotaz not_a_answer: name: other: není odpověď desc: other: "Toto bylo zveřejněno jako odpověď, ale nesnaží se odpovědět na dotaz. Měla by to být úprava, komentář, nebo úplně jiný dotaz." no_longer_needed: name: other: již není potřeba desc: other: Tento komentář je zastaralý, konverzační nebo není relevantní pro tento příspěvek. something: name: other: jiný důvod desc: other: Tento příspěvek vyžaduje pozornost moderátorů z jiného důvodu, který není uveden výše. placeholder: other: Dejte nám vědět konkrétně, v čem je problém community_specific: name: other: důvod specifický pro komunitu desc: other: Tento dotaz nesplňuje pravidla komunity. not_clarity: name: other: vyžaduje detaily nebo upřesnění desc: other: Tento dotaz v současné době obsahuje více otázek. Měl by se zaměřit pouze na jeden problém. looks_ok: name: other: vypadá v pořádku desc: other: Tento příspěvek je dobrý tak jak je, nemá nízkou kvalitu. needs_edit: name: other: potřebuje úpravu, kterou jsem udělal(a) desc: other: Zlepšete a opravte problémy s tímto příspěvkem. needs_close: name: other: potřebuje zavřít desc: other: Na uzavřený dotaz není možné odpovídat, ale stále může být upraven a je možné pro něj hlasovat a komentovat jej. needs_delete: name: other: potřebuje smazat desc: other: Tento příspěvek bude odstraněn. question: close: duplicate: name: other: spam desc: other: Tento dotaz byl položena dříve a již má odpověď. guideline: name: other: důvod specifický pro komunitu desc: other: Tento dotaz nesplňuje pravidla komunity. multiple: name: other: vyžaduje detaily nebo upřesnění desc: other: Tento dotaz v současné době obsahuje více otázek. Měla by se zaměřit pouze na jeden problém. other: name: other: jiný důvod desc: other: Tento příspěvek vyžaduje pozornost moderátorů z jiného důvodu, který není uveden výše. operation_type: asked: other: dotázáno answered: other: zodpovězeno modified: other: upraveno deleted_title: other: Smazat dotaz questions_title: other: Dotazy tag: tags_title: other: Štítky no_description: other: Štítek nemá žádný popis. notification: action: update_question: other: upravený dotaz answer_the_question: other: položil(a) dotaz update_answer: other: upravil(a) odpověď accept_answer: other: přijal(a) odpověď comment_question: other: okomentoval(a) dotaz comment_answer: other: okomentoval(a) odpověď reply_to_you: other: vám odpověděl(a) mention_you: other: vás zmínil(a) your_question_is_closed: other: Váš dotaz byl uzavřen your_question_was_deleted: other: Váš dotaz byl odstraněn your_answer_was_deleted: other: Vaše odpověď byla smazána your_comment_was_deleted: other: Váš komentář byl odstraněn up_voted_question: other: hlasoval(a) pro dotaz down_voted_question: other: hlasoval(a) proti dotazu up_voted_answer: other: hlasoval(a) pro odpověď down_voted_answer: other: hlasoval(a) proti odpovědi up_voted_comment: other: hlasoval(a) pro komentář invited_you_to_answer: other: vás pozval, abyste odpověděl(a) earned_badge: other: Získali jste odznak "{{.BadgeName}}" email_tpl: change_email: title: other: "[{{.SiteName}}] Potvrďte svůj nový email" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} odpověděl(a) na váš dotaz" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Obnova hesla" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Potvrďte svůj nový účet" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Zkušební email" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: hlasovat pro upvoted: other: hlasováno pro downvote: other: hlasovat proti downvoted: other: hlasováno proti accept: other: přijmout accepted: other: přijato edit: other: upravit review: queued_post: other: Příspěvek ve frontě flagged_post: other: Nahlášený příspěvek suggested_post_edit: other: Navrhované úpravy reaction: tooltip: other: "{{ .Names }} a {{ .Count }} dalších..." badge: default_badges: autobiographer: name: other: Životopisec desc: other: Profil vyplněn. certified: name: other: Certifikovaný desc: other: Tutoriál pro nové uživatele dokončen. editor: name: other: Editor desc: other: První úprava příspěvku. first_flag: name: other: První nahlášení desc: other: První nahlášení příspěvku. first_upvote: name: other: První hlas pro desc: other: První hlas pro příspěvek. first_link: name: other: První odkaz desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: První sdílení desc: other: První sdílení příspěvku. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Napište 5 komentářů. new_user_of_the_month: name: other: Nový uživatel měsíce desc: other: Výjimečný přínos ve svém prvním měsíci na stránce. read_guidelines: name: other: Přečíst pravidla desc: other: Přečtěte si [pravidla komunity]. reader: name: other: Čtenář desc: other: Přečtěte si všechny odpovědi v tématu s více než 10 odpověďmi. welcome: name: other: Vítejte desc: other: Obdržel(a) hlas. nice_share: name: other: Povedené sdílení desc: other: Sdílel(a) příspěvek s 25 unikátními návštěvníky. good_share: name: other: Dobré sdílení desc: other: Sdílel(a) příspěvek s 300 unikátními návštěvníky. great_share: name: other: Skvělé sdílení desc: other: Sdílel(a) příspěvek s 1000 unikátními návštěvníky. out_of_love: name: other: Optimista desc: other: Využito 50 hlasů pro za den. higher_love: name: other: Vytrvalý optimista desc: other: 5 krát využito 50 hlasů pro za den. crazy_in_love: name: other: Bláznivý optimista desc: other: 20 krát využito 50 hlasů pro za den. promoter: name: other: Promotér desc: other: Pozval(a) uživatele. campaigner: name: other: Campaigner desc: other: Pozval(a) 3 uživatele. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki create_tag: Create Tag edit_tag: Edit Tag ask_a_question: Create Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users oauth_callback: Processing http_404: HTTP Error 404 http_50X: HTTP Error 500 http_403: HTTP Error 403 logout: Log Out posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notifications inbox: Inbox achievement: Achievements new_alerts: New alerts all_read: Mark all as read show_more: Show more someone: Someone inbox_type: all: All posts: Posts invites: Invites votes: Votes answer: Answer question: Question badge_award: Badge suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. contact_us: Contact us editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image file btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed {{size}} MB. desc: label: Description tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered list unordered_list: text: Bulleted list table: text: Table heading: Heading cell: Cell file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: Create new tag form: fields: display_name: label: Display name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description revision: label: Revision edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_cancel: Cancel btn_submit: Submit btn_post: Post new tag tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Are you sure you wish to delete? close: Close merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Edit Tag default_reason: Edit tag default_first_reason: Add tag btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day hours: hours days: days month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: "{{count}} more comments" tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. tip_vote: It adds something useful to the post edit_answer: title: Edit Answer default_reason: Edit answer default_first_reason: Add answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: Newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More wiki: Wiki ask: title: Create Question edit_title: Edit Question default_reason: Edit question default_first_reason: Create question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: What's your topic? Be specific. msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users badges: Badges profile: Profile setting: Settings logout: Log out admin: Admin review: Review bookmark: Bookmarks moderation: Moderation search: placeholder: Search footer: build_on: Powered by <1> Apache Answer upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. resend_email: url_label: Are you sure you want to resend the activation email? url_text: You can also give the activation link above to the user. login: login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New email msg: empty: Email cannot be empty. oauth: connect: Connect with {{ auth_name }} remove: Remove {{ auth_name }} oauth_bind_email: subtitle: Add a recovery email to your account. btn_update: Update email address email: label: Email msg: empty: Email cannot be empty. modal_title: Email already existes. modal_content: This email address already registered. Are you sure you want to connect to the existing account? modal_cancel: Change email modal_confirm: Connect to the existing account password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm new password settings: page_title: Settings goto_modify: Go to modify nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile image gravatar: Gravatar gravatar_text: You can change image on custom: Custom custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About me website: label: Website placeholder: "https://example.com" msg: Website incorrect format location: label: Location placeholder: "City, Country" notification: heading: Email Notifications turn_on: Turn on inbox: label: Inbox notifications description: Answers to your questions, comments, invites, and more. all_new_question: label: All new questions description: Get notified of all new questions. Up to 50 questions per week. all_new_question_for_following_tags: label: All new questions for following tags description: Get notified of new questions for following tags. account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. pass: label: Current password msg: Password cannot be empty. password_title: Password current_pass: label: Current password msg: empty: Current password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New password pass_confirm: label: Confirm new password interface: heading: Interface lang: label: Interface language text: User interface language. It will change when you refresh the page. my_logins: title: My logins label: Log in or sign up on this site using these accounts. modal_title: Remove login modal_content: Are you sure you want to remove this login from your account? modal_confirm_btn: Remove remove_success: Removed successfully toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. sent_success: Sent successfully related_question: title: Related answers: answers linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Pozvěte další uživatele desc: Pozvěte lidi, o kterých si myslíte, že mohou odpovědět. invite: Invite to answer add: Add people search: Search people question_detail: action: Action created: Created Asked: Asked asked: asked update: Modified Edited: Edited edit: edited commented: commented Views: Viewed Follow: Follow Following: Following follow_tip: Follow this question to receive notifications answered: answered closed_in: Closed in show_exist: Show existing question. useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: title: Answers score: Score newest: Newest oldest: Oldest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer edit_answer: Edit my existing answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. tips: header_1: Thanks for your answer li1_1: Please be sure to answer the question. Provide details and share your research. li1_2: Back up any statements you make with references or personal experience. header_2: But avoid ... li2_1: Asking for help, seeking clarification, or responding to other answers. reopen: confirm_btn: Reopen title: Reopen this post content: Are you sure you want to reopen? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. confirm_btn: Pin delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_answer_deleted: This answer has been deleted undelete_title: Undelete this post undelete_desc: Are you sure you wish to undelete? btns: confirm: Confirm cancel: Cancel edit: Edit save: Save delete: Delete undelete: Undelete list: List unlist: Unlist unlisted: Unlisted login: Log in signup: Sign up logout: Log out verify: Verify create: Create approve: Approve reject: Reject skip: Skip discard_draft: Discard draft pinned: Pinned all: All question: Question answer: Answer comment: Comment refresh: Refresh resend: Resend deactivate: Deactivate active: Active suspend: Suspend unsuspend: Unsuspend close: Close reopen: Reopen ok: OK light: Light dark: Dark system_setting: System setting default: Default reset: Reset tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" counts_loading: "... Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Error... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage oops: Oops! invalid: The link you used no longer works. confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" x_posts: "{{ count }} Posts" questions: Questions answers: Answers newest: Newest active: Active hot: Hot frequent: Frequent recommend: Recommend score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes badges: Badges newest: Newest score: Score edit_profile: Edit profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined comma: "," last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? content_empty: No posts found. accepted: Accepted answered: answered asked: asked downvoted: downvoted mod_short: MOD mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions recent_badges: Recent Badges install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please choose a language db_type: label: Database engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database host placeholder: "db:3306" msg: Database host cannot be empty. db_name: label: Database name placeholder: answer msg: Database name cannot be empty. db_file: label: Database file placeholder: /data/answer.db msg: Database file cannot be empty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site name msg: Site name cannot be empty. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. max_length: Site URL must be at maximum 512 characters in length. contact_email: label: Contact email text: Email address of key contact responsible for this site. msg: empty: Contact email cannot be empty. incorrect: Contact email incorrect format. login_required: label: Private switch: Login required text: Only logged in users can access this community. admin_name: label: Name msg: Name cannot be empty. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_error: http_error: HTTP Error {{ code }} desc_403: You don't have permission to access this page. desc_404: Unfortunately, this page doesn't exist. desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users badges: Badges flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write terms: Terms tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes login: Login privileges: Privileges plugins: Plugins installed_plugins: Installed Plugins apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Welcome to {{site_name}} user_center: login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site statistics questions: "Questions:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Answers:" comments: "Comments:" votes: "Votes:" users: "Users:" flags: "Flags:" reviews: "Reviews:" site_health: Site health version: "Version:" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Private public: Public smtp: "SMTP:" timezone: "Timezone:" system_info: System info go_version: "Go version:" database: "Database:" database_size: "Database size:" storage_used: "Storage used:" uptime: "Uptime:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Contact forum: Forum documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled writable: Writable not_writable: Not writable flags: title: Flags pending: Pending completed: Completed flagged: Flagged flagged_type: Flagged {{ type }} created: Created action: Action review: Review user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: users: label: Bulk add user placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separate “name, email, password” with commas. One user per line. msg: "Please enter the user's email, one per line." display_name: label: Display name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Status role: Role action: Action change: Change all: All staff: Staff more: More inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password edit_profile: Edit profile change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user deactivate_user: title: Deactivate user content: An inactive user must re-validate their email. delete_user: title: Delete this user content: Are you sure you want to delete this user? This is permanent! remove: Remove their content label: Remove all questions, answers, comments, etc. text: Don’t check this if you wish to only delete the user’s account. suspend_user: title: Suspend this user content: A suspended user can't log in. label: How long will the user be suspended for? forever: Forever questions: page_title: Questions unlisted: Unlisted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change pending: Pending filter: placeholder: "Filter by title, question:id" answers: page_title: Answers post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short site description msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site description msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. check_update: label: Software updates text: Automatically check for updates interface: page_title: Interface language: label: Interface language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: From email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL tls: TLS none: None smtp_port: label: SMTP port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP username msg: SMTP username cannot be empty. smtp_password: label: SMTP password msg: SMTP password cannot be empty. test_email_recipient: label: Test email recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile logo text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square icon msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Každý uživatel může napsat pouze jednu odpověď na stejný dotaz text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. color_scheme: label: Color scheme navbar_style: label: Navbar background style primary_color: label: Primary color text: Modify the colors used by your themes layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: > head: label: Head text: > header: label: Header text: > footer: label: Footer text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: Allowed email domains text: Email domains that users must register accounts with. One domain per line. Ignored when empty. private: title: Private label: Login required text: Only logged in users can access this community. password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: All active: Active inactive: Inactive outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Name version: Version status: Status action: Action deactivate: Deactivate activate: Activate settings: Settings settings_users: title: Users avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: URL základny Gravatar text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Allow users to change their display name allow_update_username: label: Allow users to change their username allow_update_avatar: label: Allow users to change their profile image allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optional) empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." select: Select page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created pin: pinned unpin: unpinned show: listed hide: unlisted title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: Our community staff reputation: reputation votes: votes prompt: leave_page: Are you sure you want to leave the page? changes_not_save: Your changes may not be saved. draft: discard_confirm: Are you sure you want to discard your draft? messages: post_deleted: This post has been deleted. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/cy_GB.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Llwyddiant. unknown: other: Gwall anhysbys. request_format_error: other: Nid yw fformat y cais yn ddilys. unauthorized_error: other: Anawdurdodedig. database_error: other: Gwall gweinydd data. forbidden_error: other: Forbidden. duplicate_request_error: other: Duplicate submission. action: report: other: Tynnu sylw edit: other: Golygu delete: other: Dileu close: other: Cau reopen: other: Ailagor forbidden_error: other: Forbidden. pin: other: Pinio hide: other: Dad-restru unpin: other: Dadbinio show: other: Rhestr invite_someone_to_answer: other: Edit undelete: other: Undelete merge: other: Merge role: name: user: other: Defnyddiwr admin: other: Gweinyddwr moderator: other: Cymedrolwr description: user: other: Diofyn heb unrhyw fynediad arbennig. admin: other: Bod â'r pŵer llawn i gael mynediad i'r safle. moderator: other: Mae ganddo fynediad i bob post ac eithrio gosodiadau gweinyddol. privilege: level_1: description: other: Level 1 (less reputation required for private team, group) level_2: description: other: Level 2 (low reputation required for startup community) level_3: description: other: Level 3 (high reputation required for mature community) level_custom: description: other: Custom Level rank_question_add_label: other: Ask question rank_answer_add_label: other: Write answer rank_comment_add_label: other: Write comment rank_report_add_label: other: Flag rank_comment_vote_up_label: other: Upvote comment rank_link_url_limit_label: other: Post more than 2 links at a time rank_question_vote_up_label: other: Upvote question rank_answer_vote_up_label: other: Upvote answer rank_question_vote_down_label: other: Downvote question rank_answer_vote_down_label: other: Downvote answer rank_invite_someone_to_answer_label: other: Invite someone to answer rank_tag_add_label: other: Create new tag rank_tag_edit_label: other: Edit tag description (need to review) rank_question_edit_label: other: Edit other's question (need to review) rank_answer_edit_label: other: Edit other's answer (need to review) rank_question_edit_without_review_label: other: Edit other's question without review rank_answer_edit_without_review_label: other: Edit other's answer without review rank_question_audit_label: other: Review question edits rank_answer_audit_label: other: Review answer edits rank_tag_audit_label: other: Review tag edits rank_tag_edit_without_review_label: other: Edit tag description without review rank_tag_synonym_label: other: Manage tag synonyms email: other: Ebost e_mail: other: Email password: other: Cyfrinair pass: other: Password old_pass: other: Current password original_text: other: This post email_or_password_wrong_error: other: Nid yw e-bost a chyfrinair yn cyfateb. error: common: invalid_url: other: Invalid URL. status_invalid: other: Invalid status. password: space_invalid: other: Password cannot contain spaces. admin: cannot_update_their_password: other: Ni allwch addasu eich cyfrinair. cannot_edit_their_profile: other: You cannot modify your profile. cannot_modify_self_status: other: Ni allwch addasu eich statws. email_or_password_wrong: other: Nid yw e-bost a chyfrinair yn cyfateb. answer: not_found: other: Ni cheir yr ateb. cannot_deleted: other: Dim caniatâd i ddileu. cannot_update: other: Dim caniatâd i ddiweddaru. question_closed_cannot_add: other: Mae cwestiynau ar gau ac ni ellir eu hychwanegu. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Nid oes modd golygu sylwadau. not_found: other: Sylw heb ei ganfod. cannot_edit_after_deadline: other: Mae'r amser sylwadau wedi bod yn rhy hir i'w addasu. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: E-bost yn bodoli eisoes. need_to_be_verified: other: Dylid gwirio e-bost. verify_url_expired: other: Mae'r URL wedi'i wirio gan e-bost wedi dod i ben, anfonwch yr e-bost eto. illegal_email_domain_error: other: Email is not allowed from that email domain. Please use another one. lang: not_found: other: Ffeil iaith heb ei chanfod. object: captcha_verification_failed: other: Captcha anghywir. disallow_follow: other: Ni chaniateir i chi ddilyn. disallow_vote: other: Ni chaniateir i chi pleidleisio. disallow_vote_your_self: other: Ni allwch bleidleisio dros eich post eich hun. not_found: other: Heb ganfod y gwrthrych. verification_failed: other: Methodd y dilysu. email_or_password_incorrect: other: Nid yw e-bost a chyfrinair yn cyfateb. old_password_verification_failed: other: Methodd yr hen ddilysiad cyfrinair new_password_same_as_previous_setting: other: Mae'r cyfrinair newydd yr un fath â'r un blaenorol. already_deleted: other: This post has been deleted. meta: object_not_found: other: Meta object not found question: already_deleted: other: Mae'r postiad hwn wedi'i ddileu. under_review: other: Your post is awaiting review. It will be visible after it has been approved. not_found: other: Cwestiwn heb ei ganfod. cannot_deleted: other: Dim caniatâd i ddileu. cannot_close: other: Dim caniatâd i cau. cannot_update: other: Dim caniatâd i ddiweddaru. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. vote_fail_to_meet_the_condition: other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. no_enough_rank_to_operate: other: You need at least {{.Rank}} reputation to do this. report: handle_failed: other: Methodd handlen yr adroddiad. not_found: other: Heb ganfod yr adroddiad. tag: already_exist: other: Mae tag eisoes yn bodoli. not_found: other: Tag heb ei ddarganfod. recommend_tag_not_found: other: Recommend tag is not exist. recommend_tag_enter: other: Rhowch o leiaf un tag gofynnol. not_contain_synonym_tags: other: Ni ddylai gynnwys tagiau cyfystyr. cannot_update: other: Dim caniatâd i ddiweddaru. is_used_cannot_delete: other: You cannot delete a tag that is in use. cannot_set_synonym_as_itself: other: Ni allwch osod cyfystyr y tag cyfredol fel ei hun. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: The from name cannot be a email address. theme: not_found: other: Thema heb ei ddarganfod. revision: review_underway: other: Methu â golygu ar hyn o bryd, mae fersiwn yn y ciw adolygu. no_permission: other: No permission to revise. user: external_login_missing_user_id: other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. external_login_unbinding_forbidden: other: Please set a login password for your account before you remove this login. email_or_password_wrong: other: other: Nid yw e-bost a chyfrinair yn cyfateb. not_found: other: Defnyddwr heb ei ddarganfod. suspended: other: Mae'r defnyddiwr hwn wedi'i atal. username_invalid: other: Mae'r enw defnyddiwr yn annilys. username_duplicate: other: Cymerwyd yr enw defnyddiwr eisoes. set_avatar: other: Methodd set avatar. cannot_update_your_role: other: Ni allwch addasu eich rôl. not_allowed_registration: other: Currently the site is not open for registration. not_allowed_login_via_password: other: Currently the site is not allowed to login via password. access_denied: other: Access denied page_access_denied: other: You do not have access to this page. add_bulk_users_format_error: other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Wedi methu darllen y ffurfwedd database: connection_failed: other: Methodd cysylltiad cronfa ddata create_table_failed: other: Methwyd creu tabl install: create_config_failed: other: Methu creu'r ffeil config.yaml. upload: unsupported_file_format: other: Fformat ffeil heb ei gefnogi. site_info: config_not_found: other: Site config not found. badge: object_not_found: other: Badge object not found reason: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude_or_abusive: name: other: rude or abusive desc: other: "A reasonable person would find this content inappropriate for respectful discourse." a_duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. placeholder: other: Enter the existing question link not_a_answer: name: other: not an answer desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." no_longer_needed: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. something: name: other: something else desc: other: This post requires staff attention for another reason not listed above. placeholder: other: Let us know specifically what you are concerned about community_specific: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. not_clarity: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. looks_ok: name: other: looks OK desc: other: This post is good as-is and not low quality. needs_edit: name: other: needs edit, and I did it desc: other: Improve and correct problems with this post yourself. needs_close: name: other: needs close desc: other: A closed question can't answer, but still can edit, vote and comment. needs_delete: name: other: needs delete desc: other: This post will be deleted. question: close: duplicate: name: other: sbam desc: other: Mae'r cwestiwn hwn wedi'i ofyn o'r blaen ac mae ganddo ateb yn barod. guideline: name: other: rheswm cymunedol-benodol desc: other: Nid yw'r cwestiwn hwn yn bodloni canllaw cymunedol. multiple: name: other: angen manylion neu eglurder desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: rhywbeth arall desc: other: Mae'r swydd hon angen reswm arall nad yw wedi'i restru uchod. operation_type: asked: other: gofynnodd answered: other: atebodd modified: other: wedi newid deleted_title: other: Deleted question questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: cwestiwn wedi'i ddiweddaru answer_the_question: other: cwestiwn wedi ei ateb update_answer: other: ateb wedi'i ddiweddaru accept_answer: other: ateb derbyniol comment_question: other: cwestiwn a wnaed comment_answer: other: ateb a wnaed reply_to_you: other: atebodd i chi mention_you: other: wedi sôn amdanoch your_question_is_closed: other: Mae eich cwestiwn wedi’i gau your_question_was_deleted: other: Mae eich cwestiwn wedi’i dileu your_answer_was_deleted: other: Mae eich ateb wedi’i dileu your_comment_was_deleted: other: Mae eich sylw wedi’i dileu up_voted_question: other: upvoted question down_voted_question: other: downvoted question up_voted_answer: other: upvoted answer down_voted_answer: other: downvoted answer up_voted_comment: other: upvoted comment invited_you_to_answer: other: invited you to answer earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "[{{.SiteName}}] Confirm your new email address" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Password reset" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Confirm your new account" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Test Email" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: upvote upvoted: other: upvoted downvote: other: downvote downvoted: other: downvoted accept: other: accept accepted: other: accepted edit: other: edit review: queued_post: other: Queued post flagged_post: other: Flagged post suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: Editor desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Sut i Fformatio desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Cynt next: Nesaf page_title: question: Cwestiwn questions: Cwestiynau tag: Tag tags: Tagiau tag_wiki: tag wiki create_tag: Creu Tag edit_tag: Golygu Tag ask_a_question: Create Question edit_question: Golygu Cwestiwn edit_answer: Golygu Ateb search: Chwiliwch posts_containing: Postiadau yn cynnwys settings: Gosodiadau notifications: Hysbysiadau login: Mewngofnodi sign_up: Cofrestru account_recovery: Adfer Cyfrif account_activation: Ysgogi Cyfrif confirm_email: Cadarnhau e-bost account_suspended: Cyfrif wedi'i atal admin: Gweinyddu change_email: Addasu E-bost install: Ateb Gosod upgrade: Ateb Uwchraddio maintenance: Cynnal a Chadw Gwefan users: Defnyddwyr oauth_callback: Processing http_404: Gwall HTTP 404 http_50X: Gwall HTTP 500 http_403: Gwall HTTP 403 logout: Log Out posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Hysbysiadau inbox: Mewnflwch achievement: Llwyddiannau new_alerts: New alerts all_read: Marciwch y cyfan fel wedi'i ddarllen show_more: Dangos mwy someone: Someone inbox_type: all: All posts: Posts invites: Invites votes: Votes answer: Answer question: Question badge_award: Badge suspended: title: Mae'ch Cyfrif wedi'i Atal until_time: "Cafodd eich cyfrif ei atal tan {{ time }}." forever: Cafodd y defnyddiwr hwn ei atal am byth. end: Nid ydych yn arwain cymunedol. contact_us: Contact us editor: blockquote: text: Dyfyniad bold: text: Cryf chart: text: Siart flow_chart: Siart llif sequence_diagram: Diagram dilyniant class_diagram: Diagram dosbarth state_diagram: Diagram cyflwr entity_relationship_diagram: Diagram perthynas endid user_defined_diagram: Diagram wedi'i ddiffinio gan y defnyddiwr gantt_chart: Siart Gantt pie_chart: Siart cylch code: text: Sampl côd add_code: Ychwanegu sampl côd form: fields: code: label: Côd msg: empty: Ni all côd fod yn wag. language: label: Iaith placeholder: Synhwyriad awtomatig btn_cancel: Canslo btn_confirm: Ychwanegu formula: text: Fformiwla options: inline: Fformiwla mewn-lein block: Fformiwla bloc heading: text: Pennawd options: h1: Pennawd 1 h2: Pennawd 2 h3: Pennawd 3 h4: Pennawd 4 h5: Pennawd 5 h6: Pennawd 6 help: text: Cymorth hr: text: Horizontal rule image: text: Delwedd add_image: Ychwanegu delwedd tab_image: Uwchlwytho delwedd form_image: fields: file: label: Image file btn: Dewis delwedd msg: empty: Ni all ffeil fod yn wag. only_image: Dim ond ffeiliau delwedd a ganiateir. max_size: File size cannot exceed {{size}} MB. desc: label: Disgrifiad tab_url: URL delwedd form_url: fields: url: label: URL delwedd msg: empty: Ni all URL delwedd fod yn wag. name: label: Disgrifiad btn_cancel: Canslo btn_confirm: Ychwanegu uploading: Wrthi'n uwchlwytho indent: text: Mewnoliad outdent: text: Alloliad italic: text: Pwyslais link: text: Hypergyswllt add_link: Ychwanegu hypergyswllt form: fields: url: label: URL msg: empty: Ni all URL fod yn wag. name: label: Disgrifiad btn_cancel: Canslo btn_confirm: Ychwanegu ordered_list: text: Numbered list unordered_list: text: Bulleted list table: text: Tabl heading: Pennawd cell: Cell file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: Rwy'n cau'r post hon fel... btn_cancel: Canslo btn_submit: Cyflwyno remark: empty: Ni all fod yn wag. msg: empty: Dewis rheswm. report_modal: flag_title: Dwi'n tynnu sylw i adrodd y swydd hon fel... close_title: Rwy'n cau'r post hon fel... review_question_title: Adolygu cwestiwn review_answer_title: Adolygu ateb review_comment_title: Adolygu sylwad btn_cancel: Canslo btn_submit: Cyflwyno remark: empty: Ni all fod yn wag. msg: empty: Dewis rheswm. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: Creu tag newydd form: fields: display_name: label: Display name msg: empty: Ni all fod enw dangos yn wag. range: Enw arddangos hyd at 35 nod. slug_name: label: URL slug desc: Slug URL hyd at 35 nod. msg: empty: Ni all Slug URL fod yn wag. range: Slug URL hyd at 35 nod. character: Mae slug URL yn cynnwys set nodau na caniateir. desc: label: Disgrifiad revision: label: Revision edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_cancel: Canslo btn_submit: Cyflwyno btn_post: Post tag newydd tag_info: created_at: Creuwyd edited_at: Golygwyd history: Hanes synonyms: title: Cyfystyron text: Bydd y tagiau canlynol yn cael eu hail-fapio i empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Are you sure you wish to delete? close: Close merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Edit Tag default_reason: Edit tag default_first_reason: Add tag btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day hours: hours days: days month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: "{{count}} more comments" tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. tip_vote: It adds something useful to the post edit_answer: title: Edit Answer default_reason: Edit answer default_first_reason: Add answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Canslo tags: title: Tagiau sort_buttons: popular: Poblogaidd name: Enw newest: Newest button_follow: Dilyn button_following: Yn dilyn tag_label: cwestiynau search_placeholder: Hidlo yn ôl enw tag no_desc: Nid oes gan y tag unrhyw ddisgrifiad. more: Mwy wiki: Wiki ask: title: Create Question edit_title: Golygu Cwestiwn default_reason: Golygu Cwestiwn default_first_reason: Create question similar_questions: Cwestiynau tebyg form: fields: revision: label: Diwygiad title: label: Teitl placeholder: What's your topic? Be specific. msg: empty: Ni all teitl fod yn wag. range: Teitl hyd at 20 nod body: label: Corff msg: empty: Ni all corff fod yn wag. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Tagiau msg: empty: Ni all tagiau fod yn wag. answer: label: Ateb msg: empty: Ni all ateb fod yn wag. edit_summary: label: Edit summary placeholder: >- Eglurwch yn fyr eich newidiadau (sillafu wedi'i gywiro, gramadeg sefydlog, fformatio gwell) btn_post_question: Post cweistiwn btn_save_edits: Cadw golygiadau answer_question: Atebwch eich cwestiwn eich hun post_question&answer: Postiwch eich cwestiwn ac ateb tag_selector: add_btn: Ychwanegu tag create_btn: Creu tag newydd search_tag: Chwilio tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users badges: Badges profile: Profile setting: Settings logout: Log out admin: Admin review: Review bookmark: Bookmarks moderation: Moderation search: placeholder: Search footer: build_on: Powered by <1> Apache Answer upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. resend_email: url_label: Are you sure you want to resend the activation email? url_text: You can also give the activation link above to the user. login: login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New email msg: empty: Email cannot be empty. oauth: connect: Connect with {{ auth_name }} remove: Remove {{ auth_name }} oauth_bind_email: subtitle: Add a recovery email to your account. btn_update: Update email address email: label: Email msg: empty: Email cannot be empty. modal_title: Email already existes. modal_content: This email address already registered. Are you sure you want to connect to the existing account? modal_cancel: Change email modal_confirm: Connect to the existing account password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm new password settings: page_title: Settings goto_modify: Go to modify nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile image gravatar: Gravatar gravatar_text: You can change image on custom: Custom custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About me website: label: Website placeholder: "https://example.com" msg: Website incorrect format location: label: Location placeholder: "City, Country" notification: heading: Email Notifications turn_on: Turn on inbox: label: Inbox notifications description: Answers to your questions, comments, invites, and more. all_new_question: label: All new questions description: Get notified of all new questions. Up to 50 questions per week. all_new_question_for_following_tags: label: All new questions for following tags description: Get notified of new questions for following tags. account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. pass: label: Current password msg: Password cannot be empty. password_title: Password current_pass: label: Current password msg: empty: Current password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New password pass_confirm: label: Confirm new password interface: heading: Interface lang: label: Interface language text: User interface language. It will change when you refresh the page. my_logins: title: My logins label: Log in or sign up on this site using these accounts. modal_title: Remove login modal_content: Are you sure you want to remove this login from your account? modal_confirm_btn: Remove remove_success: Removed successfully toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. sent_success: Sent successfully related_question: title: Related answers: answers linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: People Asked desc: Select people who you think might know the answer. invite: Invite to answer add: Add people search: Search people question_detail: action: Action created: Created Asked: Asked asked: asked update: Modified Edited: Edited edit: edited commented: commented Views: Viewed Follow: Follow Following: Following follow_tip: Follow this question to receive notifications answered: answered closed_in: Closed in show_exist: Show existing question. useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: title: Answers score: Score newest: Newest oldest: Oldest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer edit_answer: Edit my existing answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. tips: header_1: Thanks for your answer li1_1: Please be sure to answer the question. Provide details and share your research. li1_2: Back up any statements you make with references or personal experience. header_2: But avoid ... li2_1: Asking for help, seeking clarification, or responding to other answers. reopen: confirm_btn: Reopen title: Reopen this post content: Are you sure you want to reopen? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. confirm_btn: Pin delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_answer_deleted: This answer has been deleted undelete_title: Undelete this post undelete_desc: Are you sure you wish to undelete? btns: confirm: Confirm cancel: Cancel edit: Edit save: Save delete: Delete undelete: Undelete list: List unlist: Unlist unlisted: Unlisted login: Log in signup: Sign up logout: Log out verify: Verify create: Create approve: Approve reject: Reject skip: Skip discard_draft: Discard draft pinned: Pinned all: All question: Question answer: Answer comment: Comment refresh: Refresh resend: Resend deactivate: Deactivate active: Active suspend: Suspend unsuspend: Unsuspend close: Close reopen: Reopen ok: OK light: Light dark: Dark system_setting: System setting default: Default reset: Reset tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" counts_loading: "... Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Error... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage oops: Oops! invalid: The link you used no longer works. confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" x_posts: "{{ count }} Posts" questions: Questions answers: Answers newest: Newest active: Active hot: Hot frequent: Frequent recommend: Recommend score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes badges: Badges newest: Newest score: Score edit_profile: Edit profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined comma: "," last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? content_empty: No posts found. accepted: Accepted answered: answered asked: asked downvoted: downvoted mod_short: MOD mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions recent_badges: Recent Badges install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please choose a language db_type: label: Database engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database host placeholder: "db:3306" msg: Database host cannot be empty. db_name: label: Database name placeholder: answer msg: Database name cannot be empty. db_file: label: Database file placeholder: /data/answer.db msg: Database file cannot be empty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site name msg: Site name cannot be empty. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. max_length: Site URL must be at maximum 512 characters in length. contact_email: label: Contact email text: Email address of key contact responsible for this site. msg: empty: Contact email cannot be empty. incorrect: Contact email incorrect format. login_required: label: Private switch: Login required text: Only logged in users can access this community. admin_name: label: Name msg: Name cannot be empty. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_error: http_error: HTTP Error {{ code }} desc_403: You don't have permission to access this page. desc_404: Unfortunately, this page doesn't exist. desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users badges: Badges flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write terms: Terms tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes login: Login privileges: Privileges plugins: Plugins installed_plugins: Installed Plugins apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Welcome to {{site_name}} user_center: login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site statistics questions: "Questions:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Answers:" comments: "Comments:" votes: "Votes:" users: "Users:" flags: "Flags:" reviews: "Reviews:" site_health: Site health version: "Version:" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Private public: Public smtp: "SMTP:" timezone: "Timezone:" system_info: System info go_version: "Go version:" database: "Database:" database_size: "Database size:" storage_used: "Storage used:" uptime: "Uptime:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Contact forum: Forum documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled writable: Writable not_writable: Not writable flags: title: Flags pending: Pending completed: Completed flagged: Flagged flagged_type: Flagged {{ type }} created: Created action: Action review: Review user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: users: label: Bulk add user placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separate “name, email, password” with commas. One user per line. msg: "Please enter the user's email, one per line." display_name: label: Display name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Status role: Role action: Action change: Change all: All staff: Staff more: More inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password edit_profile: Edit profile change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user deactivate_user: title: Deactivate user content: An inactive user must re-validate their email. delete_user: title: Delete this user content: Are you sure you want to delete this user? This is permanent! remove: Remove their content label: Remove all questions, answers, comments, etc. text: Don’t check this if you wish to only delete the user’s account. suspend_user: title: Suspend this user content: A suspended user can't log in. label: How long will the user be suspended for? forever: Forever questions: page_title: Questions unlisted: Unlisted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change pending: Pending filter: placeholder: "Filter by title, question:id" answers: page_title: Answers post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short site description msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site description msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. check_update: label: Software updates text: Automatically check for updates interface: page_title: Interface language: label: Interface language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: From email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL tls: TLS none: None smtp_port: label: SMTP port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP username msg: SMTP username cannot be empty. smtp_password: label: SMTP password msg: SMTP password cannot be empty. test_email_recipient: label: Test email recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile logo text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square icon msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for each question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. color_scheme: label: Color scheme navbar_style: label: Navbar background style primary_color: label: Primary color text: Modify the colors used by your themes layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: > head: label: Head text: > header: label: Header text: > footer: label: Footer text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: Allowed email domains text: Email domains that users must register accounts with. One domain per line. Ignored when empty. private: title: Private label: Login required text: Only logged in users can access this community. password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: All active: Active inactive: Inactive outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Name version: Version status: Status action: Action deactivate: Deactivate activate: Activate settings: Settings settings_users: title: Users avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar Base URL text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Allow users to change their display name allow_update_username: label: Allow users to change their username allow_update_avatar: label: Allow users to change their profile image allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optional) empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." select: Select page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created pin: pinned unpin: unpinned show: listed hide: unlisted title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: Our community staff reputation: reputation votes: votes prompt: leave_page: Are you sure you want to leave the page? changes_not_save: Your changes may not be saved. draft: discard_confirm: Are you sure you want to discard your draft? messages: post_deleted: This post has been deleted. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/da_DK.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Gennemført. unknown: other: Ukendt fejl. request_format_error: other: Forespørgselsformat er ikke gyldigt. unauthorized_error: other: Uautoriseret. database_error: other: Data-server fejl. forbidden_error: other: Forbudt. duplicate_request_error: other: Duplilkeret indenselse. action: report: other: Anmeld edit: other: Rediger delete: other: Slet close: other: Luk reopen: other: Genåbn forbidden_error: other: Forbudt. pin: other: Fastgør hide: other: Afliste unpin: other: Frigør show: other: Liste invite_someone_to_answer: other: Rediger undelete: other: Genopret merge: other: Sammenflet role: name: user: other: Bruger admin: other: Administrator moderator: other: Moderator description: user: other: Standard uden særlig adgang. admin: other: Hav den fulde magt til at få adgang til webstedet. moderator: other: Har adgang til alle opslag undtagen administratorindstillinger. privilege: level_1: description: other: Niveau 1 (mindre omdømme kræves for private team, gruppe) level_2: description: other: Niveau 2 (lav omdømme kræves for opstart fællesskab) level_3: description: other: Niveau 3 (højt omdømme kræves for moden fællesskab) level_custom: description: other: Brugerdefineret Niveau rank_question_add_label: other: Stil spørgsmål rank_answer_add_label: other: Skriv svar rank_comment_add_label: other: Skriv kommentar rank_report_add_label: other: Anmeld rank_comment_vote_up_label: other: Op-stem kommentar rank_link_url_limit_label: other: Oplæg mere end 2 links ad gangen rank_question_vote_up_label: other: Op-stem spørgsmål rank_answer_vote_up_label: other: Op-stem svar rank_question_vote_down_label: other: Ned-stem spørgsmål rank_answer_vote_down_label: other: Ned-stem svar rank_invite_someone_to_answer_label: other: Inviter nogen til at svare rank_tag_add_label: other: Opret et nyt nøgleord rank_tag_edit_label: other: Rediger nøgleord beskrivelse (skal gennemgås) rank_question_edit_label: other: Rediger andres spørgsmål (skal gennemgås) rank_answer_edit_label: other: Redigere andres svar (skal gennemgås) rank_question_edit_without_review_label: other: Rediger andres spørgsmål uden gennemgang rank_answer_edit_without_review_label: other: Rediger andres svar uden gennemgang rank_question_audit_label: other: Gennemse spørgsmål redigeringer rank_answer_audit_label: other: Gennemgå svar redigeringer rank_tag_audit_label: other: Gennemse nøgleord redigeringer rank_tag_edit_without_review_label: other: Rediger nøgleord beskrivelse uden gennemgang rank_tag_synonym_label: other: Administrer nøgleord synonymer email: other: E-mail e_mail: other: E-mail password: other: Adgangskode pass: other: Adgangskode old_pass: other: Nuværende adgangskode original_text: other: Dette opslag email_or_password_wrong_error: other: E-mail og adgangskode stemmer ikke overens. error: common: invalid_url: other: Ugyldig URL. status_invalid: other: Ugyldig status. password: space_invalid: other: Adgangskoden må ikke indeholde mellemrum. admin: cannot_update_their_password: other: Du kan ikke ændre din adgangskode. cannot_edit_their_profile: other: Du kan ikke ændre din profil. cannot_modify_self_status: other: Du kan ikke ændre din status. email_or_password_wrong: other: E-mail og adgangskode stemmer ikke overens. answer: not_found: other: Svar ikke fundet. cannot_deleted: other: Ingen tilladelser til at slette. cannot_update: other: Ingen tilladelse til at opdatere. question_closed_cannot_add: other: Spørgsmål er lukket og kan ikke tilføjes. content_cannot_empty: other: Svar skal udfyldes. comment: edit_without_permission: other: Kommentar er ikke tilladt at redigere. not_found: other: Kommentar ikke fundet. cannot_edit_after_deadline: other: Kommentaren er for gammel til at blive redigeret. content_cannot_empty: other: Kommentar indhold kan ikke være tomt. email: duplicate: other: Email eksisterer allerede. need_to_be_verified: other: E-mail skal bekræftes. verify_url_expired: other: Email bekræftet URL er udløbet. Send venligst e-mailen igen. illegal_email_domain_error: other: E-mail er ikke tilladt fra dette e-mail-domæne. Brug venligst et andet. lang: not_found: other: Sprog-fil kunne ikke findes. object: captcha_verification_failed: other: Captcha er forkert. disallow_follow: other: Du har ikke tilladelse til at følge. disallow_vote: other: Du har ikke tilladelse til at stemme. disallow_vote_your_self: other: Du kan ikke stemme på dit eget indlæg. not_found: other: Objekt ikke fundet. verification_failed: other: Verifikation mislykkedes. email_or_password_incorrect: other: E-mail og adgangskode stemmer ikke overens. old_password_verification_failed: other: Den gamle adgangskodebekræftelse mislykkedes new_password_same_as_previous_setting: other: Den nye adgangskode er den samme som den foregående. already_deleted: other: Dette indlæg er blevet slettet. meta: object_not_found: other: Metaobjekt ikke fundet question: already_deleted: other: Dette indlæg er blevet slettet. under_review: other: Dit indlæg afventer gennemgang. Det vil være synligt, når det er blevet godkendt. not_found: other: Spørgsmål ikke fundet. cannot_deleted: other: Ingen tilladelser til at slette. cannot_close: other: Ingen tilladelse til at lukke. cannot_update: other: Ingen tilladelse til at opdatere. content_cannot_empty: other: Indhold kan ikke være tomt. content_less_than_minimum: other: Ikke nok indhold indtastet. rank: fail_to_meet_the_condition: other: Omdømmelse rang opfylder ikke betingelsen. vote_fail_to_meet_the_condition: other: Tak for feedback. Du skal mindst have {{.Rank}} ry for at afgive en stemme. no_enough_rank_to_operate: other: Du skal mindst {{.Rank}} omdømme for at gøre dette. report: handle_failed: other: Report handle failed. not_found: other: Rapport ikke fundet. tag: already_exist: other: Nøgleord findes allerede. not_found: other: Nøgleord blev ikke fundet. recommend_tag_not_found: other: Anbefal nøgleord eksisterer ikke. recommend_tag_enter: other: Indtast mindst et påkrævet nøgleord. not_contain_synonym_tags: other: Må ikke indeholde synonym nøgleord. cannot_update: other: Ingen tilladelse til at opdatere. is_used_cannot_delete: other: Du kan ikke slette et nøgleord, der er i brug. cannot_set_synonym_as_itself: other: Du kan ikke indstille synonymet for det nuværende nøgleord som sig selv. minimum_count: other: Ikke nok nøgleord blev indtastet. smtp: config_from_name_cannot_be_email: other: Fra-navnet kan ikke være en e-mail-adresse. theme: not_found: other: Tema ikke fundet. revision: review_underway: other: Kan ikke redigere i øjeblikket, der er en version i revisionskøen. no_permission: other: Ingen tilladelse til at revidere. user: external_login_missing_user_id: other: Den tredjepartsplatform giver ikke et unikt UserID, så du kan ikke logge ind, kontakt venligst webstedsadministratoren. external_login_unbinding_forbidden: other: Angiv en adgangskode til din konto, før du fjerner dette login. email_or_password_wrong: other: other: E-mail og adgangskode stemmer ikke overens. not_found: other: Bruger ikke fundet. suspended: other: Brugeren er suspenderet. username_invalid: other: Brugernavn er ugyldigt. username_duplicate: other: Brugernavn er allerede i brug. set_avatar: other: Avatar sæt mislykkedes. cannot_update_your_role: other: Du kan ikke ændre din rolle. not_allowed_registration: other: Webstedet er ikke åbent for registrering. not_allowed_login_via_password: other: I øjeblikket er det ikke tilladt at logge ind via adgangskode. access_denied: other: Adgang nægtet page_access_denied: other: Du har ikke adgang til denne side. add_bulk_users_format_error: other: "Fejl {{.Field}} format nær '{{.Content}}' i linje {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Antallet af brugere du tilføjer på én gang skal være i intervallet 1 -{{.MaxAmount}}." status_suspended_forever: other: "Denne bruger blev suspenderet for evigt. Denne bruger opfylder ikke en fællesskabsretningslinje." status_suspended_until: other: "Denne bruger blev suspenderet indtil {{.SuspendedUntil}}. Denne bruger opfylder ikke en fællesskabsretningslinje." status_deleted: other: "Denne bruger blev slettet." status_inactive: other: "Denne bruger er ikke aktiv." config: read_config_failed: other: Kunne ikke læse konfigurationen database: connection_failed: other: Database forbindelse mislykkedes create_table_failed: other: Tabellen kunne ikke oprettes install: create_config_failed: other: Kan ikke oprette filen config.yaml. upload: unsupported_file_format: other: Ikke understøttet filformat. site_info: config_not_found: other: Site config ikke fundet. badge: object_not_found: other: Emblem objekt ikke fundet reason: spam: name: other: spam desc: other: Dette indlæg er en annonce eller vandalisme. Det er ikke nyttigt eller relevant for det aktuelle emne. rude_or_abusive: name: other: uhøflig eller misbrug desc: other: "En fornuftig person ville finde dette indhold upassende eller ikke respektfuldt." a_duplicate: name: other: en duplikering desc: other: Dette spørgsmål er blevet stillet før og har allerede et svar. placeholder: other: Indtast linket til eksisterende spørgsmål not_a_answer: name: other: ikke et svar desc: other: "Dette blev sendt som svar, men det forsøger ikke at besvare spørgsmålet. Det bør muligvis være en redigering, en kommentar, et andet spørgsmål, eller slettet helt." no_longer_needed: name: other: ikke længere nødvendigt desc: other: Denne kommentar er forældet, samtale-agtig eller ikke relevant for dette indlæg. something: name: other: noget andet desc: other: Dette indlæg kræver personalets opmærksomhed af en anden grund, som ikke er nævnt ovenfor. placeholder: other: Lad os vide specifikt, hvad du er bekymret over community_specific: name: other: en fællesskabsspecifik årsag desc: other: Dette spørgsmål opfylder ikke en fællesskabsretningslinje. not_clarity: name: other: kræver detaljer eller klarhed desc: other: Dette spørgsmål indeholder i øjeblikket flere spørgsmål i én. Det bør kun fokusere på ét problem. looks_ok: name: other: ser OK ud desc: other: Dette indlæg er godt som er og ikke lav kvalitet. needs_edit: name: other: har brug for redigering, og jeg gjorde det desc: other: Forbedre og ret selv problemer med dette indlæg. needs_close: name: other: skal lukkes desc: other: Et lukket spørgsmål kan ikke besvares, men du kan stadig redigere, stemme og kommentere. needs_delete: name: other: skal slettes desc: other: Dette indlæg bliver slettet. question: close: duplicate: name: other: spam desc: other: Dette spørgsmål er blevet stillet før og har allerede et svar. guideline: name: other: en fællesskabsspecifik årsag desc: other: Dette spørgsmål opfylder ikke en fællesskabsretningslinje. multiple: name: other: kræver detaljer eller klarhed desc: other: Dette spørgsmål indeholder i øjeblikket flere spørgsmål i én. Det bør kun fokusere på ét problem. other: name: other: noget andet desc: other: Dette indlæg kræver en anden grund som ikke er nævnt ovenfor. operation_type: asked: other: spurgt answered: other: besvaret modified: other: ændret deleted_title: other: Slettet spørgsmål questions_title: other: Spørgsmål tag: tags_title: other: Nøgleord no_description: other: Nøgleord har ingen beskrivelse. notification: action: update_question: other: opdateret spørgsmål answer_the_question: other: besvaret spørgsmål update_answer: other: opdateret svar accept_answer: other: accepteret svar comment_question: other: kommenteret spørgsmål comment_answer: other: kommenteret svar reply_to_you: other: svarede dig mention_you: other: nævnte dig your_question_is_closed: other: Dit spørgsmål er blevet lukket your_question_was_deleted: other: Dit spørgsmål er blevet slettet your_answer_was_deleted: other: Dit svar er blevet slettet your_comment_was_deleted: other: Din kommentar er slettet up_voted_question: other: op-stemt spørgsmål down_voted_question: other: ned-stemt spørgsmål up_voted_answer: other: op-stemt svar down_voted_answer: other: ned-stemt svar up_voted_comment: other: op-stemt kommentar invited_you_to_answer: other: inviterede dig til at svare earned_badge: other: Du har optjent "{{.BadgeName}}" emblem email_tpl: change_email: title: other: "[{{.SiteName}}] Bekræft din nye e-mailadresse" body: other: "Bekræft din nye e-mailadresse for {{.SiteName}} ved at klikke på følgende link:
\n{{.ChangeEmailUrl}}

\n\nHvis du ikke anmodede om denne ændring, ignorér venligst denne e-mail.

\n\n--
\nBemærk: Dette er en automatisk systeme-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} besvarede dit spørgsmål" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nSe den på {{.SiteName}}

\n\n--
\nBemærk: Dette er en automatisk system-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set.

\n\nAfmeld" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} inviterede dig til at svare" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Jeg tror du måske kender svaret.

\nSe den på {{.SiteName}}

\n\n--
\nBemærk: Dette er en automatisk systeme-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set.

\n\nAfmeld" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} kommenterede dit indlæg" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nSe den på {{.SiteName}}

\n\n--
\nBemærk: Dette er en automatisk system-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set.

\n\nAfmeld" new_question: title: other: "[{{.SiteName}}] Nyt spørgsmål: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nBemærk: Dette er en automatisk system-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set.

\n\nAfmeld" pass_reset: title: other: "[{{.SiteName }}] Nulstilling af adgangskode" body: other: "Nogen bad om at nulstille din adgangskode på {{.SiteName}}.

\n\nHvis det ikke var dig, kan du trygt ignorere denne e-mail.

\n\nKlik på følgende link for at vælge en ny adgangskode:
\n{{.PassResetUrl}}\n

\n\n--
\nBemærk: Dette er en automatisk system-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set." register: title: other: "[{{.SiteName}}] Bekræft din nye konto" body: other: "Velkommen til {{.SiteName}}!

\n\nKlik på følgende link for at bekræfte og aktivere din nye konto:
\n{{.RegisterUrl}}

\n\nHvis ovenstående link ikke kan klikkes på, prøv at kopiere og indsætte det i adresselinjen i din webbrowser.\n

\n\n--
\nBemærk: Dette er en automatisk system-mail svar venligst ikke på denne besked, da dit svar ikke vil blive set." test: title: other: "[{{.SiteName}}] Test E-Mail" body: other: "Dette er en test e-mail.\n

\n\n--
\nBemærk: Dette er en automatisk system-mail svar venligst ikke på denne besked, da dit svar ikke vil blive set." action_activity_type: upvote: other: stem op upvoted: other: stemt op downvote: other: stem ned downvoted: other: stemt ned accept: other: acceptér accepted: other: accepteret edit: other: rediger review: queued_post: other: Indlæg i kø flagged_post: other: Anmeldt indlæg suggested_post_edit: other: Foreslåede redigeringer reaction: tooltip: other: "{{ .Names }} og {{ .Count }} mere..." badge: default_badges: autobiographer: name: other: Autobiografer desc: other: Udfyldte profil information. certified: name: other: Certificeret desc: other: Færdiggjorde vores nye brugervejledning. editor: name: other: Redaktør desc: other: Første indlæg redigeret. first_flag: name: other: Første Markering desc: other: Først markerede et indlæg. first_upvote: name: other: Første Opstemme desc: other: Først stemte et indlæg op. first_link: name: other: Første Link desc: other: Først tilføjede et link til et andet indlæg. first_reaction: name: other: Første Reaktion desc: other: Først reagerede på indlægget. first_share: name: other: Første Deling desc: other: Først delte et indlæg. scholar: name: other: Elev desc: other: Stillede et spørgsmål og accepterede et svar. commentator: name: other: Kommentator desc: other: Efterlad 5 kommentarer. new_user_of_the_month: name: other: Månedens nye bruger desc: other: Fremragende bidrag i deres første måned. read_guidelines: name: other: Læs Retningslinjer desc: other: Læs [fællesskabsretningslinjerne]. reader: name: other: Læser desc: other: Læs alle svar i et emne med mere end 10 svar. welcome: name: other: Velkommen desc: other: Modtog en op-stemme. nice_share: name: other: Dejlig Deling desc: other: Delte et indlæg med 25 unikke besøgende. good_share: name: other: God Deling desc: other: Delte et indlæg med 300 unikke besøgende. great_share: name: other: Fantastisk Deling desc: other: Delte et indlæg med 1000 unikke besøgende. out_of_love: name: other: Af kærlighed desc: other: Brugte 50 op-stemmer på en dag. higher_love: name: other: Højere Kærlighed desc: other: Brugte 50 stemmer på en dag 5 gange. crazy_in_love: name: other: Vanvittig forelsket desc: other: Brugte 50 op-stemmer på en dag 20 gange. promoter: name: other: Promovør desc: other: Inviterede en bruger. campaigner: name: other: Kampagnefører desc: other: Inviterede 3 basis-brugere. champion: name: other: Mester desc: other: Inviterede 5 medlemmer. thank_you: name: other: Tak desc: other: Har 20 op-stemte opslag og afgav 10 op-stemmer. gives_back: name: other: Giver Tilbage desc: other: Har 100 op-stemte opslag og afgav 100 op-stemmer. empathetic: name: other: Empatisk desc: other: Har 500 op-stemte opslag og afgav 1000 op-stemmer. enthusiast: name: other: Entusiast desc: other: Besøgte 10 på hinanden følgende dage. aficionado: name: other: Aficionado desc: other: Besøgte 100 på hinanden følgende dage. devotee: name: other: Hengiven desc: other: Besøgte 365 på hinanden følgende dage. anniversary: name: other: Jubilæum desc: other: Aktivt medlem i et år, oprettet indlæg mindst én gang. appreciated: name: other: Værdsat desc: other: Modtog 1 op-stemme på 20 opslag. respected: name: other: Respekteret desc: other: Modtog 2 op-stemmer på 100 opslag. admired: name: other: Beundret desc: other: Modtog 5 op-stemmer på 300 indlæg. solved: name: other: Løst desc: other: Har fået et svar accepteret. guidance_counsellor: name: other: Vejledningsrådgiver desc: other: Har fået 10 svar accepteret. know_it_all: name: other: Bedrevidende desc: other: Har fået 50 svar accepteret. solution_institution: name: other: Institution For Løsning desc: other: Har fået 150 svar accepteret. nice_answer: name: other: Dejligt Svar desc: other: Besvar score på 10 eller mere. good_answer: name: other: Godt Svar desc: other: Besvar score på 25 eller mere. great_answer: name: other: Fantastisk Svar desc: other: Besvar score på 50 eller mere. nice_question: name: other: Godt Spørgsmål desc: other: Spørgsmål score på 10 eller mere. good_question: name: other: Godt Spørgsmål desc: other: Spørgsmål score på 25 eller mere. great_question: name: other: Fantastisk Spørgsmål desc: other: Spørgsmål score på 50 eller mere. popular_question: name: other: Populært Spørgsmål desc: other: Spørgsmål med 500 visninger. notable_question: name: other: Bemærkelsesværdigt Spørgsmål desc: other: Spørgsmål med 1.000 visninger. famous_question: name: other: Berømt Spørgsmål desc: other: Spørgsmål med 5.000 visninger. popular_link: name: other: Populært Link desc: other: Sendt et eksternt link med 50 kliks. hot_link: name: other: Hot Link desc: other: Sendt et eksternt link med 300 kliks. famous_link: name: other: Berømt Link desc: other: Sendt et eksternt link med 100 kliks. default_badge_groups: getting_started: name: other: Sådan kommer du igang community: name: other: Fællesskab posting: name: other: Oplæg # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Sådan formaterer du desc: >-
  • for at lave links

    <https://url.com>

    [Titel](https://url.com)
  • indsæt linieskift mellem paragraffer

  • _kursiv_ or **fed**

  • indskyd kode med 4 mellemrum

  • citér ved at placere > på starten af linie

  • backtick escapes `som _dette_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Forrige next: Næste page_title: question: Spørgsmål questions: Spørgsmål tag: Nøgleord tags: Nøgleord tag_wiki: nøgleord info create_tag: Opret nøgleord edit_tag: Rediger nøgleord ask_a_question: Opret spørgsmål edit_question: Rediger spørgsmål edit_answer: Rediger Svar search: Søg posts_containing: Opslag som indeholder settings: Indstillinger notifications: Notifikationer login: Log Ind sign_up: Tilmeld dig account_recovery: Konto-gendannelse account_activation: Aktivering af konto confirm_email: Bekræft e-mail account_suspended: Konto suspenderet admin: Administrator change_email: Ændre E-Mail install: Answer Installation upgrade: Answer Opgradering maintenance: Vedligeholdelse af websted users: Brugere oauth_callback: Behandler http_404: HTTP Fejl 404 http_50X: Http Fejl 500 http_403: HTTP Fejl 403 logout: Log Ud posts: Opslag ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notifikationer inbox: Indbakke achievement: Bedrifter new_alerts: Nye adviseringer all_read: Markér alle som læst show_more: Vis mere someone: Nogen inbox_type: all: Alle posts: Opslag invites: Invitationer votes: Stemmer answer: Svar question: Spørgsmål badge_award: emblem suspended: title: Din konto er blevet suspenderet until_time: "Din konto blev suspenderet indtil {{ time }}." forever: Denne bruger blev suspenderet for evigt. end: Du opfylder ikke en fællesskabsretningslinje. contact_us: Kontakt os editor: blockquote: text: Citatblok bold: text: Fed chart: text: Diagram flow_chart: Flow- diagram sequence_diagram: Sekvensdiagram class_diagram: Klassediagram state_diagram: Tilstands-diagram entity_relationship_diagram: Enheds-forhold-diagram user_defined_diagram: Brugerdefineret diagram gantt_chart: Gantt- diagram pie_chart: Cirkeldiagram code: text: Kode-eksempel add_code: Tilføj kodeeksempel form: fields: code: label: Kode msg: empty: Kode skal udfyldes. language: label: Sprog placeholder: Automatisk detektering btn_cancel: Annuller btn_confirm: Tilføj formula: text: Formel options: inline: Indlejret formel block: Formel blok heading: text: Overskrift options: h1: Overskrift 1 h2: Overskrift 2 h3: Overskrift 3 h4: Overskrift 4 h5: Overskrift 5 h6: Overskrift 6 help: text: Hjælp hr: text: Vandret streg image: text: Billede add_image: Tilføj billede tab_image: Upload billede form_image: fields: file: label: Billedfil btn: Vælg billede msg: empty: Filen skal udfyldes. only_image: Kun billedfiler er tilladt. max_size: Filstørrelse må ikke overstige {{size}} MB. desc: label: Beskriveslse tab_url: Billede-URL form_url: fields: url: label: Billede-URL msg: empty: Billede-URL skal udfyldes. name: label: Beskriveslse btn_cancel: Annuller btn_confirm: Tilføj uploading: Uploader indent: text: Indrykning outdent: text: Udrykning italic: text: Fremhævning link: text: Link add_link: Tilføj link form: fields: url: label: URL msg: empty: URL må ikke være tom. name: label: Beskriveslse btn_cancel: Annuller btn_confirm: Tilføj ordered_list: text: Nummereret liste unordered_list: text: Punktliste table: text: Tabel heading: Overskrift cell: Celle file: text: Vedhæft filer not_supported: "Understøtter ikke denne filtype. Prøv igen med {{file_type}}." max_size: "Vedhæftet filers størrelse kan ikke overstige {{size}} MB." close_modal: title: Jeg lukker dette indlæg fordi... btn_cancel: Annuller btn_submit: Indsend remark: empty: skal udfyldes. msg: empty: Vælg en grund. report_modal: flag_title: Jeg markerer for at rapportere dette indlæg som... close_title: Jeg lukker dette indlæg fordi... review_question_title: Gennemgå spørgsmål review_answer_title: Gennemgå svar review_comment_title: Gennemgå kommentar btn_cancel: Annuller btn_submit: Indsend remark: empty: skal udfyldes. msg: empty: Vælg en grund. not_a_url: URL-format er forkert. url_not_match: URL oprindelsen matcher ikke det aktuelle websted. tag_modal: title: Opret et nyt nøgleord form: fields: display_name: label: Visnings-navn msg: empty: Visnings-navn skal udfyldes. range: Visnings-navn på op til 35 tegn. slug_name: label: URL-slug desc: URL slug op til 35 tegn. msg: empty: URL slug må ikke være tom. range: URL slug op til 35 tegn. character: URL slug indeholder ikke tilladte tegn. desc: label: Beskriveslse revision: label: Revision edit_summary: label: Rediger resumé placeholder: >- Forklar kort dine ændringer (korrigeret stavning, fast grammatik, forbedret formatering) btn_cancel: Annuller btn_submit: Indsend btn_post: Send nyt tag tag_info: created_at: Oprettet edited_at: Redigeret history: Historik synonyms: title: Synonymer text: Følgende tags vil blive genmappet til empty: Ingen synonymer fundet. btn_add: Tilføj et synonym btn_edit: Rediger btn_save: Gem synonyms_text: Følgende nøgleord vil blive genmappet til delete: title: Slet dette nøgleord tip_with_posts: >-

Vi tillader ikke at slette nøgleord med opslag.

Fjern venligst dette nøgleord fra opslagene først.

tip_with_synonyms: >-

Vi tillader ikke at slette tag med indlæg.

Fjern venligst dette tag fra indlæggene først.

tip: Er du sikker på, at du vil slette? close: Luk merge: title: Sammenflet nøgleord source_tag_title: Kilde nøgleord source_tag_description: Kildenøgleordet og dets tilknyttede data vil blive omlagt til målmærket. target_tag_title: Målnøgleord target_tag_description: Et synonym mellem disse to nøgleord vil blive oprettet efter sammenlægning. no_results: Ingen nøgleord matchede btn_submit: Indsend btn_close: Luk edit_tag: title: Rediger nøgleord default_reason: Rediger nøgleord default_first_reason: Tilføj nøgleord btn_save_edits: Gem ændringer btn_cancel: Annuller dates: long_date: MMM D long_date_with_year: "D MMMM, YYYY" long_date_with_time: "MMM D, YYYY [kl.] HH:mm" now: nu x_seconds_ago: "{{count}}s siden" x_minutes_ago: "{{count}}s siden" x_hours_ago: "{{count}}t siden" hour: time day: dag hours: timer days: dag month: måned months: måneder year: År reaction: heart: hjerte smile: smil frown: rynke panden btn_label: tilføj eller fjern reaktioner undo_emoji: fortryd {{ emoji }} reaktion react_emoji: reager med {{ emoji }} unreact_emoji: ikke reager med {{ emoji }} comment: btn_add_comment: Tilføj kommentar reply_to: Svar til btn_reply: Svar btn_edit: Rediger btn_delete: Slet btn_flag: Anmeld btn_save_edits: Gem ændringer btn_cancel: Annuller show_more: "{{count}} flere kommentarer" tip_question: >- Brug kommentarer til at bede om mere information eller foreslå forbedringer. Undgå at besvare spørgsmål i kommentarer. tip_answer: >- Brug kommentarer til at svare andre brugere eller give dem besked om ændringer. Hvis du tilføjer nye oplysninger, skal du redigere dit indlæg i stedet for at kommentere. tip_vote: Det tilføjer noget nyttigt til indlægget edit_answer: title: Rediger Svar default_reason: Rediger svar default_first_reason: Tilføj svar form: fields: revision: label: Revision answer: label: Svar feedback: characters: indhold skal være mindst 6 tegn. edit_summary: label: Rediger resumé placeholder: >- Forklar kort dine ændringer (korrigeret stavning, fast grammatik, forbedret formatering) btn_save_edits: Gem ændringer btn_cancel: Annuller tags: title: Nøgleord sort_buttons: popular: Populære name: Navn newest: Nyeste button_follow: Følg button_following: Følger tag_label: spørgsmål search_placeholder: Filtrer efter nøgleord-navn no_desc: Nøgleord har ingen beskrivelse. more: Mere wiki: Info ask: title: Opret spørgsmål edit_title: Rediger spørgsmål default_reason: Rediger spørgsmål default_first_reason: Opret spørgsmål similar_questions: Lignende spørgsmål form: fields: revision: label: Revision title: label: Titel placeholder: Hvad er dit emne? Vær specifik. msg: empty: Titel må ikke være tom. range: Titel på op til 150 tegn body: label: Brødtekst msg: empty: Brødtekst skal udfyldes. hint: optional_body: Beskriv hvad spørgsmålet handler om. minimum_characters: "Beskriv hvad spørgsmålet handler om, mindst {{min_content_length}} tegn er påkrævet." tags: label: Nøgleord msg: empty: Nøgleord må ikke være tom. answer: label: Svar msg: empty: Svar må ikke være tomt. edit_summary: label: Rediger resumé placeholder: >- Forklar kort dine ændringer (korrigeret stavning, fast grammatik, forbedret formatering) btn_post_question: Indsend dit spørgsmål btn_save_edits: Gem ændringer answer_question: Besvar dit eget spørgsmål post_question&answer: Send dit spørgsmål og svar tag_selector: add_btn: Tilføj nøgleord create_btn: Opret et nyt nøgleord search_tag: Søg nøgleord hint: Beskriv hvad dit spørgsmål handler om, mindst et nøgleord er påkrævet. hint_zero_tags: Beskriv hvad dit indhold handler om. hint_more_than_one_tag: "Beskriv hvad dit indhold handler om, i det mindste {{min_tags_number}} nøgleord er påkrævet." no_result: Ingen nøgleord matchede tag_required_text: Påkrævet nøgleord (mindst én) header: nav: question: Spørgsmål tag: Nøgleord user: Brugere badges: Emblemer profile: Profil setting: Indstillinger logout: Log Ud admin: Administrator review: Gennemgå bookmark: Bogmærker moderation: Moderering search: placeholder: Søg footer: build_on: Drevet af <1>Apache Answer upload_img: name: Skift loading: indlæser... pic_auth_code: title: Captcha placeholder: Skriv teksten ovenfor msg: empty: Captcha må ikke være tomt. inactive: first: >- Du er næsten færdig! Vi har sendt en aktiveringsmail til {{mail}}. Følg venligst instruktionerne i mailen for at aktivere din konto. info: "Hvis det ikke ankommer, tjek din spam-mappe." another: >- Vi har sendt endnu en aktiverings-e-mail til dig på {{mail}}. Det kan tage nogen få minutter før den når frem; kontrollér også din spam-mappe. btn_name: Send aktiverings-e-mail igen change_btn_name: Ændre e-mail msg: empty: skal udfyldes. resend_email: url_label: Er du sikker på, at du vil sende aktiveringse-mailen? url_text: Du kan også give aktiveringslinket ovenfor til brugeren. login: login_to_continue: Log ind for at fortsætte info_sign: Har du ikke en konto? <1>Tilmeld dig info_login: Har du allerede en konto? <1>Log ind agreements: Ved at registrere dig accepterer du <1>privacy policy og <3>terms of service . forgot_pass: Glemt adgangskoden? name: label: Navn msg: empty: Navn må ikke være tomt. range: Navn skal være mellem 2 og 30 tegn i længden. character: 'Skal bruge tegnsættet "a-z", "0-9", " - . _"' email: label: E-mail msg: empty: E-mail skal udfyldes. password: label: Adgangskode msg: empty: Adgangskoden skal udfyldes. different: De indtastede adgangskoder er ikke ens account_forgot: page_title: Glemt adgangskode btn_name: Send mig gendannelsesmail send_success: >- Hvis en konto matcher {{mail}}, vil du modtage en e-mail med instruktioner om, hvordan du nulstiller din adgangskode. email: label: E-mail msg: empty: E-mail skal udfyldes. change_email: btn_cancel: Annuller btn_update: Opdater e-mailadresse send_success: >- Hvis en konto matcher {{mail}}, vil du modtage en e-mail med instruktioner om, hvordan du nulstiller din adgangskode. email: label: Ny e-mail msg: empty: E-mail skal udfyldes. oauth: connect: Forbind med {{ auth_name }} remove: Fjern {{ auth_name }} oauth_bind_email: subtitle: Tilføj en gendannelsese-mail til din konto. btn_update: Opdater e-mailadresse email: label: E-mail msg: empty: E-mail skal udfyldes. modal_title: Email eksisterer allerede. modal_content: Denne e-mailadresse er allerede registreret. Er du sikker på, at du vil oprette forbindelse til den eksisterende konto? modal_cancel: Ændre e-mail modal_confirm: Opret forbindelse til den eksisterende konto password_reset: page_title: Nulstil adgangskode btn_name: Nulstil min adgangskode reset_success: >- Du har ændret din adgangskode. Du vil blive omdirigeret til siden log ind. link_invalid: >- Beklager, dette link til nulstilling af adgangskode er ikke længere gyldigt. Måske er din adgangskode allerede nulstillet? to_login: Fortsæt til log-ind siden password: label: Adgangskode msg: empty: Adgangskoden skal udfyldes. length: Længden skal være mellem 8 og 32 tegn different: De indtastede adgangskoder er ikke ens password_confirm: label: Bekræft den nye adgangskode settings: page_title: Indstillinger goto_modify: Gå til at ændre nav: profile: Profil notification: Notifikationer account: Konto interface: Grænseflade profile: heading: Profil btn_name: Gem display_name: label: Visnings-navn msg: Visnings-navn skal udfyldes. msg_range: Visningsnavnet skal være 2-30 tegn i længden. username: label: Brugernavn caption: Man kan nævne dig som "@username". msg: Brugernavn skal udfyldes. msg_range: Brugernavn skal være 2-30 tegn i længden. character: 'Skal bruge tegnsættet "a-z", "0-9", "- . _"' avatar: label: Profilbillede gravatar: Gravatar gravatar_text: Du kan ændre billede på custom: Brugerdefineret custom_text: Du kan uploade dit billede. default: System msg: Upload en avatar bio: label: Om mig website: label: Websted placeholder: "https://example.com" msg: Forkert format på websted location: label: Placering placeholder: "By, land" notification: heading: Email-notifikationer turn_on: Slå til inbox: label: Notifikationer i indbakken description: Svar på dine spørgsmål, kommentarer, invitationer og mere. all_new_question: label: Alle nye spørgsmål description: Få besked om alle nye spørgsmål. Op til 50 spørgsmål om ugen. all_new_question_for_following_tags: label: Alle nye spørgsmål til følgende nøgleord description: Få besked om nye spørgsmål til følgende nøgleord. account: heading: Konto change_email_btn: Ændre e-mail change_pass_btn: Skift adgangskode change_email_info: >- Vi har sendt en e-mail til denne adresse. Følg venligst bekræftelsesinstruktionerne. email: label: E-mail new_email: label: Ny e-mail msg: Ny e-mail skal udfyldes. pass: label: Nuværende adgangskode msg: Adgangskoden skal udfyldes. password_title: Adgangskode current_pass: label: Nuværende adgangskode msg: empty: Nuværende adgangskode skal udfyldes. length: Længden skal være mellem 8 og 32 tegn. different: De to indtastede adgangskoder er ikke ens. new_pass: label: Ny adgangskode pass_confirm: label: Bekræft den nye adgangskode interface: heading: Grænseflade lang: label: Grænseflade sprog text: Brugergrænseflade sprog. Det vil ændres, når du opdaterer siden. my_logins: title: Mine log ind label: Log ind eller tilmeld dig på dette websted ved hjælp af disse konti. modal_title: Fjern login modal_content: Er du sikker på, at du vil fjerne dette login fra din konto? modal_confirm_btn: Slet remove_success: Fjernet toast: update: opdatering gennemført update_password: Adgangskoden er ændret. flag_success: Tak for at anmelde. forbidden_operate_self: Forbudt at operere på dig selv review: Din revision vil blive vist efter gennemgang. sent_success: Sendt related_question: title: Relateret answers: svar linked_question: title: Knyttet description: Opslag knyttet til no_linked_question: Intet indhold linket fra dette indhold. invite_to_answer: title: Inviter personer desc: Inviter personer, som du tror, kan svare. invite: Inviter til at svare add: Tilføj personer search: Søg personer question_detail: action: Handling created: Oprettet Asked: Spurgt asked: spurgt update: Ændret Edited: Redigeret edit: redigeret commented: kommenteret Views: Set Follow: Følg Following: Følger follow_tip: Følg dette spørgsmål for at modtage notifikationer answered: besvaret closed_in: Lukket om show_exist: Vis eksisterende spørgsmål. useful: Nyttigt question_useful: Det er nyttigt og klart question_un_useful: Det er uklart eller ikke nyttigt question_bookmark: Bogmærk dette spørgsmål answer_useful: Det er nyttigt answer_un_useful: Det er ikke nyttigt answers: title: Svar score: Bedømmelse newest: Nyeste oldest: Ældste btn_accept: Acceptér btn_accepted: Accepteret write_answer: title: Dit Svar edit_answer: Redigér mit eksisterende svar btn_name: Indsend dit svar add_another_answer: Tilføj endnu et svar confirm_title: Fortsæt med at svare continue: Forsæt confirm_info: >-

Er du sikker på, at du vil tilføje et andet svar?

Du kan i stedet bruge redigeringslinket til at forfine og forbedre dit eksisterende svar.

empty: Svar skal udfyldes. characters: indhold skal være mindst 6 tegn. tips: header_1: Tak for dit svar li1_1: Vær sikker på at besvare spørgsmålet. Giv oplysninger og del din forskning. li1_2: Begrund eventuelle udsagn med referencer eller personlige erfaringer. header_2: Men undgå... li2_1: Spørger om hjælp, søger afklaring, eller reagerer på andre svar. reopen: confirm_btn: Genåbn title: Genåbn dette indlæg content: Er du sikker på, at du vil genåbne? list: confirm_btn: Liste title: Sæt dette indlæg på listen content: Er du sikker på du vil sætte på listen? unlist: confirm_btn: Fjern fra listen title: Fjern dette indlæg fra listen content: Er du sikker på at du vil fjerne fra listen? pin: title: Fastgør dette indlæg content: Er du sikker på, at du ønsker at fastgøre globalt? Dette indlæg vises øverst på alle indlægs-lister. confirm_btn: Fastgør delete: title: Slet dette indlæg question: >- Vi anbefaler ikke, at sletter spørgsmål med svar, fordi det fratager fremtidige læsere denne viden.

Gentaget sletning af besvarede spørgsmål kan resultere i, at din konto bliver blokeret fra at spørge. Er du sikker på, at du ønsker at slette? answer_accepted: >-

Vi anbefaler ikke at slette accepteret svar fordi det fratager fremtidige læsere denne viden.

Gentagen sletning af accepterede svar kan resultere i, at din konto bliver blokeret fra besvarelse. Er du sikker på, at du ønsker at slette? other: Er du sikker på, at du vil slette? tip_answer_deleted: Dette svar er blevet slettet undelete_title: Genopret dette indlæg undelete_desc: Er du sikker på du ønsker at genoprette? btns: confirm: Bekræft cancel: Annuller edit: Rediger save: Gem delete: Slet undelete: Genopret list: Sæt på liste unlist: Fjern fra liste unlisted: Fjernet fra liste login: Log ind signup: Opret konto logout: Log Ud verify: Verificér create: Opret approve: Godkend reject: Afvis skip: Spring Over discard_draft: Kassér udkast pinned: Fastgjort all: Alle question: Spørgsmål answer: Svar comment: Kommentar refresh: Genopfrisk resend: Send igen deactivate: Deaktiver active: Aktiv suspend: Suspendér unsuspend: Ophæv suspendering close: Luk reopen: Genåbn ok: Ok light: Lys dark: Mørk system_setting: Systemindstilling default: Standard reset: Nulstil tag: Nøgleord post_lowercase: indlæg filter: Filtrer ignore: Ignorér submit: Indsend normal: Normal closed: Lukket deleted: Slettet deleted_permanently: Slettet permanent pending: Ventende more: Mere view: Vis card: Kort compact: Kompakt display_below: Vis nedenfor always_display: Vis altid or: eller back_sites: Tilbage til websteder search: title: Søgeresultater keywords: Nøgleord options: Muligheder follow: Følg following: Følger counts: "{{count}} Resultater" counts_loading: "... Resultater" more: Mere sort_btns: relevance: Relevans newest: Nyeste active: Aktiv score: Bedømmelse more: Mere tips: title: Avancerede Søgetips tag: "<1>[tag] søgning med et nøgleord" user: "<1>user:username søgning efter forfatter" answer: "<1>answers:0 ubesvarede spørgsmål" score: "<1>score:3 opslag med 3+ score" question: "<1>is:question søgespørgsmål" is_answer: "<1>is:answer søgesvar" empty: Vi kunne ikke finde noget.
Prøv forskellige eller mindre specifikke søgeord. share: name: Del copy: Kopiér link via: Del indlæg via... copied: Kopieret facebook: Del på Facebook twitter: Del til X cannot_vote_for_self: Du kan ikke stemme på dit eget indlæg. modal_confirm: title: Fejl... delete_permanently: title: Slet permanent content: Er du sikker på du vil slette permanent? account_result: success: Din nye konto er bekræftet. Du vil blive omdirigeret til hjemmesiden. link: Fortsæt til startside oops: Hovsa! invalid: Linket, du brugte, virker ikke længere. confirm_new_email: Din e-mail er blevet opdateret. confirm_new_email_invalid: >- Beklager, dette bekræftelseslink er ikke længere gyldigt. Måske blev din e-mail allerede ændret? unsubscribe: page_title: Afmeld success_title: Afmelding Lykkedes success_desc: Du er blevet fjernet fra denne abonnentliste og vil ikke modtage yderligere e-mails fra os. link: Skift indstillinger question: following_tags: Følger Nøgleord edit: Rediger save: Gem follow_tag_tip: Følg nøgleord for at udvælge dine spørgsmål. hot_questions: Populære Spørgsmål all_questions: Alle Spørgsmål x_questions: "{{ count }} Spørgsmål" x_answers: "{{ count }} svar" x_posts: "{{ count }} Opslag" questions: Spørgsmål answers: Svar newest: Nyeste active: Aktiv hot: Populært frequent: Ofte recommend: Anbefal score: Bedømmelse unanswered: Ubesvaret modified: ændret answered: besvaret asked: spurgt closed: lukket follow_a_tag: "Følg et nøgleord\n" more: Mere personal: overview: Oversigt answers: Svar answer: svar questions: Spørgsmål question: spørgsmål bookmarks: Bogmærker reputation: Omdømme comments: Kommentarer votes: Stemmer badges: Emblemer newest: Nyeste score: Bedømmelse edit_profile: Rediger profil visited_x_days: "Besøgte {{ count }} dage" viewed: Set joined: Tilmeldt comma: "," last_login: Set about_me: Om Mig about_me_empty: "// Hej, Verden!" top_answers: Populære Svar top_questions: Populære Spørgsmål stats: Statistik list_empty: Ingen opslag fundet.
Måske vil du vælge en anden fane? content_empty: Ingen opslag fundet. accepted: Accepteret answered: besvaret asked: spurgt downvoted: nedstemt mod_short: MOD mod_long: Moderatorer x_reputation: omdømme x_votes: stemmer modtaget x_answers: svar x_questions: spørgsmål recent_badges: Seneste emblemer install: title: Installation next: Næste done: Udført config_yaml_error: Kan ikke oprette filen config.yaml. lang: label: Vælg et sprog db_type: label: Database type db_username: label: Brugernavn placeholder: rod msg: Brugernavn skal udfyldes. db_password: label: Adgangskode placeholder: rod msg: Adgangskoden skal udfyldes. db_host: label: Database host placeholder: "db:3306" msg: Database host skal udfyldes. db_name: label: Database navn placeholder: answer msg: Databasenavn skal udfyldes. db_file: label: Databasefil placeholder: /data/answer.db msg: Databasefil skal udfyldes. ssl_enabled: label: Aktiver SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL-tilstand ssl_root_cert: placeholder: sslrootcert filsti msg: Sti til sslrootcert fil kan ikke være tom ssl_cert: placeholder: sslrootcert filsti msg: Sti til sslrootcert fil kan ikke være tom ssl_key: placeholder: sslrootcert filsti msg: Sti til sslrootcert fil kan ikke være tom config_yaml: title: Opret config.yaml label: Filen config.yaml blev oprettet. desc: >- Du kan manuelt oprette filen <1>config.yaml i mappen <1>/var/wwww/xxx/ og indsætte følgende tekst i den. info: Når du har gjort det, skal du klikke på "Næste" knappen. site_information: Websted Information admin_account: Administrator Konto site_name: label: Websted navn msg: Websted-navn skal udfyldes. msg_max_length: Webstedsnavn kan ikke være længere end 30 tegn. site_url: label: Websted URL text: Adressen på dit websted. msg: empty: Webstedets URL skal udfyldes. incorrect: Websteds URL forkert format. max_length: WebstedsURL skal højst være 512 tegn. contact_email: label: Kontakt e-mail text: E-mailadresse på nøglekontakt ansvarlig for dette websted. msg: empty: Kontakt-e-mail skal udfyldes. incorrect: Ugyldig kontakt e-mail adresse. login_required: label: Privat switch: Log ind påkrævet text: Kun brugere som er logget ind har adgang til dette fællesskab. admin_name: label: Navn msg: Navn skal udfyldes. character: 'Skal bruge tegnsættet "a-z", "0-9", " - . _"' msg_max_length: Navn skal være mellem 2 og 30 tegn i længden. admin_password: label: Adgangskode text: >- Du skal bruge denne adgangskode for at logge ind. Opbevar den et sikkert sted. msg: Adgangskoden skal udfyldes. msg_min_length: Adgangskoden skal være mindst 8 tegn. msg_max_length: Adgangskoden skal højst udgøre 32 tegn. admin_confirm_password: label: "Bekræft adgangskode" text: "Indtast venligst din adgangskode igen for at bekræfte." msg: "Bekræft adgangskoden stemmer ikke overens." admin_email: label: E-mail text: Du skal bruge denne e-mail for at logge ind. msg: empty: E-mail skal udfyldes. incorrect: Ugyldig e-mail adresse. ready_title: Dit websted er klar ready_desc: >- Hvis du nogensinde har lyst til at ændre flere indstillinger, kan du besøge <1>admin-sektion; find det i site-menuen. good_luck: "Hav det sjovt, og held og lykke!" warn_title: Advarsel warn_desc: >- Filen <1>config.yaml findes allerede. Hvis du har brug for at nulstille en af konfigurationselementerne i denne fil, så slet den først. install_now: Du kan prøve <1>at installere nu. installed: Allerede installeret installed_desc: >- Du synes allerede at være installeret. For at geninstallere skal du først rydde dine gamle databasetabeller. db_failed: Database forbindelse mislykkedes db_failed_desc: >- Det betyder enten, at databaseinformationen i din <1>config. aml fil er forkert eller at kontakt med databaseserveren ikke kunne etableres. Dette kan betyde, at din værts databaseserver er nede. counts: views: visninger votes: stemmer answers: svar accepted: Accepteret page_error: http_error: HTTP Fejl {{ code }} desc_403: Du har ikke adgang til denne side. desc_404: Denne side findes desværre ikke. desc_50X: Der skete en fejl på serveren og den kunne ikke fuldføre din anmodning. back_home: Tilbage til forsiden page_maintenance: desc: "Vi laver vedligeholdelse, men er snart tilbage igen." nav_menus: dashboard: Kontrolpanel contents: Indhold questions: Spørgsmål answers: Svar users: Brugere badges: Emblemer flags: Anmeldelser settings: Indstillinger general: Generelt interface: Brugerflade smtp: SMTP branding: Branding legal: Jura write: Skrivning terms: Vilkår tos: Betingelser for brug privacy: Privatliv seo: SEO customize: Tilpas themes: Temaer login: Log Ind privileges: Rettigheder plugins: Plugins installed_plugins: Installerede Plugins apperance: Udseende community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Velkommen til {{site_name}} user_center: login: Log Ind qrcode_login_tip: Brug {{ agentName }} til at scanne QR-koden og logge ind. login_failed_email_tip: Log ind mislykkedes, tillad denne app at få adgang til dine e-mail-oplysninger, før du prøver igen. badges: modal: title: Tillykke content: Du har optjent et nyt badge. close: Luk confirm: Se emblemer title: emblem awarded: Tildelt earned_×: Optjent ×{{ number }} ×_awarded: "{{ number }} tildelt" can_earn_multiple: Du kan tjene dette flere gange. earned: Optjent admin: admin_header: title: Administrator dashboard: title: Kontrolpanel welcome: Velkommen til Administration! site_statistics: Statistik for webstedet questions: "Spørgsmål:" resolved: "Løst" unanswered: "Ubesvaret:" answers: "Svar:" comments: "Kommentarer:" votes: "Stemmer:" users: "Brugere:" flags: "Anmeldelser:" reviews: "Gennemgange:" site_health: Websteds sundhed version: "Version:" https: "HTTPS:" upload_folder: "Upload mappe:" run_mode: "Kørselstilstand:" private: Privat public: Offentlig smtp: "SMTP:" timezone: "Tidszone:" system_info: System information go_version: "Go version:" database: "Database:" database_size: "Database størrelse:" storage_used: "Anvendt lagerplads:" uptime: "Oppetid:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Kontakt os forum: Forum documents: Dokumenter feedback: Tilbagemelding support: Support review: Gennemgå config: Konfiguration update_to: Opdatér til latest: Seneste check_failed: Tjek mislykkedes "yes": "Ja" "no": "Nej" not_allowed: Ikke tilladt allowed: Tilladt enabled: Aktiveret disabled: Deaktiveret writable: Skrivbar not_writable: Ikke skrivbar flags: title: Anmeldelser pending: Ventende completed: Gennemført flagged: Anmeldt flagged_type: Anmeldt{{ type }} created: Oprettet action: Handling review: Gennemgå user_role_modal: title: Skift brugerrolle til... btn_cancel: Annuller btn_submit: Indsend new_password_modal: title: Angiv ny adgangskode form: fields: password: label: Adgangskode text: Brugeren vil blive logget ud og skal logge ind igen. msg: Adgangskoden skal være på 8- 32 tegn. btn_cancel: Annuller btn_submit: Indsend edit_profile_modal: title: Rediger profil form: fields: display_name: label: Visnings-navn msg_range: Visningsnavnet skal være 2-30 tegn i længden. username: label: Brugernavn msg_range: Brugernavn skal være 2-30 tegn i længden. email: label: E-mail msg_invalid: Ugyldig E-Mail Adresse. edit_success: Redigering lykkedes btn_cancel: Annuller btn_submit: Indsend user_modal: title: Tilføj ny bruger form: fields: users: label: Masse-tilføj brugere placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Adskil “navn, e-mail, adgangskode” med kommaer. Én bruger pr. linje. msg: "Indtast venligst brugerens e-mail, en pr. linje." display_name: label: Visnings-navn msg: Visningsnavnet skal være 2-30 tegn i længden. email: label: E-mail msg: E-mail er ugyldig. password: label: Adgangskode msg: Adgangskoden skal være 8- 32 tegn. btn_cancel: Annuller btn_submit: Indsend users: title: Brugere name: Navn email: E-mail reputation: Omdømme created_at: Oprettet Tidspunkt delete_at: Slettet Tidspunkt suspend_at: Suspenderet Tidspunkt suspend_until: Suspenderet indtil status: Status role: Rolle action: Handling change: Ændre all: Alle staff: Ansatte more: Mere inactive: Inaktiv suspended: Suspenderet deleted: Slettet normal: Normal Moderator: Moderator Admin: Administrator User: Bruger filter: placeholder: "Filtrer efter navn, user:id" set_new_password: Angiv ny adgangskode edit_profile: Rediger profil change_status: Ændre status change_role: Ændre rolle show_logs: Vis logfiler add_user: Tilføj bruger deactivate_user: title: Deaktiver bruger content: En inaktiv bruger skal bekræfte deres e-mail igen. delete_user: title: Slet denne bruger content: Er du sikker på, at du vil slette denne bruger? Dette er permanent! remove: Fjern deres indhold label: Fjern alle spørgsmål, svar, kommentarer osv. text: Tjek ikke dette, hvis du kun ønsker at slette brugerens konto. suspend_user: title: Suspendér denne bruger content: En suspenderet bruger kan ikke logge ind. label: Hvor længe vil brugeren blive suspenderet til? forever: For evigt questions: page_title: Spørgsmål unlisted: Fjernet fra liste post: Indlæg votes: Stemmer answers: Svar created: Oprettet status: Status action: Handling change: Ændre pending: Ventende filter: placeholder: "Filtrer efter titel, question:id" answers: page_title: Svar post: Indlæg votes: Stemmer created: Oprettet status: Status action: Handling change: Ændre filter: placeholder: "Filtrer efter titel, answer:id" general: page_title: Generelt name: label: Websted navn msg: Websted-navn skal udfyldes. text: "Navnet på dette websted, som bruges i title-nøgleord." site_url: label: Websted URL msg: Websted-URL skal udfyldes. validate: Angiv et gyldigt URL. text: Adressen på dit websted. short_desc: label: Kort beskrivelse af websted msg: Kort beskrivelse af websted skal udfyldes. text: "Kort beskrivelse, som anvendt i title-nøgleord på hjemmesiden." desc: label: Websted beskrivelse msg: Webstedsbeskrivelse skal udfyldes. text: "Beskriv dette websted i en sætning, som bruges i meta description nøgleord." contact_email: label: Kontakt e-mail msg: Kontakt-e-mail skal udfyldes. validate: Kontakt-e-mail er ugyldig. text: E-mailadresse på nøglekontakt ansvarlig for dette websted. check_update: label: Opdatering af software text: Søg automatisk efter opdateringer interface: page_title: Brugerflade language: label: Brugerflade sprog msg: Brugerflade-sprog skal udfyldes. text: Brugergrænseflade sprog. Det vil ændres, når du opdaterer siden. time_zone: label: Tidszone msg: Tidszone skal udfyldes. text: Vælg en by i samme tidszone som dig selv. avatar: label: Standard avatar text: For brugere uden en brugerdefineret avatar. gravatar_base_url: label: Gravatar base-URL text: URL for Gravatar-udbyderens API-base. Ignoreres når tom. smtp: page_title: SMTP from_email: label: Fra e-mail msg: Fra e-mail skal udfyldes. text: E-mail-adressen som e-mails sendes fra. from_name: label: Fra navn msg: Fra navn skal udfyldes. text: Navnet som e-mails sendes fra. smtp_host: label: SMTP host msg: SMTP host skal udfyldes. text: Din mail-server. encryption: label: Kryptering msg: Kryptering skal udfyldes. text: For de fleste servere er SSL den anbefalede indstilling. ssl: SSL tls: TLS none: Ingen smtp_port: label: SMTP port msg: SMTP port skal være nummer 1 ~ 65535. text: Porten til din mailserver. smtp_username: label: SMTP brugernavn msg: SMTP brugernavn skal udfyldes. smtp_password: label: SMTP adgangskode msg: SMTP adgangskode skal udfyldes. test_email_recipient: label: Test e-mail modtagere text: Angiv e-mail-adresse, der vil modtage test-beskedder. msg: Test e-mail modtagere er ugyldige smtp_authentication: label: Aktiver autentificering title: SMTP autentificering msg: SMTP autentificering skal udfyldes. "yes": "Ja" "no": "Nej" branding: page_title: Branding logo: label: Logo msg: Logo skal udfyldes. text: Logoet billede øverst til venstre på dit websted. Brug et bredt rektangulært billede med en højde på 56 og et breddeforhold større end 3:1. Hvis efterladt tom, vil webstedets titeltekst blive vist. mobile_logo: label: Mobil logo text: Logoet bruges på mobile version af dit websted. Brug et bredt rektangulært billede med en højde på 56. Hvis efterladt tom, vil billedet fra indstillingen "logo" blive brugt. square_icon: label: Kvadratisk ikon msg: Kvadratisk ikon skal udfyldes. text: Billede brugt som basis for metadata-ikoner. Bør være større end 512x512. favicon: label: Favicon text: En favicon til dit websted. For at fungere korrekt over en CDN skal det være en png. Vil blive ændret til 32x32. Hvis efterladt tomt, vil "firkantet ikon" blive brugt. legal: page_title: Jura terms_of_service: label: Betingelser for brug text: "Du kan tilføje servicevilkår her. Hvis du allerede har et dokument hostet et andet sted, så angiv den fulde URL her." privacy_policy: label: Privatlivspolitik text: "Du kan tilføje privatlivspolitik indhold her. Hvis du allerede har et dokument hostet et andet sted, så angiv den fulde URL her." external_content_display: label: Eksternt indhold text: "Indholdet indeholder billeder, videoer og medier indlejret fra eksterne hjemmesider." always_display: Vis altid eksternt indhold ask_before_display: Spørg før visning af eksternt indhold write: page_title: Files min_content: label: Minimum længde for spørgsmål-tekst text: Mindste tilladte spørgsmåls længde i tegn. restrict_answer: title: Skriv svar label: Hver bruger kan kun skrive et svar for det samme spørgsmål text: "Slå fra for at give brugerne mulighed for at skrive flere svar på det samme spørgsmål, hvilket kan forårsage svar at være ufokuseret." min_tags: label: "Minimumsnøgleord pr. spørgsmål" text: "Minimum antal nøgleord kræves i et spørgsmål." recommend_tags: label: Anbefal nøgleord text: "Anbefal nøgleord vil som standard blive vist i dropdown-listen." msg: contain_reserved: "anbefalede nøgleord kan ikke indeholde reserverede tags" required_tag: title: Angiv påkrævede nøgleord label: Sæt “Anbefal nøgleord” som påkrævede nøgleord text: "Hvert nyt spørgsmål skal have mindst et anbefalet nøgleord" reserved_tags: label: Reserverede nøgleord text: "Reserverede nøgleord kan kun bruges af moderator." image_size: label: Maks billedstørrelse (MB) text: "Den maksimale billedupload størrelse." attachment_size: label: Max vedhæftningsstørrelse (MB) text: "Den maksimale vedhæftede filer upload størrelse." image_megapixels: label: Max billed megapixels text: "Maksimalt antal megapixels tilladt for et billede." image_extensions: label: Godkendte billedudvidelser text: "En liste over tilladte fil udvidelser til billedvisning, adskilt med kommaer." attachment_extensions: label: Autoriserede vedhæftningsudvidelser text: "En liste over fil udvidelser tilladt for upload, adskil med kommaer. ADVARSEL: At tillade uploads kan forårsage sikkerhedsproblemer." seo: page_title: SEO permalink: label: Permalink text: Brugerdefinerede URL-strukturer kan forbedre brugervenlighed og fremadrettet kompatibilitet af dine links. robots: label: robots.txt text: Dette vil permanent tilsidesætte eventuelle relaterede webstedsindstillinger. themes: page_title: Temaer themes: label: Temaer text: Vælg et eksisterende tema. color_scheme: label: Farveskema navbar_style: label: Navigationsbjælke baggrundsstil primary_color: label: Primær farve text: Ændre farver, der bruges af dine temaer layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS og HTML custom_css: label: Brugerdefineret CSS text: > head: label: Head text: > header: label: Overskrift text: > footer: label: Sidefod text: Dette indsættes før </body>. sidebar: label: Sidebjælke text: Dette vil indsætte i sidebjælken. login: page_title: Log Ind membership: title: Medlemskab label: Tillad nye registreringer text: Slå fra for at forhindre at nogen opretter en ny konto. email_registration: title: E-mail-registrering label: Tillad e-mail registrering text: Slå fra for at forhindre, at der oprettes en ny konto via e-mail. allowed_email_domains: title: Tilladte e-mail-domæner text: E-mail-domæner som brugere skal registrere konti med. Et domæne pr. linje. Ignoreres når tomt. private: title: Privat label: Log ind påkrævet text: Kun brugere som er logget ind har adgang til dette fællesskab. password_login: title: Adgangskode log ind label: Tillad e-mail og adgangskode login text: "ADVARSEL: Hvis du slår fra, kan du muligvis ikke logge ind, hvis du ikke tidligere har konfigureret en anden loginmetode." installed_plugins: title: Installerede Plugins plugin_link: Plugins udvider og udvider funktionaliteten. Du kan finde plugins i <1>Plugin Repository. filter: all: Alle active: Aktiv inactive: Inaktiv outdated: Forældet plugins: label: Plugins text: Vælg et eksisterende plugin. name: Navn version: Version status: Status action: Handling deactivate: Deaktiver activate: Aktivér settings: Indstillinger settings_users: title: Brugere avatar: label: Standard avatar text: For brugere uden en brugerdefineret avatar. gravatar_base_url: label: Gravatar base-URL text: URL for Gravatar-udbyderens API-base. Ignoreres når tom. profile_editable: title: Profil redigerbar allow_update_display_name: label: Tillad brugere at ændre deres visningsnavn allow_update_username: label: Tillad brugere at ændre deres brugernavn allow_update_avatar: label: Tillad brugere at ændre deres profilbillede allow_update_bio: label: Tillad brugere at ændre deres om-mig allow_update_website: label: Tillad brugere at ændre deres hjemmeside allow_update_location: label: Tillad brugere at ændre deres placering privilege: title: Rettigheder level: label: Omdømme påkrævet niveau text: Vælg det omdømme der kræves for rettighederne msg: should_be_number: input skal være et tal number_larger_1: tal skal være lig med eller større end 1 badges: action: Handling active: Aktiv activate: Aktiver all: Alle awards: Præmier deactivate: Deaktiver filter: placeholder: Filtrer efter navn, user:id group: Gruppe inactive: Inaktiv name: Navn show_logs: Vis log status: Status title: Emblemer apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (valgfrit) empty: skal udfyldes invalid: er ugyldigt btn_submit: Gem not_found_props: "Nødvendig egenskab {{ key }} ikke fundet." select: Vælg page_review: review: Gennemgå proposed: foreslået question_edit: Rediger spørgsmål answer_edit: Svar redigér tag_edit: Nøgleord redigér edit_summary: Rediger resumé edit_question: Rediger spørgsmål edit_answer: Rediger svar edit_tag: Rediger nøgleord empty: Ingen gennemgangsopgaver tilbage. approve_revision_tip: Godkender du denne revision? approve_flag_tip: Godkender du denne anmeldelse? approve_post_tip: Godkender du dette indlæg? approve_user_tip: Godkender du denne bruger? suggest_edits: Foreslåede redigeringer flag_post: Anmeld indlæg flag_user: Anmeld bruger queued_post: Indlæg i kø queued_user: Brugere i kø filter_label: Type reputation: omdømme flag_post_type: Anmeld dette indlæg som {{ type }}. flag_user_type: Anmeldte dette indlæg som {{ type }}. edit_post: Rediger opslag list_post: Sæt indlæg på liste unlist_post: Fjern indlæg fra liste timeline: undeleted: genskabt deleted: slettet downvote: stem ned upvote: stem op accept: acceptér cancelled: annulleret commented: kommenteret rollback: tilbagerul edited: redigeret answered: besvaret asked: spurgt closed: lukket reopened: genåbnet created: oprettet pin: fastgjort unpin: frigjort show: sat på liste hide: fjernet fra liste title: "Historik for" tag_title: "Tidslinje for" show_votes: "Vis stemmer" n_or_a: Ikke Relevant title_for_question: "Tidslinje for" title_for_answer: "Tidslinje for svar på {{ title }} af {{ author }}" title_for_tag: "Tidslinje for nøgleord" datetime: Datetime type: Type by: Af comment: Kommentar no_data: "Vi kunne ikke finde noget." users: title: Brugere users_with_the_most_reputation: Brugere med det højeste omdømme scorer denne uge users_with_the_most_vote: Brugere, der stemte mest i denne uge staffs: Vores fællesskabs personale reputation: omdømme votes: stemmer prompt: leave_page: Er du sikker på, at du vil forlade siden? changes_not_save: Dine ændringer er muligvis ikke gemt. draft: discard_confirm: Er du sikker på, at du vil kassere dit udkast? messages: post_deleted: Dette indlæg er blevet slettet. post_cancel_deleted: Dette opslag er blevet genoprettet. post_pin: Dette indlæg er blevet fastgjort. post_unpin: Dette indlæg er blevet frigjort. post_hide_list: Dette indlæg er blevet skjult fra listen. post_show_list: Dette indlæg er blevet vist på listen. post_reopen: Dette indlæg er blevet genåbnet. post_list: Dette indlæg er blevet listet. post_unlist: Dette indlæg er blevet aflistet. post_pending: Dit indlæg afventer gennemgang. Dette er en forhåndsvisning, det vil være synligt, når det er blevet godkendt. post_closed: Dette opslag er blebet lukket. answer_deleted: Dette svar er blevet slettet. answer_cancel_deleted: Dette svar er blevet genoprettet. change_user_role: Denne brugers rolle er blevet ændret. user_inactive: Denne bruger er allerede inaktiv. user_normal: Denne bruger er allerede normal. user_suspended: Denne bruger er blevet suspenderet. user_deleted: Denne bruger er slettet. user_added: User has been added successfully. badge_activated: Dette emblem er blevet aktiveret. badge_inactivated: Dette emblem er blevet inaktiveret. users_deleted: Disse bruger er blevet slettet. posts_deleted: Disse spørgsmål er blevet slettet. answers_deleted: Disse svar er blevet slettet. copy: Kopier til udklipsholder copied: Kopieret external_content_warning: Eksterne billeder/medier vises ikke. ================================================ FILE: i18n/de_DE.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Erfolgreich. unknown: other: Unbekannter Fehler. request_format_error: other: Format der Anfrage ist ungültig. unauthorized_error: other: Nicht autorisiert. database_error: other: Datenbank-Fehler. forbidden_error: other: Verboten. duplicate_request_error: other: Doppelte Einreichung. action: report: other: Melden edit: other: Bearbeiten delete: other: Löschen close: other: Schließen reopen: other: Wieder öffnen forbidden_error: other: Verboten. pin: other: Anpinnen hide: other: Von Liste nehmen unpin: other: Loslösen show: other: Liste invite_someone_to_answer: other: Bearbeiten undelete: other: Wiederherstellen merge: other: Zusammenführen role: name: user: other: Benutzer admin: other: Admin moderator: other: Moderator description: user: other: Standard ohne speziellen Zugriff. admin: other: Habe die volle Berechtigung, auf die Seite zuzugreifen. moderator: other: Hat Zugriff auf alle Beiträge außer Admin-Einstellungen. privilege: level_1: description: other: Level 1 (weniger Reputation für privates Team, Gruppen) level_2: description: other: Level 2 (niedrige Reputation für Startup-Community) level_3: description: other: Level 3 (hohe Reputation für eine reife Community) level_custom: description: other: Benutzerdefinierter Level rank_question_add_label: other: Fragen stellen rank_answer_add_label: other: Antwort schreiben rank_comment_add_label: other: Kommentar schreiben rank_report_add_label: other: Melden rank_comment_vote_up_label: other: Kommentar upvoten rank_link_url_limit_label: other: Mehr als 2 Links gleichzeitig posten rank_question_vote_up_label: other: Frage upvoten rank_answer_vote_up_label: other: Antwort upvoten rank_question_vote_down_label: other: Frage downvoten rank_answer_vote_down_label: other: Antwort downvoten rank_invite_someone_to_answer_label: other: Jemanden zum Antworten einladen rank_tag_add_label: other: Neuen Tag erstellen rank_tag_edit_label: other: Tag-Beschreibung bearbeiten (muss überprüft werden) rank_question_edit_label: other: Frage eines anderen bearbeiten (muss überarbeitet werden) rank_answer_edit_label: other: Antwort eines anderen bearbeiten (muss überarbeitet werden) rank_question_edit_without_review_label: other: Frage eines anderen ohne Überprüfung bearbeiten rank_answer_edit_without_review_label: other: Antwort eines anderen ohne Überprüfung bearbeiten rank_question_audit_label: other: Frageänderungen überprüfen rank_answer_audit_label: other: Bearbeitete Antworten überprüfen rank_tag_audit_label: other: Tag-Bearbeitungen überprüfen rank_tag_edit_without_review_label: other: Tag-Beschreibung ohne Überprüfung bearbeiten rank_tag_synonym_label: other: Tag-Synonyme verwalten email: other: E-Mail e_mail: other: E-Mail password: other: Passwort pass: other: Passwort old_pass: other: Aktuelles Passwort original_text: other: Dieser Beitrag email_or_password_wrong_error: other: E-Mail und Passwort stimmen nicht überein. error: common: invalid_url: other: Ungültige URL. status_invalid: other: Ungültiger Status. password: space_invalid: other: Passwort darf keine Leerzeichen enthalten. admin: cannot_update_their_password: other: Du kannst dein Passwort nicht ändern. cannot_edit_their_profile: other: Du kannst dein Profil nicht bearbeiten. cannot_modify_self_status: other: Du kannst deinen Status nicht ändern. email_or_password_wrong: other: E-Mail und Password stimmen nicht überein. answer: not_found: other: Antwort nicht gefunden. cannot_deleted: other: Keine Berechtigung zum Löschen. cannot_update: other: Keine Berechtigung zum Aktualisieren. question_closed_cannot_add: other: Fragen sind geschlossen und können nicht hinzugefügt werden. content_cannot_empty: other: Die Antwort darf nicht leer sein. comment: edit_without_permission: other: Kommentar kann nicht bearbeitet werden. not_found: other: Kommentar wurde nicht gefunden. cannot_edit_after_deadline: other: Die Kommentarzeit war zu lang, um sie zu ändern. content_cannot_empty: other: Der Kommentar darf nicht leer sein. email: duplicate: other: E-Mail existiert bereits. need_to_be_verified: other: E-Mail muss überprüft werden. verify_url_expired: other: Die verifizierbare E-Mail-URL ist abgelaufen, bitte sende die E-Mail erneut. illegal_email_domain_error: other: E-Mails sind von dieser E-Mail-Domäne nicht erlaubt. Bitte verwende eine andere. lang: not_found: other: Sprachdatei nicht gefunden. object: captcha_verification_failed: other: Captcha ist falsch. disallow_follow: other: Es ist dir nicht erlaubt zu folgen. disallow_vote: other: Es ist dir nicht erlaubt abzustimmen. disallow_vote_your_self: other: Du kannst nicht für deinen eigenen Beitrag stimmen. not_found: other: Objekt nicht gefunden. verification_failed: other: Verifizierung fehlgeschlagen. email_or_password_incorrect: other: E-Mail und Passwort stimmen nicht überein. old_password_verification_failed: other: Die Überprüfung des alten Passworts ist fehlgeschlagen new_password_same_as_previous_setting: other: Das neue Passwort ist das gleiche wie das vorherige Passwort. already_deleted: other: Dieser Beitrag wurde gelöscht. meta: object_not_found: other: Metaobjekt nicht gefunden question: already_deleted: other: Dieser Beitrag wurde gelöscht. under_review: other: Ihr Beitrag wartet auf Überprüfung. Er wird sichtbar sein, nachdem er genehmigt wurde. not_found: other: Frage nicht gefunden. cannot_deleted: other: Keine Berechtigung zum Löschen. cannot_close: other: Keine Berechtigung zum Schließen. cannot_update: other: Keine Berechtigung zum Aktualisieren. content_cannot_empty: other: Der Inhalt darf nicht leer sein. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Ansehenssrang erfüllt die Bedingung nicht. vote_fail_to_meet_the_condition: other: Danke für dein Feedback. Du brauchst mindestens {{.Rank}} Ansehen, um eine Stimme abzugeben. no_enough_rank_to_operate: other: Dafür brauchst du mindestens {{.Rank}} Ansehen. report: handle_failed: other: Bearbeiten der Meldung fehlgeschlagen. not_found: other: Meldung nicht gefunden. tag: already_exist: other: Tag existiert bereits. not_found: other: Tag nicht gefunden. recommend_tag_not_found: other: Das Tag "Empfehlen" ist nicht vorhanden. recommend_tag_enter: other: Bitte gib mindestens einen erforderlichen Tag ein. not_contain_synonym_tags: other: Sollte keine Synonym-Tags enthalten. cannot_update: other: Keine Berechtigung zum Aktualisieren. is_used_cannot_delete: other: Du kannst keinen Tag löschen, der in Gebrauch ist. cannot_set_synonym_as_itself: other: Du kannst das Synonym des aktuellen Tags nicht als sich selbst festlegen. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: Der Absendername kann keine E-Mail-Adresse sein. theme: not_found: other: Design nicht gefunden. revision: review_underway: other: Kann derzeit nicht bearbeitet werden, es existiert eine Version in der Überprüfungswarteschlange. no_permission: other: Keine Berechtigung zum Überarbeiten. user: external_login_missing_user_id: other: Die Plattform des Drittanbieters stellt keine eindeutige UserID zur Verfügung, sodass du dich nicht anmelden kannst. Bitte wende dich an den Administrator der Website. external_login_unbinding_forbidden: other: Bitte setze ein Login-Passwort für dein Konto, bevor du dieses Login entfernst. email_or_password_wrong: other: other: E-Mail und Passwort stimmen nicht überein. not_found: other: Benutzer nicht gefunden. suspended: other: Benutzer wurde gesperrt. username_invalid: other: Benutzername ist ungültig. username_duplicate: other: Benutzername wird bereits verwendet. set_avatar: other: Avatar setzen fehlgeschlagen. cannot_update_your_role: other: Du kannst deine Rolle nicht ändern. not_allowed_registration: other: Derzeit ist die Seite nicht für die Anmeldung geöffnet. not_allowed_login_via_password: other: Zurzeit ist es auf der Seite nicht möglich, sich mit einem Passwort anzumelden. access_denied: other: Zugriff verweigert page_access_denied: other: Du hast keinen Zugriff auf diese Seite. add_bulk_users_format_error: other: "Fehler {{.Field}}-Format in der Nähe von '{{.Content}}' in Zeile {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Die Anzahl der Benutzer, die du auf einmal hinzufügst, sollte im Bereich von 1-{{.MaxAmount}} liegen." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Lesekonfiguration fehlgeschlagen database: connection_failed: other: Datenbankverbindung fehlgeschlagen create_table_failed: other: Tabelle erstellen fehlgeschlagen install: create_config_failed: other: Kann die config.yaml-Datei nicht erstellen. upload: unsupported_file_format: other: Dateiformat nicht unterstützt. site_info: config_not_found: other: Seiten-Konfiguration nicht gefunden. badge: object_not_found: other: Abzeichen-Objekt nicht gefunden reason: spam: name: other: Spam desc: other: Dieser Beitrag ist eine Werbung oder Vandalismus. Er ist nicht nützlich oder relevant für das aktuelle Thema. rude_or_abusive: name: other: unhöflich oder beleidigend desc: other: "Eine vernünftige Person würde diesen Inhalt im respektvoll diskutierten Diskurs für unangemessen halten." a_duplicate: name: other: ein Duplikat desc: other: Diese Frage wurde schon einmal gestellt und hat bereits eine Antwort. placeholder: other: Gib den Link zur bestehenden Frage ein not_a_answer: name: other: keine Antwort desc: other: "Die Antwort versucht nicht, die Frage zu beantworten. Sie sollte entweder bearbeitet, kommentiert, als weitere Frage gestellt oder ganz gelöscht werden." no_longer_needed: name: other: nicht mehr benötigt desc: other: Dieser Kommentar ist veraltet oder nicht relevant für diesen Beitrag. something: name: other: anderer Grund desc: other: Dieser Beitrag erfordert die Aufmerksamkeit der Temmitglieder aus einem anderen, oben nicht genannten Grund. placeholder: other: Lass uns wissen, worüber du dir Sorgen machst community_specific: name: other: ein Community-spezifischer Grund desc: other: Diese Frage entspricht nicht den Gemeinschaftsrichtlinien. not_clarity: name: other: benötigt Details oder Klarheit desc: other: Diese Frage enthält derzeit mehrere Fragen in einer. Sie sollte sich auf ein einziges Problem konzentrieren. looks_ok: name: other: sieht OK aus desc: other: Dieser Beitrag ist gut so wie er ist und nicht von schlechter Qualität. needs_edit: name: other: muss bearbeitet werden, und ich habe es getan desc: other: Verbessere und korrigiere Probleme mit diesem Beitrag selbst. needs_close: name: other: muss geschlossen werden desc: other: Eine geschlossene Frage kann nicht beantwortet werden, aber du kannst sie trotzdem bearbeiten, abstimmen und kommentieren. needs_delete: name: other: muss gelöscht werden desc: other: Dieser Beitrag wird gelöscht. question: close: duplicate: name: other: Spam desc: other: Diese Frage ist bereits gestellt worden und hat bereits eine Antwort. guideline: name: other: ein Community-spezifischer Grund desc: other: Diese Frage entspricht nicht einer Gemeinschaftsrichtlinie. multiple: name: other: benötigt Details oder Klarheit desc: other: Diese Frage enthält derzeit mehrere Fragen in einer. Sie sollte sich auf ein einziges Problem konzentrieren. other: name: other: etwas anderes desc: other: Dieser Beitrag erfordert einen anderen Grund, der oben nicht aufgeführt ist. operation_type: asked: other: gefragt answered: other: beantwortet modified: other: geändert deleted_title: other: Gelöschte Frage questions_title: other: Fragen tag: tags_title: other: Schlagwörter no_description: other: Diese Kategorie hat keine Beschreibung. notification: action: update_question: other: aktualisierte Frage answer_the_question: other: beantwortete Frage update_answer: other: aktualisierte Antwort accept_answer: other: akzeptierte Antwort comment_question: other: kommentierte Frage comment_answer: other: kommentierte Antwort reply_to_you: other: hat Ihnen geantwortet mention_you: other: hat dich erwähnt your_question_is_closed: other: Deine Frage wurde geschlossen your_question_was_deleted: other: Deine Frage wurde gelöscht your_answer_was_deleted: other: Deine Antwort wurde gelöscht your_comment_was_deleted: other: Dein Kommentar wurde gelöscht up_voted_question: other: positiv bewertete Frage down_voted_question: other: negativ bewertete Frage up_voted_answer: other: positiv bewertete Antwort down_voted_answer: other: negativ bewertete Antwort up_voted_comment: other: positiv bewerteter Kommentar invited_you_to_answer: other: hat dich eingeladen, zu antworten earned_badge: other: Du hast das "{{.BadgeName}}" Abzeichen verdient email_tpl: change_email: title: other: "[{{.SiteName}}] Bestätige deine neue E-Mail-Adresse" body: other: "Bestätigen Sie Ihre neue E-Mail-Adresse für {{.SiteName}} indem Sie auf den folgenden Link klicken:
\n{{.ChangeEmailUrl}}

\n\nWenn Sie diese Änderung nicht angefordert haben bitte diese E-Mail ignorieren.

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} hat deine Frage beantwortet" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n\nAuf {{.SiteName}} anschauen

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.

\n\nAbmelden" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} hat dich eingeladen zu antworten" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Ich denke, Sie kennen die Antwort.

\n {{.SiteName}}

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.

\n\nAbmelden" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} hat deinen Beitrag kommentiert" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n\nAuf {{.SiteName}} anschauen

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.

\n\nAbmelden" new_question: title: other: "[{{.SiteName}}] Neue Frage: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nHinweis: Dies ist eine automatische Systemnachricht, bitte antworten Sie nicht darauf. Antworten werden nicht gelesen oder bearbeitet.

\n\nBenachrichtigung abbestellen" pass_reset: title: other: "[{{.SiteName }}] Passwort zurücksetzen" body: other: "Jemand bat darum, Ihr Passwort auf {{.SiteName}}zurückzusetzen.

\n\nWenn Sie es nicht waren, können Sie diese E-Mail sicher ignorieren.

\n\nKlicken Sie auf den folgenden Link, um ein neues Passwort auszuwählen:
\n{{.PassResetUrl}}\n\n

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird." register: title: other: "[{{.SiteName}}] Bestätige dein neues Konto" body: other: "Willkommen in {{.SiteName}}!

\n\nKlicken Sie auf den folgenden Link, um Ihr neues Konto zu bestätigen und zu aktivieren:
\n{{.RegisterUrl}}

\n\nWenn der obige Link nicht anklickbar ist kopieren und in die Adressleiste Ihres Webbrowsers einfügen.\n

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht sichtbar ist." test: title: other: "[{{.SiteName}}] Test-E-Mail" body: other: "Dies ist eine Test-E-Mail.\n

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird." action_activity_type: upvote: other: positiv bewerten upvoted: other: positiv bewertet downvote: other: negativ bewerten downvoted: other: negativ bewertet accept: other: akzeptieren accepted: other: akzeptiert edit: other: bearbeiten review: queued_post: other: Post in der Warteschlange flagged_post: other: Beiträge gemeldet suggested_post_edit: other: Änderungsvorschläge reaction: tooltip: other: "{{ .Names }} Und {{ .Count }} mehr..." badge: default_badges: autobiographer: name: other: Autobiograph desc: other: Gefüllt mit Profil Informationen. certified: name: other: Zertifiziert desc: other: Erledigte unser neues Benutzerhandbuch. editor: name: other: Editor desc: other: Erster Beitrag bearbeiten. first_flag: name: other: Erste Meldung desc: other: Erste Meldung eines Beitrags. first_upvote: name: other: Erster Upvote desc: other: Erste Like eines Beitrags. first_link: name: other: Erster Link desc: other: Hat erstmals einen Link zu einem anderen Beitrag hinzugefügt. first_reaction: name: other: Erste Reaktion desc: other: Zuerst reagierte auf den Beitrag. first_share: name: other: Erstes Teilen desc: other: Zuerst einen Beitrag geteilt. scholar: name: other: Gelehrter desc: other: Hat eine Frage gestellt und eine Antwort akzeptiert. commentator: name: other: Kommentator desc: other: Hinterlassen Sie 5 Kommentare. new_user_of_the_month: name: other: Neuer Benutzer des Monats desc: other: Ausstehende Beiträge in ihrem ersten Monat. read_guidelines: name: other: Lesen Sie die Richtlinien desc: other: Lesen Sie die [Community-Richtlinien]. reader: name: other: Leser desc: other: Lesen Sie alle Antworten in einem Thema mit mehr als 10 Antworten. welcome: name: other: Willkommen desc: other: Du hast eine positive Abstimmung erhalten. nice_share: name: other: Schöne teilen desc: other: Hat einen Beitrag mit 25 einzigartigen Besuchern freigegeben. good_share: name: other: Gut geteilt desc: other: Hat einen Beitrag mit 300 einzigartigen Besuchern freigegeben. great_share: name: other: Großartiges Teilen desc: other: Hat einen Beitrag mit 1000 einzigartigen Besuchern freigegeben. out_of_love: name: other: Aus Liebe desc: other: Hat an einem Tag 50 Upvotes verwendet. higher_love: name: other: Höhere Liebe desc: other: Hat an einem Tag 50 Upvotes 5 Mal verwendet. crazy_in_love: name: other: Im siebten Himmel desc: other: Hat an einem Tag 50 Upvotes 20 Mal verwendet. promoter: name: other: Förderer desc: other: Hat einen Benutzer eingeladen. campaigner: name: other: Kampagnenleiter desc: other: Lade 3 einfache Benutzer ein. champion: name: other: Champion desc: other: Hat 5 Mitglieder eingeladen. thank_you: name: other: Vielen Dank desc: other: Beitrag mit 20 Upvotes und 10 abgegebenen Upvotes. gives_back: name: other: Feedback geben desc: other: Beitrag mit 100 Upvotes und 100 abgegebenen Upvotes. empathetic: name: other: Einfühlsam desc: other: Beitrag mit 500 Upvotes und 1000 abgegebenen Upvotes. enthusiast: name: other: Enthusiast desc: other: Besucht 10 aufeinander folgende Tage. aficionado: name: other: Aficionado desc: other: Besucht 100 aufeinander folgende Tage. devotee: name: other: Anhänger desc: other: 365 aufeinander folgende Tage besucht. anniversary: name: other: Jahrestag desc: other: Aktives Mitglied für ein Jahr, mindestens einmal veröffentlicht. appreciated: name: other: Gewertschätzt desc: other: Erhalten 1 up vote für 20 posts. respected: name: other: Respektiert desc: other: Erhalten 2 up vote für 100 posts. admired: name: other: Bewundert desc: other: 5 upvotes für 300 posts erhalten. solved: name: other: Gelöst desc: other: Eine Antwort wurde akzeptiert. guidance_counsellor: name: other: Anleitungsberater desc: other: 10 Antworten wurden akzeptiert. know_it_all: name: other: Alleswisser desc: other: 50 Antworten wurden akzeptiert. solution_institution: name: other: Lösungsfinder desc: other: 150 Antworten wurden akzeptiert. nice_answer: name: other: Nette Antwort desc: other: Die Antwortpunktzahl beträgt mehr als 10 Punkte. good_answer: name: other: Gute Antwort desc: other: Die Antwortpunktzahl beträgt mehr als 25 Punkte. great_answer: name: other: Großartige Antwort desc: other: Die Antwortpunktzahl beträgt mehr als 50 Punkte. nice_question: name: other: Schöne Frage desc: other: Fragenpunktzahl von 10 oder mehr. good_question: name: other: Gute Frage desc: other: Fragen mit 25 oder mehr Punkten. great_question: name: other: Große Frage desc: other: Frage mit 50 oder mehr Punkten. popular_question: name: other: Populäre Frage desc: other: Frage mit 500 Ansichten. notable_question: name: other: Bemerkenswerte Frage desc: other: Frage mit 1.000 Ansichten. famous_question: name: other: Erstklassige Frage desc: other: Frage mit 5.000 Ansichten. popular_link: name: other: Populärer Link desc: other: Hat einen externen Link mit 50 Klicks gepostet. hot_link: name: other: Heißer Link desc: other: Geschrieben einen externen Link mit 300 Klicks. famous_link: name: other: Berühmter Link desc: other: Geschrieben einen externen Link mit 100 Klicks. default_badge_groups: getting_started: name: other: Erste Schritte community: name: other: Gemeinschaft posting: name: other: Freigeben # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Wie man formatiert desc: >-
  • einen Beitrag erwähnen: #post_id

  • um Links

    <https://url.com>

    [Titel](https://url.com)
  • Zwischen den Absätzen Zeilenumbrüche einfügen

  • _italic_ oder **fett**

  • Code um 4 Leerzeichen einrücken

  • Zitat durch Setzen von > am Anfang der Zeile

  • Backtick-Escapes `wie _this_`

  • Codeumrandungen mit Backticks `

    `
    Code hier
    ``
pagination: prev: Zurück next: Weiter page_title: question: Frage questions: Fragen tag: Schlagwort tags: Schlagwörter tag_wiki: tag Wiki create_tag: Tag erstellen edit_tag: Tag bearbeiten ask_a_question: Create Question edit_question: Frage bearbeiten edit_answer: Antwort bearbeiten search: Suchen posts_containing: Beiträge enthalten settings: Einstellungen notifications: Benachrichtigungen login: Anmelden sign_up: Registrieren account_recovery: Konto-Wiederherstellung account_activation: Account Aktivierung confirm_email: Bestätigungs-E-Mail account_suspended: Konto gesperrt admin: Verwaltung change_email: E-Mails ändern install: Installation beantworten upgrade: Antwort-Upgrade maintenance: Website-Wartung users: Benutzer oauth_callback: In Bearbeitung http_404: HTTP-Fehler 404 http_50X: HTTP-Fehler 500 http_403: HTTP Fehler 403 logout: Ausloggen posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Benachrichtigungen inbox: Posteingang achievement: Erfolge new_alerts: Neue Benachrichtigungen all_read: Alle als gelesen markieren show_more: Mehr anzeigen someone: Jemand inbox_type: all: Alle posts: Beiträge invites: Einladungen votes: Abstimmungen answer: Antwort question: Frage badge_award: Abzeichen suspended: title: Dein Konto wurde gesperrt until_time: "Dein Konto wurde bis zum {{ time }} gesperrt." forever: Dieser Benutzer wurde für immer gesperrt. end: Du erfüllst keine Community-Richtlinie. contact_us: Kontaktiere uns editor: blockquote: text: Blockzitat bold: text: Stark chart: text: Bestenliste flow_chart: Flussdiagramm sequence_diagram: Sequenzdiagramm class_diagram: Klassen Diagramm state_diagram: Zustandsdiagramm entity_relationship_diagram: Entitätsbeziehungsdiagramm user_defined_diagram: Benutzerdefiniertes Diagramm gantt_chart: Gantt-Diagramm pie_chart: Kuchendiagramm code: text: Code Beispiel add_code: Code-Beispiel hinzufügen form: fields: code: label: Code msg: empty: Code kann nicht leer sein. language: label: Sprache placeholder: Automatische Erkennung btn_cancel: Abbrechen btn_confirm: Hinzufügen formula: text: Formel options: inline: Inline Formel block: Block Formel heading: text: Überschrift options: h1: Überschrift 1 h2: Überschrift 2 h3: Überschrift 3 h4: Überschrift 4 h5: Überschrift 5 h6: Überschrift 6 help: text: Hilfe hr: text: Horizontale Richtlinie image: text: Bild add_image: Bild hinzufügen tab_image: Bild hochladen form_image: fields: file: label: Bilddatei btn: Bild auswählen msg: empty: Datei darf nicht leer sein. only_image: Nur Bilddateien sind erlaubt. max_size: Dateigröße darf {{size}} MB nicht überschreiten. desc: label: Beschreibung tab_url: Bild URL form_url: fields: url: label: Bild URL msg: empty: Bild-URL darf nicht leer sein. name: label: Beschreibung btn_cancel: Abbrechen btn_confirm: Hinzufügen uploading: Hochladen indent: text: Einzug outdent: text: Ausrücken italic: text: Hervorhebung link: text: Hyperlink add_link: Hyperlink hinzufügen form: fields: url: label: URL msg: empty: URL darf nicht leer sein. name: label: Beschreibung btn_cancel: Abbrechen btn_confirm: Hinzufügen ordered_list: text: Nummerierte Liste unordered_list: text: Aufzählungsliste table: text: Tabelle heading: Überschrift cell: Zelle file: text: Datei anhängen not_supported: "Diesen Dateityp nicht unterstützen. Versuchen Sie es erneut mit {{file_type}}." max_size: "Dateigröße anhängen darf {{size}} MB nicht überschreiten." close_modal: title: Ich schließe diesen Beitrag als... btn_cancel: Abbrechen btn_submit: Senden remark: empty: Kann nicht leer sein. msg: empty: Bitte wähle einen Grund aus. report_modal: flag_title: Ich melde diesen Beitrag als... close_title: Ich schließe diesen Beitrag wegen ... review_question_title: Frage prüfen review_answer_title: Antwort prüfen review_comment_title: Kommentar prüfen btn_cancel: Abbrechen btn_submit: Senden remark: empty: Kann nicht leer sein. msg: empty: Bitte wähle einen Grund aus. not_a_url: URL hat ein falsches Format. url_not_match: URL-Ursprung stimmt nicht mit der aktuellen Website überein. tag_modal: title: Neuen Tag erstellen form: fields: display_name: label: Anzeigename msg: empty: Anzeigename darf nicht leer sein. range: Anzeige des Namens mit bis zu 35 Zeichen. slug_name: label: URL-Slug desc: 'Muss den Zeichensatz "a-z", "0-9", "+ # - " verwenden.' msg: empty: URL-Slug darf nicht leer sein. range: URL-Slug mit bis zu 35 Zeichen. character: URL-Slug enthält nicht erlaubten Zeichensatz. desc: label: Beschreibung revision: label: Version edit_summary: label: Zusammenfassung bearbeiten placeholder: >- Erkläre kurz deine Änderungen (korrigierte Rechtschreibung, korrigierte Grammatik, verbesserte Formatierung) btn_cancel: Abbrechen btn_submit: Senden btn_post: Neuen Tag erstellen tag_info: created_at: Erstellt edited_at: Bearbeitet history: Verlauf synonyms: title: Synonyme text: Die folgenden Tags werden neu zugeordnet zu empty: Keine Synonyme gefunden. btn_add: Synonym hinzufügen btn_edit: Bearbeiten btn_save: Speichern synonyms_text: Die folgenden Tags werden neu zugeordnet zu delete: title: Diesen Tag löschen tip_with_posts: >-

Wir erlauben es nicht, Tags mit Beiträgenzu löschen.

Bitte entfernen Sie dieses Tag zuerst aus den Beiträgen.

tip_with_synonyms: >-

Wir erlauben nicht Tags mit Synonymenzu löschen.

Bitte entfernen Sie zuerst die Synonyme von diesem Schlagwort.

tip: Bist du sicher, dass du löschen möchtest? close: Schließen merge: title: Tags zusammenführen source_tag_title: Quell-Tag source_tag_description: Das Quell-Tag und seine zugehörigen Daten werden dem Ziel-Tag zugeordnet. target_tag_title: Ziel-Tag target_tag_description: Ein Synonym zwischen diesen beiden Tags wird nach dem Zusammenführen erstellt. no_results: Keine zusammenpassenden Tags gefunden btn_submit: Absenden btn_close: Schließen edit_tag: title: Tag bearbeiten default_reason: Tag bearbeiten default_first_reason: Tag hinzufügen btn_save_edits: Änderungen speichern btn_cancel: Abbrechen dates: long_date: DD. MMM long_date_with_year: "DD. MMM YYYY" long_date_with_time: "DD. MMM YYYY [at] HH:mm" now: Gerade eben x_seconds_ago: "Vor {{count}}s" x_minutes_ago: "Vor {{count}}m" x_hours_ago: "Vor {{count}}h" hour: Stunde day: tag hours: Stunden days: Tage month: month months: months year: year reaction: heart: Herz smile: Lächeln frown: Stirnrunzeln btn_label: Reaktionen hinzufügen oder entfernen undo_emoji: '{{ emoji }} Reaktion rückgängig machen' react_emoji: mit {{ emoji }} reagieren unreact_emoji: '{{ emoji }} Reaktion entfernen' comment: btn_add_comment: Einen Kommentar hinzufügen reply_to: Antwort an btn_reply: Antwort btn_edit: Bearbeiten btn_delete: Löschen btn_flag: Melden btn_save_edits: Änderungen speichern btn_cancel: Abbrechen show_more: "{{count}} mehr Kommentare" tip_question: >- Verwende Kommentare, um nach weiteren Informationen zu fragen oder Verbesserungen vorzuschlagen. Vermeide es, Fragen in Kommentaren zu beantworten. tip_answer: >- Verwende Stellungsnahmen, um anderen Nutzern zu antworten oder sie über Änderungen zu informieren. Wenn du neue Informationen hinzufügst, bearbeite deinen Beitrag, anstatt zu kommentieren. tip_vote: Es fügt dem Beitrag etwas Nützliches hinzu edit_answer: title: Antwort bearbeiten default_reason: Antwort bearbeiten default_first_reason: Antwort hinzufügen form: fields: revision: label: Version answer: label: Antwort feedback: characters: der Inhalt muss mindestens 6 Zeichen lang sein. edit_summary: label: Zusammenfassung bearbeiten placeholder: >- Erkläre kurz deine Änderungen (korrigierte Rechtschreibung, korrigierte Grammatik, verbesserte Formatierung) btn_save_edits: Änderungen speichern btn_cancel: Abbrechen tags: title: Schlagwörter sort_buttons: popular: Beliebt name: Name newest: Neueste button_follow: Folgen button_following: Folgend tag_label: fragen search_placeholder: Nach Tagnamen filtern no_desc: Der Tag hat keine Beschreibung. more: Mehr wiki: Wiki ask: title: Create Question edit_title: Frage bearbeiten default_reason: Frage bearbeiten default_first_reason: Create question similar_questions: Ähnliche Fragen form: fields: revision: label: Version title: label: Titel placeholder: What's your topic? Be specific. msg: empty: Der Titel darf nicht leer sein. range: Titel bis zu 150 Zeichen body: label: Körper msg: empty: Körper darf nicht leer sein. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Stichworte msg: empty: Tags dürfen nicht leer sein. answer: label: Antwort msg: empty: Antwort darf nicht leer sein. edit_summary: label: Zusammenfassung bearbeiten placeholder: >- Erkläre kurz deine Änderungen (korrigierte Rechtschreibung, korrigierte Grammatik, verbesserte Formatierung) btn_post_question: Poste deine Frage btn_save_edits: Änderungen speichern answer_question: Eigene Frage beantworten post_question&answer: Poste deine Frage und Antwort tag_selector: add_btn: Schlagwort hinzufügen create_btn: Neuen Tag erstellen search_tag: Tag suchen hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Keine Tags gefunden tag_required_text: Benötigter Tag (mindestens eins) header: nav: question: Fragen tag: Schlagwörter user: Benutzer badges: Abzeichen profile: Profil setting: Einstellungen logout: Ausloggen admin: Administrator review: Überprüfung bookmark: Lesezeichen moderation: Moderation search: placeholder: Suchen footer: build_on: Powered by <1> Apache Answer upload_img: name: Ändern loading: wird geladen... pic_auth_code: title: Captcha placeholder: Gib den Text oben ein msg: empty: Captcha darf nicht leer sein. inactive: first: >- Du bist fast fertig! Wir haben eine Aktivierungsmail an {{mail}} geschickt. Bitte folge den Anweisungen in der Mail, um dein Konto zu aktivieren. info: "Wenn sie nicht ankommt, überprüfe deinen Spam-Ordner." another: >- Wir haben dir eine weitere Aktivierungs-E-Mail an {{mail}} geschickt. Es kann ein paar Minuten dauern, bis sie ankommt; überprüfe daher deinen Spam-Ordner. btn_name: Aktivierungs Mail erneut senden change_btn_name: E-Mail ändern msg: empty: Kann nicht leer sein. resend_email: url_label: Bist du sicher, dass du die Aktivierungs-E-Mail erneut senden willst? url_text: Du kannst auch den Aktivierungslink oben an den Nutzer weitergeben. login: login_to_continue: Anmelden, um fortzufahren info_sign: Du verfügst noch nicht über ein Konto? Registrieren info_login: Du hast bereits ein Konto? <1>Anmelden agreements: Wenn du dich registrierst, stimmst du der <1>Datenschutzrichtlinie und den <3>Nutzungsbedingungen zu. forgot_pass: Passwort vergessen? name: label: Name msg: empty: Der Name darf nicht leer sein. range: Der Name muss zwischen 2 und 30 Zeichen lang sein. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: E-Mail msg: empty: E-Mail-Feld darf nicht leer sein. password: label: Passwort msg: empty: Passwort-Feld darf nicht leer sein. different: Die beiden eingegebenen Passwörter stimmen nicht überein account_forgot: page_title: Dein Passwort vergessen btn_name: Schicke mir eine E-Mail zur Wiederherstellung send_success: >- Wenn ein Konto mit {{mail}} übereinstimmt, solltest du in Kürze eine E-Mail mit Anweisungen erhalten, wie du dein Passwort zurücksetzen kannst. email: label: E-Mail msg: empty: E-Mail darf nicht leer sein. change_email: btn_cancel: Stornieren btn_update: E-Mail Adresse aktualisieren send_success: >- Wenn ein Konto mit {{mail}} übereinstimmt, solltest du in Kürze eine E-Mail mit Anweisungen erhalten, wie du dein Passwort zurücksetzen kannst. email: label: Neue E-Mail msg: empty: E-Mail darf nicht leer sein. oauth: connect: Mit {{ auth_name }} verbinden remove: '{{ auth_name }} entfernen' oauth_bind_email: subtitle: Wiederherstellungs-E-Mail zu deinem Konto hinzufügen. btn_update: E-Mail aktualisieren email: label: E-Mail msg: empty: E-Mail darf nicht leer sein. modal_title: E-Mail existiert bereits. modal_content: Diese E-Mail ist bereits registriert. Bist du sicher, dass du dich mit dem bestehenden Konto verbinden möchtest? modal_cancel: E-Mail ändern modal_confirm: Mit dem bestehenden Konto verbinden password_reset: page_title: Passwort zurücksetzen btn_name: Setze mein Passwort zurück reset_success: >- Du hast dein Passwort erfolgreich geändert; du wirst zur Anmeldeseite weitergeleitet. link_invalid: >- Dieser Link zum Zurücksetzen des Passworts ist leider nicht mehr gültig. Vielleicht ist dein Passwort bereits zurückgesetzt? to_login: Weiter zur Anmeldeseite password: label: Passwort msg: empty: Passwort kann nicht leer sein. length: Die Länge muss zwischen 8 und 32 liegen different: Die auf beiden Seiten eingegebenen Passwörter sind inkonsistent password_confirm: label: Neues Passwort bestätigen settings: page_title: Einstellungen goto_modify: Zum Ändern nav: profile: Profil notification: Benachrichtigungen account: Konto interface: Benutzeroberfläche profile: heading: Profil btn_name: Speichern display_name: label: Anzeigename msg: Anzeigename darf nicht leer sein. msg_range: Der Anzeigename muss zwischen 2 und 30 Zeichen lang sein. username: label: Nutzername caption: Leute können dich als "@Benutzername" erwähnen. msg: Benutzername darf nicht leer sein. msg_range: Der Benutzername muss zwischen 2 und 30 Zeichen lang sein. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profilbild gravatar: Gravatar gravatar_text: Du kannst das Bild ändern auf custom: Benutzerdefiniert custom_text: Du kannst dein Bild hochladen. default: System msg: Bitte lade einen Avatar hoch bio: label: Über mich website: label: Webseite placeholder: "https://example.com" msg: Website falsches Format location: label: Standort placeholder: "Stadt, Land" notification: heading: E-Mail-Benachrichtigungen turn_on: Aktivieren inbox: label: Posteingangsbenachrichtigungen description: Antworten auf deine Fragen, Kommentare, Einladungen und mehr. all_new_question: label: Alle neuen Fragen description: Lass dich über alle neuen Fragen benachrichtigen. Bis zu 50 Fragen pro Woche. all_new_question_for_following_tags: label: Alle neuen Fragen für folgende Tags description: Lass dich über neue Fragen zu folgenden Tags benachrichtigen. account: heading: Konto change_email_btn: E-Mail ändern change_pass_btn: Passwort ändern change_email_info: >- Wir haben eine E-Mail an diese Adresse geschickt. Bitte befolge die Anweisungen zur Bestätigung. email: label: E-Mail new_email: label: Neue E-Mail msg: Neue E-Mail darf nicht leer sein. pass: label: Aktuelles Passwort msg: Passwort kann nicht leer sein. password_title: Passwort current_pass: label: Aktuelles Passwort msg: empty: Das aktuelle Passwort darf nicht leer sein. length: Die Länge muss zwischen 8 und 32 liegen. different: Die beiden eingegebenen Passwörter stimmen nicht überein. new_pass: label: Neues Passwort pass_confirm: label: Neues Passwort bestätigen interface: heading: Benutzeroberfläche lang: label: Sprache der Benutzeroberfläche text: Sprache der Benutzeroberfläche. Sie ändert sich, wenn du die Seite aktualisierst. my_logins: title: Meine Anmeldungen label: Melde dich mit diesen Konten an oder registriere dich auf dieser Seite. modal_title: Login entfernen modal_content: Bist du sicher, dass du dieses Login aus deinem Konto entfernen möchtest? modal_confirm_btn: Entfernen remove_success: Erfolgreich entfernt toast: update: Aktualisierung erfolgreich update_password: Das Kennwort wurde erfolgreich geändert. flag_success: Danke fürs Markieren. forbidden_operate_self: Verboten, an sich selbst zu operieren review: Deine Überarbeitung wird nach der Überprüfung angezeigt. sent_success: Erfolgreich gesendet related_question: title: Related answers: antworten linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Frage jemanden desc: Lade Leute ein, von denen du glaubst, dass sie die Antwort wissen könnten. invite: Zur Antwort einladen add: Personen hinzufügen search: Personen suchen question_detail: action: Aktion created: Created Asked: Gefragt asked: gefragt update: Geändert Edited: Edited edit: bearbeitet commented: kommentiert Views: Gesehen Follow: Folgen Following: Folgend follow_tip: Folge dieser Frage, um Benachrichtigungen zu erhalten answered: beantwortet closed_in: Abgeschlossen in show_exist: Bestehende Frage anzeigen. useful: Nützlich question_useful: Es ist nützlich und klar question_un_useful: Es ist unklar oder nicht nützlich question_bookmark: Lesezeichen für diese Frage answer_useful: Es ist nützlich answer_un_useful: Es ist nicht nützlich answers: title: Antworten score: Punkte newest: Neueste oldest: Älteste btn_accept: Akzeptieren btn_accepted: Akzeptiert write_answer: title: Deine Antwort edit_answer: Meine existierende Antwort bearbeiten btn_name: Poste deine Antwort add_another_answer: Weitere Antwort hinzufügen confirm_title: Antworten fortsetzen continue: Weitermachen confirm_info: >-

Bist du sicher, dass du eine weitere Antwort hinzufügen willst?

Du könntest stattdessen den Bearbeiten-Link verwenden, um deine existierende Antwort zu verfeinern und zu verbessern.

empty: Antwort darf nicht leer sein. characters: der Inhalt muss mindestens 6 Zeichen lang sein. tips: header_1: Danke für deine Antwort li1_1: Bitte stelle sicher, dass du die Frage beantwortest. Gib Details an und erzähle von deiner Recherche. li1_2: Untermauere alle Aussagen, die du erstellst, mit Referenzen oder persönlichen Erfahrungen. header_2: Aber vermeide... li2_1: Bitte um Hilfe, um Klarstellung oder um Antwort auf andere Antworten. reopen: confirm_btn: Wieder öffnen title: Diesen Beitrag erneut öffnen content: Bist du sicher, dass du wieder öffnen willst? list: confirm_btn: Liste title: Diesen Beitrag auflisten content: Möchten Sie diesen Beitrag wirklich in der Liste anzeigen? unlist: confirm_btn: Von Liste nehmen title: Diesen Beitrag von der Liste nehmen content: Möchten Sie diesen Beitrag wirklich aus der Liste ausblenden? pin: title: Diesen Beitrag anpinnen content: Bist du sicher, dass du den Beitrag global anheften möchtest? Dieser Beitrag wird in allen Beitragslisten ganz oben erscheinen. confirm_btn: Anheften delete: title: Diesen Beitrag löschen question: >- Wir raten davon ab, Fragen mit Antworten zu löschen, weil dadurch zukünftigen Lesern dieses Wissen vorenthalten wird.

Wiederholtes Löschen von beantworteten Fragen kann dazu führen, dass dein Konto für Fragen gesperrt wird. Bist du sicher, dass du löschen möchtest? answer_accepted: >-

Wir empfehlen nicht, akzeptierte Antworten zu löschen, denn dadurch wird zukünftigen Lesern dieses Wissen vorenthalten.

Das wiederholte Löschen von akzeptierten Antworten kann dazu führen, dass dein Konto für die Beantwortung gesperrt wird. Bist du sicher, dass du löschen möchtest? other: Bist du sicher, dass du löschen möchtest? tip_answer_deleted: Diese Antwort wurde gelöscht undelete_title: Diesen Beitrag wiederherstellen undelete_desc: Bist du sicher, dass du die Löschung umkehren willst? btns: confirm: Bestätigen cancel: Abbrechen edit: Bearbeiten save: Speichern delete: Löschen undelete: Wiederherstellen list: Liste unlist: Verstecken unlisted: Versteckt login: Einloggen signup: Registrieren logout: Ausloggen verify: Überprüfen create: Erstellen approve: Genehmigen reject: Ablehnen skip: Überspringen discard_draft: Entwurf verwerfen pinned: Angeheftet all: Alle question: Frage answer: Antwort comment: Kommentar refresh: Aktualisieren resend: Erneut senden deactivate: Deaktivieren active: Aktiv suspend: Sperren unsuspend: Entsperren close: Schließen reopen: Wieder öffnen ok: Okay light: Hell dark: Dunkel system_setting: System-Einstellung default: Standard reset: Zurücksetzen tag: Tag post_lowercase: post filter: Filter ignore: Ignorieren submit: Absenden normal: Normal closed: Geschlossen deleted: Gelöscht deleted_permanently: Dauerhaft gelöscht pending: Ausstehend more: Mehr view: Betrachten card: Karte compact: Kompakt display_below: Unten anzeigen always_display: Immer anzeigen or: oder back_sites: Zurück zur Website search: title: Suchergebnisse keywords: Schlüsselwörter options: Optionen follow: Folgen following: Folgend counts: "{{count}} Ergebnisse" counts_loading: "... Results" more: Mehr sort_btns: relevance: Relevanz newest: Neueste active: Aktiv score: Punktzahl more: Mehr tips: title: Erweiterte Suchtipps tag: "<1>[tag] Suche mit einem Tag" user: "<1>user:username Suche nach Autor" answer: "<1>Antworten:0 unbeantwortete Fragen" score: "<1>score:3 Beiträge mit einer 3+ Punktzahl" question: "<1>is:question Suchfragen" is_answer: "<1>ist:answer Suchantworten" empty: Wir konnten nichts finden.
Versuche es mit anderen oder weniger spezifischen Keywords. share: name: Teilen copy: Link kopieren via: Beitrag teilen über... copied: Kopiert facebook: Auf Facebook teilen twitter: Auf X teilen cannot_vote_for_self: Du kannst nicht für deinen eigenen Beitrag stimmen. modal_confirm: title: Fehler... delete_permanently: title: Endgültig löschen content: Sind Sie sicher, dass Sie den Inhalt endgültig löschen möchten? account_result: success: Dein neues Konto ist bestätigt; du wirst zur Startseite weitergeleitet. link: Weiter zur Startseite oops: Hoppla! invalid: Der Link, den Sie verwendet haben, funktioniert nicht mehr. confirm_new_email: Deine E-Mail wurde aktualisiert. confirm_new_email_invalid: >- Dieser Bestätigungslink ist leider nicht mehr gültig. Vielleicht wurde deine E-Mail-Adresse bereits geändert? unsubscribe: page_title: Abonnement entfernen success_title: Erfolgreich vom Abo abgemeldet success_desc: Du wurdest erfolgreich aus der Abonnentenliste gestrichen und wirst keine weiteren E-Mails von uns erhalten. link: Einstellungen ändern question: following_tags: Folgende Tags edit: Bearbeiten save: Speichern follow_tag_tip: Folge den Tags, um deine Liste mit Fragen zu erstellen. hot_questions: Angesagte Fragen all_questions: Alle Fragen x_questions: "{{ count }} Fragen" x_answers: "{{ count }} Antworten" x_posts: "{{ count }} Posts" questions: Fragen answers: Antworten newest: Neueste active: Aktiv hot: Heiß frequent: Häufig recommend: Empfehlen score: Punktzahl unanswered: Unbeantwortet modified: geändert answered: beantwortet asked: gefragt closed: schließen follow_a_tag: Einem Tag folgen more: Mehr personal: overview: Übersicht answers: Antworten answer: antwort questions: Fragen question: frage bookmarks: Lesezeichen reputation: Ansehen comments: Kommentare votes: Stimmen badges: Abzeichen newest: Neueste score: Punktzahl edit_profile: Profil bearbeiten visited_x_days: "{{ count }} Tage besucht" viewed: Gesehen joined: Beigetreten comma: "," last_login: Gesehen about_me: Über mich about_me_empty: "// Hallo Welt !" top_answers: Top-Antworten top_questions: Top-Fragen stats: Statistiken list_empty: Keine Beiträge gefunden.
Vielleicht möchtest du einen anderen Reiter auswählen? content_empty: Keine Posts gefunden. accepted: Akzeptiert answered: antwortete asked: gefragt downvoted: negativ bewertet mod_short: MOD mod_long: Moderatoren x_reputation: ansehen x_votes: Stimmen erhalten x_answers: Antworten x_questions: Fragen recent_badges: Neueste Abzeichen install: title: Installation next: Nächste done: Erledigt config_yaml_error: Die Datei config.yaml kann nicht erstellt werden. lang: label: Bitte wähle eine Sprache db_type: label: Datenbank-Engine db_username: label: Nutzername placeholder: wurzel msg: Benutzername darf nicht leer sein. db_password: label: Passwort placeholder: wurzel msg: Passwort kann nicht leer sein. db_host: label: Datenbank-Host placeholder: "db:3306" msg: Datenbank-Host darf nicht leer sein. db_name: label: Datenbankname placeholder: antworten msg: Der Datenbankname darf nicht leer sein. db_file: label: Datenbank-Datei placeholder: /data/answer.Weder noch msg: Datenbankdatei kann nicht leer sein. ssl_enabled: label: SSL aktivieren ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL-Modus ssl_root_cert: placeholder: SSL-Root-Zertifikat Pfad msg: Pfad zum Ssl-Root-Zertifikat darf nicht leer sein ssl_cert: placeholder: SSL-Zertifikat Pfad msg: Pfad zum SSL-Zertifikat darf nicht leer sein ssl_key: placeholder: SSL-Key Pfad msg: Der Pfad zum SSL-Key darf nicht leer sein config_yaml: title: config.yaml erstellen label: Die erstellte config.yaml-Datei. desc: >- Du kannst die Datei <1>config.yaml manuell im Verzeichnis <1>/var/wwww/xxx/ erstellen und den folgenden Text dort einfügen. info: Nachdem du das getan hast, klickst du auf die Schaltfläche "Weiter". site_information: Standortinformationen admin_account: Administratorkonto site_name: label: Seitenname msg: Standortname darf nicht leer sein. msg_max_length: Der Name der Website darf maximal 30 Zeichen lang sein. site_url: label: Seiten-URL text: Die Adresse deiner Website. msg: empty: Die Website-URL darf nicht leer sein. incorrect: Falsches Format der Website-URL. max_length: Die URL der Website darf maximal 512 Zeichen lang sein. contact_email: label: Kontakt E-Mail text: E-Mail-Adresse des Hauptkontakts, der für diese Website verantwortlich ist. msg: empty: Kontakt-E-Mail kann nicht leer sein. incorrect: Falsches Format der Kontakt-E-Mail. login_required: label: Privat switch: Anmeldung erforderlich text: Nur eingeloggte Benutzer können auf diese Community zugreifen. admin_name: label: Name msg: Der Name darf nicht leer sein. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Der Name muss zwischen 2 und 30 Zeichen lang sein. admin_password: label: Passwort text: >- Du brauchst dieses Passwort, um dich einzuloggen. Bitte bewahre es an einem sicheren Ort auf. msg: Passwort kann nicht leer sein. msg_min_length: Passwort muss mindestens 8 Zeichen lang sein. msg_max_length: Das Passwort darf maximal 32 Zeichen lang sein. admin_confirm_password: label: "Passwort bestätigen" text: "Bitte geben Sie Ihr Passwort erneut ein, um es zu bestätigen." msg: "Passwortbestätigung stimmt nicht überein!" admin_email: label: E-Mail text: Du brauchst diese E-Mail, um dich einzuloggen. msg: empty: E-Mail darf nicht leer sein. incorrect: E-Mail falsches Format. ready_title: Ihre Seite ist bereit ready_desc: >- Wenn du noch mehr Einstellungen ändern möchtest, besuche den <1>Admin-Bereich; du findest ihn im Seitenmenü. good_luck: "Viel Spaß und viel Glück!" warn_title: Warnung warn_desc: >- Die Datei <1>config.yaml existiert bereits. Wenn du einen der Konfigurationspunkte in dieser Datei zurücksetzen musst, lösche sie bitte zuerst. install_now: Du kannst versuchen, <1>jetzt zu installieren. installed: Bereits installiert installed_desc: >- Du scheinst es bereits installiert zu haben. Um neu zu installieren, lösche bitte zuerst deine alten Datenbanktabellen. db_failed: Datenbankverbindung fehlgeschlagen db_failed_desc: >- Das bedeutet entweder, dass die Datenbankinformationen in deiner <1>config.yaml Datei falsch sind oder dass der Kontakt zum Datenbankserver nicht hergestellt werden konnte. Das könnte bedeuten, dass der Datenbankserver deines Hosts ausgefallen ist. counts: views: Ansichten votes: Stimmen answers: Antworten accepted: Akzeptiert page_error: http_error: HTTP Fehler {{ code }} desc_403: Du hast keine Berechtigung, auf diese Seite zuzugreifen. desc_404: Leider existiert diese Seite nicht. desc_50X: Der Server ist auf einen Fehler gestoßen und konnte deine Anfrage nicht vollständig abschließen. back_home: Zurück zur Startseite page_maintenance: desc: "Wir werden gewartet, wir sind bald wieder da." nav_menus: dashboard: Dashboard contents: Inhalt questions: Fragen answers: Antworten users: Benutzer badges: Abzeichen flags: Meldungen settings: Einstellungen general: Allgemein interface: Benutzeroberfläche smtp: SMTP branding: Branding legal: Rechtliches write: Schreiben terms: Terms tos: Nutzungsbedingungen privacy: Privatsphäre seo: SEO customize: Anpassen themes: Themen login: Anmeldung privileges: Berechtigungen plugins: Erweiterungen (Plugins) installed_plugins: Installierte Plugins apperance: Erscheinungsbild community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Willkommen auf {{site_name}} user_center: login: Anmelden qrcode_login_tip: Bitte verwende {{ agentName }}, um den QR-Code zu scannen und dich einzuloggen. login_failed_email_tip: Anmeldung ist fehlgeschlagen. Bitte erlaube dieser App, auf deine E-Mail-Informationen zuzugreifen, bevor du es erneut versuchst. badges: modal: title: Glückwunsch content: Sie haben sich ein neues Abzeichen verdient. close: Schließen confirm: Abzeichen ansehen title: Abzeichen awarded: Verliehen earned_×: Verdiente ×{{ number }} ×_awarded: "verliehen {{ number }} " can_earn_multiple: Du kannst das mehrmals verdienen. earned: Verdient admin: admin_header: title: Administrator dashboard: title: Dashboard welcome: Willkommen im Admin Bereich! site_statistics: Website-Statistiken questions: "Fragen:" resolved: "Belöst:" unanswered: "Nicht beantwortet:" answers: "Antworten:" comments: "Kommentare:" votes: "Stimmen:" users: "Nutzer:" flags: "Meldungen:" reviews: "Rezension:" site_health: Gesundheit der Website version: "Version:" https: "HTTPS:" upload_folder: "Hochladeverzeichnis:" run_mode: "Betriebsmodus:" private: Privat public: Öffentlich smtp: "SMTP:" timezone: "Zeitzone:" system_info: Systeminformationen go_version: "Go Version:" database: "Datenbank:" database_size: "Datenbankgröße:" storage_used: "Verwendeter Speicher:" uptime: "Betriebszeit:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Kontakt forum: Forum documents: Dokumentation feedback: Rückmeldung support: Unterstützung review: Überprüfung config: Konfig update_to: Aktualisieren zu latest: Aktuell check_failed: Prüfung fehlgeschlagen "yes": "Ja" "no": "Nein" not_allowed: Nicht erlaubt allowed: Erlaubt enabled: Aktiviert disabled: Deaktiviert writable: Schreibbar not_writable: Nicht schreibbar flags: title: Meldungen pending: Ausstehend completed: Abgeschlossen flagged: Gekennzeichnet flagged_type: '{{ type }} gemeldet' created: Erstellt action: Aktion review: Überprüfung user_role_modal: title: Benutzerrolle ändern zu... btn_cancel: Abbrechen btn_submit: Senden new_password_modal: title: Neues Passwort festlegen form: fields: password: label: Passwort text: Der Nutzer wird abgemeldet und muss sich erneut anmelden. msg: Das Passwort muss mindestens 8-32 Zeichen lang sein. btn_cancel: Abbrechen btn_submit: Senden edit_profile_modal: title: Profil bearbeiten form: fields: display_name: label: Anzeigename msg_range: Der Anzeigename muss zwischen 2 und 30 Zeichen lang sein. username: label: Nutzername msg_range: Der Benutzername muss 2-30 Zeichen lang sein. email: label: E-Mail msg_invalid: Ungültige E-Mail-Adresse. edit_success: Erfolgreich bearbeitet btn_cancel: Abbrechen btn_submit: Absenden user_modal: title: Neuen Benutzer hinzufügen form: fields: users: label: Masse Benutzer hinzufügen placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Trenne "Name, E-Mail, Passwort" mit Kommas. Ein Benutzer pro Zeile. msg: "Bitte gib die E-Mail des Nutzers ein, eine pro Zeile." display_name: label: Anzeigename msg: Der Anzeigename muss zwischen 2 und 30 Zeichen lang sein. email: label: E-Mail msg: Die E-Mail ist nicht gültig. password: label: Passwort msg: Das Passwort muss mindestens 8-32 Zeichen lang sein. btn_cancel: Abbrechen btn_submit: Senden users: title: Benutzer name: Name email: E-Mail reputation: Ansehen created_at: Angelegt am delete_at: Löschzeit suspend_at: Sperrzeit suspend_until: Suspend until status: Status role: Rolle action: Aktion change: Ändern all: Alle staff: Teammitglieder more: Mehr inactive: Inaktiv suspended: Gesperrt deleted: Gelöscht normal: Normal Moderator: Moderation Admin: Administrator User: Benutzer filter: placeholder: "Nach Namen, user:id filtern" set_new_password: Neues Passwort festlegen edit_profile: Profil bearbeiten change_status: Status ändern change_role: Rolle wechseln show_logs: Protokolle anzeigen add_user: Benutzer hinzufügen deactivate_user: title: Benutzer deaktivieren content: Ein inaktiver Nutzer muss seine E-Mail erneut bestätigen. delete_user: title: Diesen Benutzer löschen content: Bist du sicher, dass du diesen Benutzer löschen willst? Das ist dauerhaft! remove: Ihren Inhalt entfernen label: Alle Fragen, Antworten, Kommentare, etc. entfernen text: Aktiviere diese Option nicht, wenn du nur das Benutzerkonto löschen möchtest. suspend_user: title: Diesen Benutzer sperren content: Ein gesperrter Benutzer kann sich nicht einloggen. label: How long will the user be suspended for? forever: Forever questions: page_title: Fragen unlisted: Nicht gelistet post: Beitrag votes: Stimmen answers: Antworten created: Erstellt status: Status action: Aktion change: Ändern pending: Ausstehend filter: placeholder: "Filtern nach Titel, Frage:Id" answers: page_title: Antworten post: Beitrag votes: Stimmen created: Erstellt status: Status action: Aktion change: Ändern filter: placeholder: "Filtern nach Titel, Antwort: id" general: page_title: Allgemein name: label: Seitenname msg: Der Site-Name darf nicht leer sein. text: "Der Name dieser Website, wie er im Titel-Tag verwendet wird." site_url: label: Seiten-URL msg: Die Website-Url darf nicht leer sein. validate: Bitte gib eine gültige URL ein. text: Die Adresse deiner Website. short_desc: label: Kurze Seitenbeschreibung msg: Die kurze Website-Beschreibung darf nicht leer sein. text: "Kurze Beschreibung, wie im Titel-Tag auf der Homepage verwendet." desc: label: Seitenbeschreibung msg: Die Websitebeschreibung darf nicht leer sein. text: "Beschreibe diese Seite in einem Satz, wie er im Meta Description Tag verwendet wird." contact_email: label: Kontakt E-Mail msg: Kontakt-E-Mail darf nicht leer sein. validate: Kontakt-E-Mail ist ungültig. text: E-Mail-Adresse des Hauptkontakts, der für diese Website verantwortlich ist. check_update: label: Softwareaktualisierungen text: Automatisch auf Updates prüfen interface: page_title: Benutzeroberfläche language: label: Interface Sprache msg: Sprache der Benutzeroberfläche darf nicht leer sein. text: Sprache der Benutzeroberfläche. Sie ändert sich, wenn du die Seite aktualisierst. time_zone: label: Zeitzone msg: Die Zeitzone darf nicht leer sein. text: Wähle eine Stadt in der gleichen Zeitzone wie du. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: Von E-Mail msg: Von E-Mail darf nicht leer sein. text: Die E-Mail-Adresse, von der E-Mails gesendet werden. from_name: label: Von Name msg: Absendername darf nicht leer sein. text: Der Name, von dem E-Mails gesendet werden. smtp_host: label: SMTP-Host msg: Der SMTP-Host darf nicht leer sein. text: Dein Mailserver. encryption: label: Verschlüsselung msg: Verschlüsselung darf nicht leer sein. text: Für die meisten Server ist SSL die empfohlene Option. ssl: SSL tls: TLS none: Keine smtp_port: label: SMTP-Port msg: SMTP-Port muss Nummer 1 ~ 65535 sein. text: Der Port zu deinem Mailserver. smtp_username: label: SMTP-Benutzername msg: Der SMTP-Benutzername darf nicht leer sein. smtp_password: label: SMTP-Kennwort msg: Das SMTP-Passwort darf nicht leer sein. test_email_recipient: label: Test-E-Mail-Empfänger text: Gib die E-Mail-Adresse an, an die Testsendungen gesendet werden sollen. msg: Test-E-Mail-Empfänger ist ungültig smtp_authentication: label: Authentifizierung aktivieren title: SMTP-Authentifizierung msg: Die SMTP-Authentifizierung darf nicht leer sein. "yes": "Ja" "no": "Nein" branding: page_title: Branding logo: label: Logo msg: Logo darf nicht leer sein. text: Das Logobild oben links auf deiner Website. Verwende ein breites rechteckiges Bild mit einer Höhe von 56 und einem Seitenverhältnis von mehr als 3:1. Wenn du es leer lässt, wird der Text des Website-Titels angezeigt. mobile_logo: label: Mobiles Logo text: Das Logo wird auf der mobilen Version deiner Website verwendet. Verwende ein breites rechteckiges Bild mit einer Höhe von 56. Wenn du nichts angibst, wird das Bild aus der Einstellung "Logo" verwendet. square_icon: label: Quadratisches Symbol msg: Quadratisches Symbol darf nicht leer sein. text: Bild, das als Basis für Metadatensymbole verwendet wird. Sollte idealerweise größer als 512x512 sein. favicon: label: Favicon text: Ein Favicon für deine Website. Um korrekt über ein CDN zu funktionieren, muss es ein png sein. Es wird auf 32x32 verkleinert. Wenn du es leer lässt, wird das "quadratische Symbol" verwendet. legal: page_title: Rechtliches terms_of_service: label: Nutzungsbedingungen text: "Du kannst hier Inhalte zu den Nutzungsbedingungen hinzufügen. Wenn du bereits ein Dokument hast, das anderswo gehostet wird, gib hier die vollständige URL an." privacy_policy: label: Datenschutzbestimmungen text: "Du kannst hier Inhalte zur Datenschutzerklärung hinzufügen. Wenn du bereits ein Dokument hast, das anderswo gehostet wird, gib hier die vollständige URL an." external_content_display: label: Externer Inhalt text: "Inhalte umfassen Bilder, Videos und Medien, die von externen Websites eingebettet sind." always_display: Externen Inhalt immer anzeigen ask_before_display: Vor der Anzeige externer Inhalte fragen write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Antwort bearbeiten label: Jeder Benutzer kann für jede Frage nur eine Antwort schreiben text: "Schalten Sie aus, um es Benutzern zu ermöglichen, mehrere Antworten auf dieselbe Frage zu schreiben, was dazu führen kann, dass Antworten nicht im Fokus stehen." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Empfohlene Tags text: "Empfohlene Tags werden standardmäßig in der Dropdown-Liste angezeigt." msg: contain_reserved: "empfohlene Tags dürfen keine reservierten Tags enthalten" required_tag: title: Benötigte Tags festlegen label: '"Empfohlene Tags" als erforderliche Tags festlegen' text: "Jede neue Frage muss mindestens ein Empfehlungs-Tag haben." reserved_tags: label: Reservierte Tags text: "Reservierte Tags können nur vom Moderator verwendet werden." image_size: label: Maximale Bildgröße (MB) text: "Die maximale Bildladegröße." attachment_size: label: Maximale Anhanggröße (MB) text: "Die maximale Dateigröße für Dateianhänge." image_megapixels: label: Max. BildmePixel text: "Maximale Anzahl an Megapixeln für ein Bild." image_extensions: label: Autorisierte Bilderweiterungen text: "Eine Liste von Dateierweiterungen, die für die Anzeige von Bildern erlaubt sind, getrennt durch Kommata." attachment_extensions: label: Autorisierte Anhänge Erweiterungen text: "Eine Liste von Dateierweiterungen, die für das Hochladen erlaubt sind, getrennt mit Kommas. WARNUNG: Erlaubt Uploads kann Sicherheitsprobleme verursachen." seo: page_title: SEO permalink: label: Dauerlink text: Benutzerdefinierte URL-Strukturen können die Benutzerfreundlichkeit und die Vorwärtskompatibilität deiner Links verbessern. robots: label: robots.txt text: Dadurch werden alle zugehörigen Site-Einstellungen dauerhaft überschrieben. themes: page_title: Themen themes: label: Themen text: Wähle ein bestehendes Thema aus. color_scheme: label: Farbschema navbar_style: label: Hintergrundstil der Navigationsleiste primary_color: label: Primäre Farbe text: Ändere die Farben, die von deinen Themes verwendet werden layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS und HTML custom_css: label: Benutzerdefinierte CSS text: > head: label: Kopf text: > header: label: Header text: > footer: label: Fusszeile text: Dies wird vor eingefügt. sidebar: label: Seitenleiste text: Dies wird in die Seitenleiste eingefügt. login: page_title: Anmeldung membership: title: Mitgliedschaft label: Neuregistrierungen zulassen text: Schalte sie ab, um zu verhindern, dass jemand ein neues Konto erstellt. email_registration: title: E-Mail Registrierung label: E-Mail-Registrierung zulassen text: Abschalten, um zu verhindern, dass jemand ein neues Konto per E-Mail erstellt. allowed_email_domains: title: Zugelassene E-Mail-Domänen text: E-Mail-Domänen, bei denen die Nutzer Konten registrieren müssen. Eine Domäne pro Zeile. Wird ignoriert, wenn leer. private: title: Privatgelände label: Anmeldung erforderlich text: Nur angemeldete Benutzer können auf diese Community zugreifen. password_login: title: Passwort-Login label: E-Mail-und Passwort-Login erlauben text: "WARNUNG: Wenn du diese Option abschaltest, kannst du dich möglicherweise nicht mehr anmelden, wenn du zuvor keine andere Anmeldemethode konfiguriert hast." installed_plugins: title: Installierte Plugins plugin_link: Plugins erweitern und ergänzen die Funktionalität. Du kannst Plugins im <1>Pluginverzeichnis finden. filter: all: Alle active: Aktiv inactive: Inaktiv outdated: Veraltet plugins: label: Erweiterungen text: Wähle ein bestehendes Plugin aus. name: Name version: Version status: Status action: Aktion deactivate: Deaktivieren activate: Aktivieren settings: Einstellungen settings_users: title: Benutzer avatar: label: Standard-Avatar text: Für Benutzer ohne einen eigenen Avatar. gravatar_base_url: label: Gravatar Base URL text: URL der API-Basis des Gravatar-Anbieters. Wird ignoriert, wenn leer. profile_editable: title: Profil bearbeitbar allow_update_display_name: label: Benutzern erlauben, ihren Anzeigenamen zu ändern allow_update_username: label: Benutzern erlauben, ihren Benutzernamen zu ändern allow_update_avatar: label: Benutzern erlauben, ihr Profilbild zu ändern allow_update_bio: label: Benutzern erlauben, ihr Über mich zu ändern allow_update_website: label: Benutzern erlauben, ihre Website zu ändern allow_update_location: label: Benutzern erlauben, ihren Standort zu ändern privilege: title: Berechtigungen level: label: Benötigtes Reputations-Level text: Wähle die für die Privilegien erforderliche Reputation aus msg: should_be_number: die Eingabe muss numerisch sein number_larger_1: Zahl muss gleich oder größer als 1 sein badges: action: Aktion active: Aktiv activate: Aktivieren all: Alle awards: Verliehen deactivate: Deaktivieren filter: placeholder: Nach Namen, Abzeichen:id filtern group: Gruppe inactive: Inaktiv name: Name show_logs: Protokolle anzeigen status: Status title: Abzeichen apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optional) empty: kann nicht leer sein invalid: ist ungültig btn_submit: Speichern not_found_props: "Erforderliche Eigenschaft {{ key }} nicht gefunden." select: Auswählen page_review: review: Überprüfung proposed: vorgeschlagen question_edit: Frage bearbeiten answer_edit: Antwort bearbeiten tag_edit: Tag bearbeiten edit_summary: Zusammenfassung bearbeiten edit_question: Frage bearbeiten edit_answer: Antwort bearbeiten edit_tag: Tag bearbeiten empty: Keine Überprüfungsaufgaben mehr übrig. approve_revision_tip: Akzeptieren Sie diese Revision? approve_flag_tip: Sind Sie mit diesem Bericht einverstanden? approve_post_tip: Bestätigen Sie diesen Beitrag? approve_user_tip: Bestätigen Sie diesen Benutzer? suggest_edits: Änderungsvorschläge flag_post: Beitrag melden flag_user: Nutzer melden queued_post: Beitrag in Warteschlange queued_user: Benutzer in der Warteschlange filter_label: Typ reputation: ansehen flag_post_type: Diesen Beitrag als {{ type }} markiert. flag_user_type: Diesen Benutzer als {{ type }} markiert. edit_post: Beitrag bearbeiten list_post: Ausgestellte Beiträge unlist_post: Versteckte Beiträge timeline: undeleted: ungelöscht deleted: gelöscht downvote: ablehnen upvote: positiv bewerten accept: akzeptieren cancelled: abgebrochen commented: kommentiert rollback: zurückrollen edited: bearbeitet answered: antwortete asked: gefragt closed: geschlossen reopened: wiedereröffnet created: erstellt pin: angeheftet unpin: losgelöst show: gelistet hide: nicht gelistet title: "Verlauf von" tag_title: "Zeitleiste für" show_votes: "Stimmen anzeigen" n_or_a: Keine Angaben title_for_question: "Zeitleiste für" title_for_answer: "Zeitachse für die Antwort auf {{ title }} von {{ author }}" title_for_tag: "Zeitachse für Tag" datetime: Terminzeit type: Typ by: Von comment: Kommentar no_data: "Wir konnten nichts finden." users: title: Benutzer users_with_the_most_reputation: Benutzer mit den höchsten Reputationspunkten dieser Woche users_with_the_most_vote: Benutzer, die diese Woche am meisten gestimmt haben staffs: Unsere Community Teammitglieder reputation: Ansehen votes: Stimmen prompt: leave_page: Bist du sicher, dass du die Seite verlassen willst? changes_not_save: Deine Änderungen werden möglicherweise nicht gespeichert. draft: discard_confirm: Bist du sicher, dass du deinen Entwurf verwerfen willst? messages: post_deleted: Dieser Beitrag wurde gelöscht. post_cancel_deleted: Dieser Beitrag wurde wiederhergestellt. post_pin: Dieser Beitrag wurde angepinnt. post_unpin: Dieser Beitrag wurde losgelöst. post_hide_list: Dieser Beitrag wurde aus der Liste verborgen. post_show_list: Dieser Beitrag wird in der Liste angezeigt. post_reopen: Dieser Beitrag wurde wieder geöffnet. post_list: Dieser Beitrag wurde angezeigt. post_unlist: Dieser Beitrag wurde ausgeblendet. post_pending: Dein Beitrag wartet auf eine Überprüfung. Dies ist eine Vorschau, sie wird nach der Genehmigung sichtbar sein. post_closed: Dieser Beitrag wurde gelöscht. answer_deleted: Diese Antwort wurde gelöscht. answer_cancel_deleted: Diese Antwort wurde wiederhergestellt. change_user_role: Die Rolle dieses Benutzers wurde geändert. user_inactive: Dieser Benutzer ist bereits inaktiv. user_normal: Dieser Benutzer ist bereits normal. user_suspended: Dieser Nutzer wurde gesperrt. user_deleted: Benutzer wurde gelöscht. user_added: User has been added successfully. badge_activated: Dieses Abzeichen wurde aktiviert. badge_inactivated: Dieses Abzeichen wurde deaktiviert. users_deleted: Der Benutzer wurde gelöscht. posts_deleted: Deine Frage wurde gelöscht. answers_deleted: Deine Antwort wurde gelöscht. copy: In die Zwischenablage kopieren copied: Kopiert external_content_warning: Externe Bilder/Medien werden nicht angezeigt. ================================================ FILE: i18n/el_GR.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. email: other: Email password: other: Password email_or_password_wrong_error: other: Email and password do not match. error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: not_found: other: Tag not found. recommend_tag_not_found: other: Recommend Tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: other: The From Name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to Revision. user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. report: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude: name: other: rude or abusive desc: other: A reasonable person would find this content inappropriate for respectful discourse. duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. not_answer: name: other: not an answer desc: other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. not_need: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. other: name: other: something else desc: other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comments tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to {{site_name}} btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to {{site_name}} success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/en_US.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. forbidden_error: other: Forbidden. duplicate_request_error: other: Duplicate submission. action: report: other: Flag edit: other: Edit delete: other: Delete close: other: Close reopen: other: Reopen forbidden_error: other: Forbidden. pin: other: Pin hide: other: Unlist unpin: other: Unpin show: other: List invite_someone_to_answer: other: Edit undelete: other: Undelete merge: other: Merge role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. privilege: level_1: description: other: Level 1 (less reputation required for private team, group) level_2: description: other: Level 2 (low reputation required for startup community) level_3: description: other: Level 3 (high reputation required for mature community) level_custom: description: other: Custom Level rank_question_add_label: other: Ask question rank_answer_add_label: other: Write answer rank_comment_add_label: other: Write comment rank_report_add_label: other: Flag rank_comment_vote_up_label: other: Upvote comment rank_link_url_limit_label: other: Post more than 2 links at a time rank_question_vote_up_label: other: Upvote question rank_answer_vote_up_label: other: Upvote answer rank_question_vote_down_label: other: Downvote question rank_answer_vote_down_label: other: Downvote answer rank_invite_someone_to_answer_label: other: Invite someone to answer rank_tag_add_label: other: Create new tag rank_tag_edit_label: other: Edit tag description (need to review) rank_question_edit_label: other: Edit other's question (need to review) rank_answer_edit_label: other: Edit other's answer (need to review) rank_question_edit_without_review_label: other: Edit other's question without review rank_answer_edit_without_review_label: other: Edit other's answer without review rank_question_audit_label: other: Review question edits rank_answer_audit_label: other: Review answer edits rank_tag_audit_label: other: Review tag edits rank_tag_edit_without_review_label: other: Edit tag description without review rank_tag_synonym_label: other: Manage tag synonyms email: other: Email e_mail: other: Email password: other: Password pass: other: Password old_pass: other: Current password original_text: other: This post email_or_password_wrong_error: other: Email and password do not match. error: common: invalid_url: other: Invalid URL. status_invalid: other: Invalid status. password: space_invalid: other: Password cannot contain spaces. admin: cannot_update_their_password: other: You cannot modify your password. cannot_edit_their_profile: other: You cannot modify your profile. cannot_modify_self_status: other: You cannot modify your status. email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. question_closed_cannot_add: other: Questions are closed and cannot be added. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. illegal_email_domain_error: other: Email is not allowed from that email domain. Please use another one. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. already_deleted: other: This post has been deleted. meta: object_not_found: other: Meta object not found question: already_deleted: other: This post has been deleted. under_review: other: Your post is awaiting review. It will be visible after it has been approved. not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. vote_fail_to_meet_the_condition: other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. no_enough_rank_to_operate: other: You need at least {{.Rank}} reputation to do this. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: already_exist: other: Tag already exists. not_found: other: Tag not found. recommend_tag_not_found: other: Recommend tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. is_used_cannot_delete: other: You cannot delete a tag that is in use. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: The from name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to revise. user: external_login_missing_user_id: other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. external_login_unbinding_forbidden: other: Please set a login password for your account before you remove this login. email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration. not_allowed_login_via_password: other: Currently the site is not allowed to login via password. access_denied: other: Access denied page_access_denied: other: You do not have access to this page. add_bulk_users_format_error: other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. site_info: config_not_found: other: Site config not found. badge: object_not_found: other: Badge object not found reason: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude_or_abusive: name: other: rude or abusive desc: other: "A reasonable person would find this content inappropriate for respectful discourse." a_duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. placeholder: other: Enter the existing question link not_a_answer: name: other: not an answer desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." no_longer_needed: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. something: name: other: something else desc: other: This post requires staff attention for another reason not listed above. placeholder: other: Let us know specifically what you are concerned about community_specific: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. not_clarity: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. looks_ok: name: other: looks OK desc: other: This post is good as-is and not low quality. needs_edit: name: other: needs edit, and I did it desc: other: Improve and correct problems with this post yourself. needs_close: name: other: needs close desc: other: A closed question can't answer, but still can edit, vote and comment. needs_delete: name: other: needs delete desc: other: This post will be deleted. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified deleted_title: other: Deleted question questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted up_voted_question: other: upvoted question down_voted_question: other: downvoted question up_voted_answer: other: upvoted answer down_voted_answer: other: downvoted answer up_voted_comment: other: upvoted comment invited_you_to_answer: other: invited you to answer earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "[{{.SiteName}}] Confirm your new email address" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Password reset" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Confirm your new account" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Test Email" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: upvote upvoted: other: upvoted downvote: other: downvote downvoted: other: downvoted accept: other: accept accepted: other: accepted edit: other: edit review: queued_post: other: Queued post flagged_post: other: Flagged post suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: Editor desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki create_tag: Create Tag edit_tag: Edit Tag ask_a_question: Create Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users oauth_callback: Processing http_404: HTTP Error 404 http_50X: HTTP Error 500 http_403: HTTP Error 403 logout: Log Out posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notifications inbox: Inbox achievement: Achievements new_alerts: New alerts all_read: Mark all as read show_more: Show more someone: Someone inbox_type: all: All posts: Posts invites: Invites votes: Votes answer: Answer question: Question badge_award: Badge suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. contact_us: Contact us editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image file btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed {{size}} MB. desc: label: Description tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered list unordered_list: text: Bulleted list table: text: Table heading: Heading cell: Cell file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: Create new tag form: fields: display_name: label: Display name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description revision: label: Revision edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_cancel: Cancel btn_submit: Submit btn_post: Post new tag tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Are you sure you wish to delete? close: Close merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Edit Tag default_reason: Edit tag default_first_reason: Add tag btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day hours: hours days: days month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: "{{count}} more comments" tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. tip_vote: It adds something useful to the post edit_answer: title: Edit Answer default_reason: Edit answer default_first_reason: Add answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: Newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More wiki: Wiki ask: title: Create Question edit_title: Edit Question default_reason: Edit question default_first_reason: Create question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: What's your topic? Be specific. msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users badges: Badges profile: Profile setting: Settings logout: Log out admin: Admin review: Review bookmark: Bookmarks moderation: Moderation search: placeholder: Search footer: build_on: Powered by <1> Apache Answer upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. resend_email: url_label: Are you sure you want to resend the activation email? url_text: You can also give the activation link above to the user. login: login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New email msg: empty: Email cannot be empty. oauth: connect: Connect with {{ auth_name }} remove: Remove {{ auth_name }} oauth_bind_email: subtitle: Add a recovery email to your account. btn_update: Update email address email: label: Email msg: empty: Email cannot be empty. modal_title: Email already existes. modal_content: This email address already registered. Are you sure you want to connect to the existing account? modal_cancel: Change email modal_confirm: Connect to the existing account password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm new password settings: page_title: Settings goto_modify: Go to modify nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile image gravatar: Gravatar gravatar_text: You can change image on custom: Custom custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About me website: label: Website placeholder: "https://example.com" msg: Website incorrect format location: label: Location placeholder: "City, Country" notification: heading: Email Notifications turn_on: Turn on inbox: label: Inbox notifications description: Answers to your questions, comments, invites, and more. all_new_question: label: All new questions description: Get notified of all new questions. Up to 50 questions per week. all_new_question_for_following_tags: label: All new questions for following tags description: Get notified of new questions for following tags. account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. pass: label: Current password msg: Password cannot be empty. password_title: Password current_pass: label: Current password msg: empty: Current password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New password pass_confirm: label: Confirm new password interface: heading: Interface lang: label: Interface language text: User interface language. It will change when you refresh the page. my_logins: title: My logins label: Log in or sign up on this site using these accounts. modal_title: Remove login modal_content: Are you sure you want to remove this login from your account? modal_confirm_btn: Remove remove_success: Removed successfully toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. sent_success: Sent successfully related_question: title: Related answers: answers linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Invite People desc: Invite people you think can answer. invite: Invite to answer add: Add people search: Search people question_detail: action: Action created: Created Asked: Asked asked: asked update: Modified Edited: Edited edit: edited commented: commented Views: Viewed Follow: Follow Following: Following follow_tip: Follow this question to receive notifications answered: answered closed_in: Closed in show_exist: Show existing question. useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: title: Answers score: Score newest: Newest oldest: Oldest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer edit_answer: Edit my existing answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. tips: header_1: Thanks for your answer li1_1: Please be sure to answer the question. Provide details and share your research. li1_2: Back up any statements you make with references or personal experience. header_2: But avoid ... li2_1: Asking for help, seeking clarification, or responding to other answers. reopen: confirm_btn: Reopen title: Reopen this post content: Are you sure you want to reopen? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. confirm_btn: Pin delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_answer_deleted: This answer has been deleted undelete_title: Undelete this post undelete_desc: Are you sure you wish to undelete? btns: confirm: Confirm cancel: Cancel edit: Edit save: Save delete: Delete undelete: Undelete list: List unlist: Unlist unlisted: Unlisted login: Log in signup: Sign up logout: Log out verify: Verify create: Create approve: Approve reject: Reject skip: Skip discard_draft: Discard draft pinned: Pinned all: All question: Question answer: Answer comment: Comment refresh: Refresh resend: Resend deactivate: Deactivate active: Active suspend: Suspend unsuspend: Unsuspend close: Close reopen: Reopen ok: OK light: Light dark: Dark system_setting: System setting default: Default reset: Reset tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" counts_loading: "... Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Error... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage oops: Oops! invalid: The link you used no longer works. confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" x_posts: "{{ count }} Posts" questions: Questions answers: Answers newest: Newest active: Active hot: Hot frequent: Frequent recommend: Recommend score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes badges: Badges newest: Newest score: Score edit_profile: Edit profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined comma: "," last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? content_empty: No posts found. accepted: Accepted answered: answered asked: asked downvoted: downvoted mod_short: MOD mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions recent_badges: Recent Badges install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please choose a language db_type: label: Database engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database host placeholder: "db:3306" msg: Database host cannot be empty. db_name: label: Database name placeholder: answer msg: Database name cannot be empty. db_file: label: Database file placeholder: /data/answer.db msg: Database file cannot be empty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site name msg: Site name cannot be empty. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. max_length: Site URL must be at maximum 512 characters in length. contact_email: label: Contact email text: Email address of key contact responsible for this site. msg: empty: Contact email cannot be empty. incorrect: Contact email incorrect format. login_required: label: Private switch: Login required text: Only logged in users can access this community. admin_name: label: Name msg: Name cannot be empty. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_error: http_error: HTTP Error {{ code }} desc_403: You don't have permission to access this page. desc_404: Unfortunately, this page doesn't exist. desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users badges: Badges flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write terms: Terms tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes login: Login privileges: Privileges plugins: Plugins installed_plugins: Installed Plugins apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Welcome to {{site_name}} user_center: login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site statistics questions: "Questions:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Answers:" comments: "Comments:" votes: "Votes:" users: "Users:" flags: "Flags:" reviews: "Reviews:" site_health: Site health version: "Version:" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Private public: Public smtp: "SMTP:" timezone: "Timezone:" system_info: System info go_version: "Go version:" database: "Database:" database_size: "Database size:" storage_used: "Storage used:" uptime: "Uptime:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Contact forum: Forum documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled writable: Writable not_writable: Not writable flags: title: Flags pending: Pending completed: Completed flagged: Flagged flagged_type: Flagged {{ type }} created: Created action: Action review: Review user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: users: label: Bulk add user placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separate “name, email, password” with commas. One user per line. msg: "Please enter the user's email, one per line." display_name: label: Display name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Status role: Role action: Action change: Change all: All staff: Staff more: More inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password edit_profile: Edit profile change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user deactivate_user: title: Deactivate user content: An inactive user must re-validate their email. delete_user: title: Delete this user content: Are you sure you want to delete this user? This is permanent! remove: Remove their content label: Remove all questions, answers, comments, etc. text: Don’t check this if you wish to only delete the user’s account. suspend_user: title: Suspend this user content: A suspended user can't log in. label: How long will the user be suspended for? forever: Forever questions: page_title: Questions unlisted: Unlisted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change pending: Pending filter: placeholder: "Filter by title, question:id" answers: page_title: Answers post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short site description msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site description msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. check_update: label: Software updates text: Automatically check for updates interface: page_title: Interface language: label: Interface language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: From email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL tls: TLS none: None smtp_port: label: SMTP port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP username msg: SMTP username cannot be empty. smtp_password: label: SMTP password msg: SMTP password cannot be empty. test_email_recipient: label: Test email recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile logo text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square icon msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for the same question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. color_scheme: label: Color scheme navbar_style: label: Navbar background style primary_color: label: Primary color text: Modify the colors used by your themes layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as <link> head: label: Head text: This will insert before </head> header: label: Header text: This will insert after <body> footer: label: Footer text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: Allowed email domains text: Email domains that users must register accounts with. One domain per line. Ignored when empty. private: title: Private label: Login required text: Only logged in users can access this community. password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: All active: Active inactive: Inactive outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Name version: Version status: Status action: Action deactivate: Deactivate activate: Activate settings: Settings settings_users: title: Users avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Allow users to change their display name allow_update_username: label: Allow users to change their username allow_update_avatar: label: Allow users to change their profile image allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optional) empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." select: Select page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created pin: pinned unpin: unpinned show: listed hide: unlisted title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: Our community staff reputation: reputation votes: votes prompt: leave_page: Are you sure you want to leave the page? changes_not_save: Your changes may not be saved. draft: discard_confirm: Are you sure you want to discard your draft? messages: post_deleted: This post has been deleted. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/es_ES.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Completado. unknown: other: Error desconocido. request_format_error: other: Formato de la solicitud inválido. unauthorized_error: other: No autorizado. database_error: other: Error en el servidor de datos. forbidden_error: other: Prohibido. duplicate_request_error: other: Solicitud duplicada. action: report: other: Reportar edit: other: Editar delete: other: Eliminar close: other: Cerrar reopen: other: Reabrir forbidden_error: other: Prohibido. pin: other: Fijar hide: other: Retirar unpin: other: Desfijar show: other: Lista invite_someone_to_answer: other: Editar undelete: other: Recuperar merge: other: Merge role: name: user: other: Usuario admin: other: Administrador moderator: other: Moderador description: user: other: Predeterminado sin acceso especial. admin: other: Dispone de acceso total al sitio web y sus ajustes. moderator: other: Dispone de acceso a todas las publicaciones pero no a los ajustes de administrador. privilege: level_1: description: other: Nivel 1 (reputación menor requerida para un equipo privado, grupo) level_2: description: other: Nivel 2 (reputación baja requerida para una comunidad de Startup) level_3: description: other: Nivel 3 (reputación alta requerida para una comunidad madura) level_custom: description: other: Nivel personalizado rank_question_add_label: other: Hacer una pregunta rank_answer_add_label: other: Escribir respuesta rank_comment_add_label: other: Escribir comentario rank_report_add_label: other: Reportar rank_comment_vote_up_label: other: Votar comentario a favor rank_link_url_limit_label: other: Publica más de 2 enlaces a la vez rank_question_vote_up_label: other: Votar pregunta a favor rank_answer_vote_up_label: other: Votar respuesta a favor rank_question_vote_down_label: other: Votar pregunta en contra rank_answer_vote_down_label: other: Votar respuesta en contra rank_invite_someone_to_answer_label: other: Invitar a alguien a responder rank_tag_add_label: other: Crear nueva etiqueta rank_tag_edit_label: other: Editar descripción de etiqueta (revisión necesaria) rank_question_edit_label: other: Editar pregunta ajena (revisión necesaria) rank_answer_edit_label: other: Editar respuesta ajena (revisión necesaria) rank_question_edit_without_review_label: other: Editar pregunta ajena sin revisión rank_answer_edit_without_review_label: other: Editar respuesta ajena sin revisión rank_question_audit_label: other: Revisar ediciones de pregunta rank_answer_audit_label: other: Revisar ediciones de respuesta rank_tag_audit_label: other: Revisar ediciones de etiqueta rank_tag_edit_without_review_label: other: Editar descripción de etiqueta sin revisión rank_tag_synonym_label: other: Administrar sinónimos de etiqueta email: other: Correo electrónico e_mail: other: Correo electrónico password: other: Contraseña pass: other: Contraseña old_pass: other: Current password original_text: other: Esta publicación email_or_password_wrong_error: other: Contraseña o correo incorrecto. error: common: invalid_url: other: URL no válido. status_invalid: other: Estado inválido. password: space_invalid: other: La contraseña no puede contener espacios. admin: cannot_update_their_password: other: No puede modificar su contraseña. cannot_edit_their_profile: other: No puede modificar su perfil. cannot_modify_self_status: other: No puede modificar su contraseña. email_or_password_wrong: other: Contraseña o correo incorrecto. answer: not_found: other: Respuesta no encontrada. cannot_deleted: other: Sin permiso para eliminar. cannot_update: other: Sin permiso para actualizar. question_closed_cannot_add: other: Las preguntas están cerradas y no pueden añadirse. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Edición del comentario no permitida. not_found: other: Comentario no encontrado. cannot_edit_after_deadline: other: El tiempo del comentario ha sido demasiado largo para modificarlo. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: Correo electrónico ya en uso. need_to_be_verified: other: El correo debe ser verificado. verify_url_expired: other: La URL verificada del correo electrónico ha caducado. Por favor, vuelva a enviar el correo electrónico. illegal_email_domain_error: other: No está permitido el correo electrónico de ese dominio. Por favor utilice otro. lang: not_found: other: Archivo de idioma no encontrado. object: captcha_verification_failed: other: Captcha fallido. disallow_follow: other: No dispones de permiso para seguir. disallow_vote: other: No dispones de permiso para votar. disallow_vote_your_self: other: No puedes votar a tu propia publicación. not_found: other: Objeto no encontrado. verification_failed: other: Verificación fallida. email_or_password_incorrect: other: Contraseña o correo incorrecto. old_password_verification_failed: other: La verificación de la contraseña antigua falló new_password_same_as_previous_setting: other: La nueva contraseña es igual a la anterior. already_deleted: other: Esta publicación ha sido borrada. meta: object_not_found: other: Meta objeto no encontrado question: already_deleted: other: Esta publicación ha sido eliminada. under_review: other: Tu publicación está siendo revisada. Será visible una vez sea aprobada. not_found: other: Pregunta no encontrada. cannot_deleted: other: Sin permiso para eliminar. cannot_close: other: Sin permiso para cerrar. cannot_update: other: Sin permiso para actualizar. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: El rango de reputación no cumple la condición. vote_fail_to_meet_the_condition: other: Gracias por los comentarios. Necesitas al menos reputación {{.Rank}} para votar. no_enough_rank_to_operate: other: Necesitas al menos reputación {{.Rank}} para hacer esto. report: handle_failed: other: Error en el manejador del reporte. not_found: other: Informe no encontrado. tag: already_exist: other: La etiqueta ya existe. not_found: other: Etiqueta no encontrada. recommend_tag_not_found: other: La etiqueta recomendada no existe. recommend_tag_enter: other: Por favor, introduce al menos una de las etiquetas requeridas. not_contain_synonym_tags: other: No debe contener etiquetas sinónimas. cannot_update: other: Sin permiso para actualizar. is_used_cannot_delete: other: No puedes eliminar una etiqueta que está en uso. cannot_set_synonym_as_itself: other: No se puede establecer como sinónimo de una etiqueta la propia etiqueta. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: El nombre no puede ser una dirección de correo electrónico. theme: not_found: other: Tema no encontrado. revision: review_underway: other: No se puede editar actualmente, hay una versión en la cola de revisiones. no_permission: other: Sin permisos para ver. user: external_login_missing_user_id: other: La plataforma de terceros no proporciona un UserID único, por lo que si no puede iniciar sesión, contacte al administrador del sitio. external_login_unbinding_forbidden: other: Por favor añada una contraseña de inicio de sesión a su cuenta antes de eliminar este método de acceso. email_or_password_wrong: other: other: Contraseña o correo incorrecto. not_found: other: Usuario no encontrado. suspended: other: El usuario ha sido suspendido. username_invalid: other: Nombre de usuario no válido. username_duplicate: other: El nombre de usuario ya está en uso. set_avatar: other: Fallo al actualizar el avatar. cannot_update_your_role: other: No puedes modificar tu propio rol. not_allowed_registration: other: Actualmente el sitio no está abierto para el registro. not_allowed_login_via_password: other: Actualmente el sitio no está abierto para iniciar sesión por contraseña. access_denied: other: Acceso denegado page_access_denied: other: No tienes acceso a esta página. add_bulk_users_format_error: other: "Error {{.Field}} formato cerca de '{{.Content}}' en la línea {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "El número de usuarios que añadas a la vez debe estar en el rango de 1 a {{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Lectura de configuración fallida database: connection_failed: other: Conexión a la base de datos fallida create_table_failed: other: Creación de tabla fallida install: create_config_failed: other: No es posible crear el archivo config.yaml. upload: unsupported_file_format: other: Formato de archivo no soportado. site_info: config_not_found: other: Configuración del sitio no encontrada. badge: object_not_found: other: Insignia no encontrada reason: spam: name: other: correo no deseado desc: other: Esta publicación es un anuncio, o vandalismo. No es útil o relevante para el tema actual. rude_or_abusive: name: other: grosero u ofensivo desc: other: "Alguna persona podría considerar este contenido inapropiado para una discusión respetuosa." a_duplicate: name: other: un duplicado desc: other: Esta pregunta ha sido hecha antes y ya ha sido resuelta. placeholder: other: Introduce el enlace de la pregunta existente not_a_answer: name: other: no es una respuesta desc: other: "Esto fue publicado como respuesta pero no intenta responder a la pregunta. Podría ser una edición, un comentario, otra pregunta diferente o ser eliminado por completo." no_longer_needed: name: other: ya no es necesario desc: other: Este comentario está desactualizado, es conversacional o no es relevante a esta publicación. something: name: other: otro motivo desc: other: Esta publicación requiere revisión del personal por otro motivo no listado arriba. placeholder: other: Háganos saber qué le interesa en concreto community_specific: name: other: un motivo determinado de la comunidad desc: other: Esta pregunta no cumple con una norma comunitaria. not_clarity: name: other: requiere detalles o aclaraciones desc: other: Esta pregunta actualmente incluye múltiples preguntas en una. Debería enfocarse en una única cuestión. looks_ok: name: other: parece correcto desc: other: Esta publicación es buena como es y no es de baja calidad. needs_edit: name: other: necesita editarse, y lo hice desc: other: Mejora y corrige los problemas con esta publicación personalmente. needs_close: name: other: necesita cerrar desc: other: Una pregunta cerrada no puede responderse, pero aún se puede editar, votar y comentar. needs_delete: name: other: necesita eliminación desc: other: Esta publicación será eliminada. question: close: duplicate: name: other: correo no deseado desc: other: La pregunta ya ha sido preguntada y resuelta previamente. guideline: name: other: razón específica de la comunidad desc: other: Esta pregunta infringe alguna norma de la comunidad. multiple: name: other: necesita más detalles o aclaraciónes desc: other: Esta pregunta incluye múltiples preguntas en una sola. Debería centrarse únicamente en un solo tema. other: name: other: otra razón desc: other: Esta publicación requiere otra razón no listada arriba. operation_type: asked: other: preguntada answered: other: respondida modified: other: modificada deleted_title: other: Pregunta eliminada questions_title: other: Preguntas tag: tags_title: other: Etiquetas no_description: other: La etiqueta no tiene descripción. notification: action: update_question: other: pregunta actualizada answer_the_question: other: pregunta respondidas update_answer: other: respuesta actualizada accept_answer: other: respuesta aceptada comment_question: other: pregunta comentada comment_answer: other: respuesta comentada reply_to_you: other: te ha respondido mention_you: other: te ha mencionado your_question_is_closed: other: Tu pregunta ha sido cerrada your_question_was_deleted: other: Tu pregunta ha sido eliminada your_answer_was_deleted: other: Tu respuesta ha sido eliminada your_comment_was_deleted: other: Tu comentario ha sido eliminado up_voted_question: other: pregunta votada a favor down_voted_question: other: pregunta votada en contra up_voted_answer: other: respuesta votada a favor down_voted_answer: other: respuesta votada en contra up_voted_comment: other: comentario votado a favor invited_you_to_answer: other: te invitó a responder earned_badge: other: Ha ganado la insignia "{{.BadgeName}}" email_tpl: change_email: title: other: "[{{.SiteName}}] Confirma tu nueva dirección de correo" body: other: "Confirme su nueva dirección de correo electrónico para {{.SiteName}} haciendo clic en el siguiente enlace:
\n{{.ChangeEmailUrl}}

\n\nSi no solicitó este cambio, por favor, ignore este mensaje.

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} respondió tu pregunta" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nVerlo en {{.SiteName}}

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.

\n\nDarse de baja" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} te invitó a responder" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Es posible que conozca la respuesta.

\nVerlo en {{.SiteName}}

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.

\n\nDarse de baja" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} comentó en tu publicación" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nVerlo en {{.SiteName}}

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.

\n\nDarse de baja" new_question: title: other: "[{{.SiteName}}] Nueva pregunta: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Reestablecimiento de contraseña" body: other: "Se solicitó el restablecimiento de su contraseña en {{.SiteName}}.

\n\nSi no lo solicitó, puede ignorar este mensaje.

\n\nHaga clic en el siguiente enlace para generar una nueva contraseña:
\n{{.PassResetUrl}}\n

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída." register: title: other: "[{{.SiteName}}] Confirma tu nueva cuenta" body: other: "¡Bienvenido a {{.SiteName}}!

\n\nHaga clic en el siguiente enlace y active su nueva cuenta:
\n{{.RegisterUrl}}

\n\nSi no puede hacer clic en el enlace anterior, intente copiando y pegando el enlace en la barra de dirección en su navegador web.

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída." test: title: other: "[{{.SiteName}}] Correo de prueba" body: other: "Este es un mensaje de prueba.\n

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída." action_activity_type: upvote: other: votar a favor upvoted: other: votado a favor downvote: other: voto negativo downvoted: other: votado en contra accept: other: aceptar accepted: other: aceptado edit: other: editar review: queued_post: other: Publicación en cola flagged_post: other: Publicación marcada suggested_post_edit: other: Ediciones sugeridas reaction: tooltip: other: "{{ .Names }} y {{ .Count }} más..." badge: default_badges: autobiographer: name: other: Autobiógrafo desc: other: Completó la información de su perfil. certified: name: other: Certificado desc: other: Completó nuestro nuevo tutorial de usuario. editor: name: other: Editor desc: other: Primer mensaje editado. first_flag: name: other: Primera Denuncia desc: other: Primer denuncia de un post. first_upvote: name: other: Primer voto favorable desc: other: Primera vez que le doy un 'like' a un post. first_link: name: other: Primer Enlace desc: other: First added a link to another post. first_reaction: name: other: Primera reacción desc: other: Primero reaccionó al post. first_share: name: other: Primer Compartir desc: other: Primero compartió un post. scholar: name: other: Erudito desc: other: Hecha una pregunta y aceptada una respuesta. commentator: name: other: Comentador desc: other: Deja 5 comentarios. new_user_of_the_month: name: other: Nuevo usuario del mes desc: other: Contribuciones pendientes en su primer mes. read_guidelines: name: other: Lea los lineamientos desc: other: Lea las [directrices de la comunidad]. reader: name: other: Lector desc: other: Lee cada respuesta en un tema con más de 10 respuestas. welcome: name: other: Bienvenido desc: other: Recibió un voto a favor. nice_share: name: other: Buena Compartición desc: other: Compartió un post con 25 visitantes únicos. good_share: name: other: Buena Compartida desc: other: Compartió un post con 300 visitantes únicos. great_share: name: other: Excelente Compartida desc: other: Compartió un post con 1000 visitantes únicos. out_of_love: name: other: Fuera del Amor desc: other: Utilizó 50 votos positivos en un día. higher_love: name: other: Amor Más Alto desc: other: Utilizó 50 votos positivos en un día 5 veces. crazy_in_love: name: other: Loco(a) por el Amor desc: other: Utilizó 50 votos positivos en un día 20 veces. promoter: name: other: Promotor desc: other: Invitó a un usuario. campaigner: name: other: Activista desc: other: Invitó a 3 usuarios básicos. champion: name: other: Campeón desc: other: Invitado 5 miembros. thank_you: name: other: Gracias desc: other: Tiene 20 publicaciones con votos positivos y dio 10 votos positivos. gives_back: name: other: Da a Cambio desc: other: Tiene 100 publicaciones con votos positivos y dio 100 votos positivos. empathetic: name: other: Empático desc: other: Tiene 500 publicaciones con votos positivos y dio 1000 votos positivos. enthusiast: name: other: Entusiasta desc: other: Visita 10 días consecutivos. aficionado: name: other: Aficionado desc: other: Visita 100 días consecutivos. devotee: name: other: Devoto desc: other: Visita 365 días consecutivos. anniversary: name: other: Aniversario desc: other: Miembro activo por un año, publicó al menos una vez. appreciated: name: other: Apreciación desc: other: Recibió 1 voto positivo en 20 puestos. respected: name: other: Respetado desc: other: Recibió 2 voto positivo en 100 puestos. admired: name: other: Admirado desc: other: Recibió 5 voto positivo en 300 puestos. solved: name: other: Resuelto desc: other: Tener una respuesta aceptada. guidance_counsellor: name: other: Consejero de Orientación desc: other: Tener 10 respuestas aceptadas. know_it_all: name: other: Sabelotodo desc: other: Tener 50 respuestas aceptadas. solution_institution: name: other: Institución de Soluciones desc: other: Tener 150 respuestas aceptadas. nice_answer: name: other: Buena Respuesta desc: other: Respuesta con una puntuación de 10 o más. good_answer: name: other: Excelente Respuesta desc: other: Respuesta con una puntuación de 25 o más. great_answer: name: other: Gran Respuesta desc: other: Respuesta con una puntuación de 50 o más. nice_question: name: other: Buena Pregunta desc: other: Pregunta con una puntuación de 10 o más. good_question: name: other: Excelente Pregunta desc: other: Pregunta con una puntuación de 25 o más. great_question: name: other: Gran Pregunta desc: other: Pregunta con una puntuación de 50 o más. popular_question: name: other: Pregunta popular desc: other: Pregunta con 500 puntos de vista. notable_question: name: other: Pregunta Notable desc: other: Pregunta con 1,000 vistas. famous_question: name: other: Pregunta Famosa desc: other: Pregunta con 5,000 vistas. popular_link: name: other: Enlace Popular desc: other: Publicado un enlace externo con 50 clics. hot_link: name: other: Enlace caliente desc: other: Publicado un enlace externo con 300 clics. famous_link: name: other: Enlace familiar desc: other: Publicado un enlace externo con 100 clics. default_badge_groups: getting_started: name: other: Primeros pasos community: name: other: Comunidad posting: name: other: Publicación # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Cómo formatear desc: >-
  • menciona una publicación: #post_id

  • para hacer enlaces

    <https://url.com>

    [Título](https://url.com)
  • poner retornos entre párrafos

  • _italic_ o **negrita**

  • sangría del código con 4 espacios

  • cita colocando > al inicio de la línea

  • comillas invertidas se escapa `like _this_`

  • crear barreras de código con comillas invertidas `

    ```
    código aquí
    ```
pagination: prev: Anterior next: Siguiente page_title: question: Pregunta questions: Preguntas tag: Etiqueta tags: Etiquetas tag_wiki: wiki de Etiquetas create_tag: Crear etiqueta edit_tag: Editar etiqueta ask_a_question: Create Question edit_question: Editar Pregunta edit_answer: Editar respuesta search: Buscar posts_containing: Publicaciones que contienen settings: Ajustes notifications: Notificaciones login: Acceder sign_up: Registrarse account_recovery: Recuperación de la cuenta account_activation: Activación de la cuenta confirm_email: Confirmar correo electrónico account_suspended: Cuenta suspendida admin: Administrador change_email: Modificar correo install: Instalación de Answer upgrade: Actualización de Answer maintenance: Mantenimiento del sitio web users: Usuarios oauth_callback: Procesando http_404: HTTP Error 404 http_50X: HTTP Error 500 http_403: HTTP Error 403 logout: Cerrar sesión posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notificaciones inbox: Buzón de entrada achievement: Logros new_alerts: Nuevas alertas all_read: Marcar todo como leído show_more: Mostrar más someone: Alguien inbox_type: all: Todo posts: Publicaciones invites: Invitaciones votes: Votos answer: Respuesta question: Pregunta badge_award: Medalla suspended: title: Tu cuenta ha sido suspendida until_time: "Tu cuenta ha sido suspendida hasta el {{ time }}." forever: Este usuario ha sido suspendido indefinidamente. end: Has infringido alguna norma de la comunidad. contact_us: Contáctanos editor: blockquote: text: Cita bold: text: Negrita chart: text: Gráfica flow_chart: Diagrama de flujo sequence_diagram: Diagrama de secuencia class_diagram: Diagrama de clase state_diagram: Diagrama de estado entity_relationship_diagram: Diagrama de relación de entidad user_defined_diagram: Diagrama definido por el usuario gantt_chart: Diagrama de Gantt pie_chart: Grafico de torta code: text: Código add_code: Añadir código form: fields: code: label: Código msg: empty: Código no puede estar vacío. language: label: Idioma placeholder: Detección automática btn_cancel: Cancelar btn_confirm: Añadir formula: text: Fórmula options: inline: Fórmula en línea block: Bloque de fórmula heading: text: Encabezado options: h1: Encabezado 1 h2: Encabezado 2 h3: Encabezado 3 h4: Encabezado 4 h5: Encabezado 5 h6: Encabezado 6 help: text: Ayuda hr: text: Regla horizontal image: text: Imagen add_image: Añadir imagen tab_image: Subir imagen form_image: fields: file: label: Archivo de imagen btn: Seleccionar imagen msg: empty: El título no puede estar vacío. only_image: Solo se permiten archivos de imagen. max_size: El tamaño del archivo no puede exceder {{size}} MB. desc: label: Descripción tab_url: URL de la imagen form_url: fields: url: label: URL de la imagen msg: empty: La URL de la imagen no puede estar vacía. name: label: Descripción btn_cancel: Cancelar btn_confirm: Añadir uploading: Subiendo indent: text: Sangría outdent: text: Quitar sangría italic: text: Cursiva link: text: Enlace add_link: Añadir enlace form: fields: url: label: Por sus siglas en ingles (Localizador Uniforme de recursos), dirección electrónica de un sitio web msg: empty: La dirección no puede estar vacía. name: label: Descripción btn_cancel: Cancelar btn_confirm: Añadir ordered_list: text: Lista numerada unordered_list: text: Lista con viñetas table: text: Tabla heading: Encabezado cell: Celda file: text: Adjuntar archivos not_supported: "No soporta ese tipo de archivo. Inténtalo de nuevo con {{file_type}}." max_size: "El tamaño de los archivos adjuntos no puede exceder {{size}} MB." close_modal: title: Estoy cerrando este post como... btn_cancel: Cancelar btn_submit: Enviar remark: empty: No puede estar en blanco. msg: empty: Por favor selecciona una razón. report_modal: flag_title: Estoy marcando para reportar este post de... close_title: Estoy cerrando este post como... review_question_title: Revisar pregunta review_answer_title: Revisar respuesta review_comment_title: Revisar comentario btn_cancel: Cancelar btn_submit: Enviar remark: empty: No puede estar en blanco. msg: empty: Por favor selecciona una razón. not_a_url: El formato de la URL es incorrecto. url_not_match: El origen de la URL no coincide con el sitio web actual. tag_modal: title: Crear nueva etiqueta form: fields: display_name: label: Nombre público msg: empty: El nombre a mostrar no puede estar vacío. range: Nombre a mostrar con un máximo de 35 caracteres. slug_name: label: Ruta de la URL desc: Slug de URL de hasta 35 caracteres. msg: empty: URL no puede estar vacío. range: URL slug hasta 35 caracteres. character: La URL amigable contiene caracteres no permitidos. desc: label: Descripción revision: label: Revisión edit_summary: label: Editar resumen placeholder: >- Explica brevemente los cambios (corrección ortográfica, mejora de formato) btn_cancel: Cancelar btn_submit: Enviar btn_post: Publicar nueva etiqueta tag_info: created_at: Creado edited_at: Editado history: Historial synonyms: title: Sinónimos text: Las siguientes etiquetas serán reasignadas a empty: No se encontraron sinónimos. btn_add: Añadir un sinónimo btn_edit: Editar btn_save: Guardar synonyms_text: Las siguientes etiquetas serán reasignadas a delete: title: Eliminar esta etiqueta tip_with_posts: >-

No permitimos eliminar etiquetas con publicaciones.

Primero elimine esta etiqueta de las publicaciones.

tip_with_synonyms: >-

No permitimos eliminar etiqueta con sinónimos.

Primero elimine los sinónimos de esta etiqueta.

tip: '¿Estás seguro de que deseas borrarlo?' close: Cerrar merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Editar etiqueta default_reason: Editar etiqueta default_first_reason: Añadir etiqueta btn_save_edits: Guardar cambios btn_cancel: Cancelar dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [a las] HH:mm" now: ahora x_seconds_ago: "hace {{count}}s" x_minutes_ago: "hace {{count}}m" x_hours_ago: "hace {{count}}h" hour: hora day: día hours: horas days: días month: month months: months year: year reaction: heart: corazón smile: sonrisa frown: frown btn_label: añadir o eliminar reacciones undo_emoji: deshacer reacción de {{ emoji }} react_emoji: reaccionar con {{ emoji }} unreact_emoji: desreaccionar con {{ emoji }} comment: btn_add_comment: Añadir comentario reply_to: Responder a btn_reply: Responder btn_edit: Editar btn_delete: Eliminar btn_flag: Reportar btn_save_edits: Guardar cambios btn_cancel: Cancelar show_more: "{{count}} comentarios más" tip_question: >- Utiliza los comentarios para pedir más información o sugerir mejoras y modificaciones. Evita responder preguntas en los comentarios. tip_answer: >- Usa comentarios para responder a otros usuarios o notificarles de cambios. Si estás añadiendo nueva información, edita tu publicación en vez de comentar. tip_vote: Añade algo útil a la publicación edit_answer: title: Editar respuesta default_reason: Editar respuesta default_first_reason: Añadir respuesta form: fields: revision: label: Revisión answer: label: Respuesta feedback: characters: El contenido debe tener al menos 6 caracteres. edit_summary: label: Editar resumen placeholder: >- Explique brevemente sus cambios (ortografía corregida, gramática corregida, formato mejorado) btn_save_edits: Guardar cambios btn_cancel: Cancelar tags: title: Etiquetas sort_buttons: popular: Popular name: Nombre newest: Más reciente button_follow: Seguir button_following: Siguiendo tag_label: preguntas search_placeholder: Filtrar por nombre de etiqueta no_desc: La etiqueta no tiene descripción. more: Mas wiki: Wiki ask: title: Create Question edit_title: Editar pregunta default_reason: Editar pregunta default_first_reason: Create question similar_questions: Preguntas similares form: fields: revision: label: Revisión title: label: Título placeholder: What's your topic? Be specific. msg: empty: El título no puede estar vacío. range: Título hasta 150 caracteres body: label: Cuerpo msg: empty: Cuerpo no puede estar vacío. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Etiquetas msg: empty: Se requiere al menos una etiqueta. answer: label: Respuesta msg: empty: La respuesta no puede estar vacía. edit_summary: label: Editar resumen placeholder: >- Explique brevemente sus cambios (ortografía corregida, gramática corregida, formato mejorado) btn_post_question: Publica tu pregunta btn_save_edits: Guardar cambios answer_question: Responde a tu propia pregunta post_question&answer: Publicar una pregunta y su respuesta tag_selector: add_btn: Añadir etiqueta create_btn: Crear nueva etiqueta search_tag: Buscar etiqueta hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Ninguna etiqueta coincide tag_required_text: Etiqueta requerida (al menos una) header: nav: question: Preguntas tag: Etiquetas user: Usuarios badges: Insignias profile: Perfil setting: Ajustes logout: Cerrar sesión admin: Administrador review: Revisar bookmark: Marcadores moderation: Moderación search: placeholder: Buscar footer: build_on: Powered by <1> Apache Answer upload_img: name: Cambiar loading: cargando... pic_auth_code: title: Captcha placeholder: Introduce el texto anterior msg: empty: El Captcha no puede estar vacío. inactive: first: >- ¡Casi estás listo! Te hemos enviado un correo de activación a {{mail}}. Por favor, sigue las instrucciones en el correo para activar tu cuenta. info: "Si no te ha llegado el correo, comprueba la carpeta de SPAM." another: >- Te hemos enviado otro correo de activación a {{mail}}. Puede tardar algunos minutos en llegar; asegúrate de revisar tu carpeta de SPAM. btn_name: Reenviar correo de activación change_btn_name: Cambiar correo msg: empty: No puede estar en blanco. resend_email: url_label: '¿Estás seguro de reenviar el correo de activación?' url_text: También puedes dar el enlace de activación de arriba al usuario. login: login_to_continue: Inicia sesión para continuar info_sign: '¿No tienes cuenta? <1>Regístrate' info_login: '¿Ya tienes una cuenta? <1>Inicia sesión' agreements: Al registrarte, aceptas la <1>política de privacidad y los <3>términos de servicio. forgot_pass: '¿Has olvidado la contraseña?' name: label: Nombre msg: empty: El nombre no puede estar vacío. range: El nombre debe tener entre 2 y 30 caracteres de largo. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Correo electrónico msg: empty: El correo electrónico no puede estar vacío. password: label: Contraseña msg: empty: La contraseña no puede estar vacía. different: Las contraseñas introducidas en ambos lados no coinciden account_forgot: page_title: Olvidaste Tu Contraseña btn_name: Enviadme un correo electrónico de recuperación send_success: >- Si existe una cuenta con el correo {{mail}}, deberías de recibir un email con instrucciones sobre cómo recuperar tu contraseña próximamente. email: label: Correo electrónico msg: empty: El correo electrónico no puede estar vacío. change_email: btn_cancel: Cancelar btn_update: Actualizar dirección de correo send_success: >- Si existe una cuenta con el correo {{mail}}, deberías de recibir un email con instrucciones sobre cómo recuperar tu contraseña próximamente. email: label: Nuevo correo msg: empty: El correo electrónico no puede estar vacío. oauth: connect: Conectar con {{ auth_name }} remove: Eliminar {{ auth_name }} oauth_bind_email: subtitle: Añade un correo de recuperación a tu cuenta. btn_update: Actualizar dirección de correo email: label: Correo msg: empty: El correo no puede estar vacío. modal_title: El correo ya está en uso. modal_content: Este correo electrónico ha sido registrado. ¿Estás seguro de conectarlo a la cuenta existente? modal_cancel: Cambiar correo modal_confirm: Conectarse a la cuenta existente password_reset: page_title: Restablecimiento de Contraseña btn_name: Restablecer mi contraseña reset_success: >- Tu contraseña ha sido actualizada con éxito; vas a ser redirigido a la página de inicio de sesión. link_invalid: >- Lo sentimos, este enlace de restablecimiento de contraseña ya no es válido. ¿Tal vez tu contraseña ya está restablecida? to_login: Continuar a la página de inicio de sesión password: label: Contraseña msg: empty: La contraseña no puede estar vacía. length: La longitud debe ser de entre 8 y 32 caracteres different: Las contraseñas introducidas en ambos lados no coinciden password_confirm: label: Confirmar nueva contraseña settings: page_title: Ajustes goto_modify: Ir a modificar nav: profile: Perfil notification: Notificaciones account: Cuenta interface: Interfaz profile: heading: Perfil btn_name: Guardar display_name: label: Nombre público msg: El nombre a mostrar no puede estar vacío. msg_range: Display name must be 2-30 characters in length. username: label: Nombre de usuario caption: La gente puede mencionarte con "@nombredeusuario". msg: El nombre de usuario no puede estar vacío. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Imagen de perfil gravatar: Gravatar gravatar_text: Puedes cambiar la imagen en custom: Propia custom_text: Puedes subir tu propia imagen. default: Sistema msg: Por favor, sube una imagen bio: label: Sobre mí website: label: Sitio Web placeholder: "https://example.com" msg: Formato del sitio web incorrecto location: label: Ubicación placeholder: "Ciudad, País" notification: heading: Notificaciones por correo turn_on: Activar inbox: label: Notificaciones de bandeja description: Respuestas a tus preguntas, comentarios, invitaciones, y más. all_new_question: label: Todas las preguntas nuevas description: Recibe notificaciones de todas las preguntas nuevas. Hasta 50 preguntas por semana. all_new_question_for_following_tags: label: Todas las preguntas nuevas para las etiquetas siguientes description: Recibe notificaciones de nuevas preguntas para las etiquetas siguientes. account: heading: Cuenta change_email_btn: Cambiar correo electrónico change_pass_btn: Cambiar contraseña change_email_info: >- Te hemos enviado un email a esa dirección. Por favor sigue las instrucciones de confirmación. email: label: Correo new_email: label: Nuevo correo msg: El nuevo correo no puede estar vacío. pass: label: Contraseña actual msg: La contraseña no puede estar vacía. password_title: Contraseña current_pass: label: Contraseña actual msg: empty: La contraseña actual no puede estar vacía. length: El largo necesita estar entre 8 y 32 caracteres. different: Las contraseñas no coinciden. new_pass: label: Nueva contraseña pass_confirm: label: Confirmar nueva contraseña interface: heading: Interfaz lang: label: Idioma de Interfaz text: Idioma de la interfaz de usuario. Cambiará cuando actualices la página. my_logins: title: Mis accesos label: Inicia sesión o regístrate en este sitio usando estas cuentas. modal_title: Eliminar acceso modal_content: '¿Estás seguro de querer eliminar esta sesión de tu cuenta?' modal_confirm_btn: Eliminar remove_success: Eliminado con éxito toast: update: actualización correcta update_password: Contraseña cambiada con éxito. flag_success: Gracias por reportar. forbidden_operate_self: No puedes modificar tu propio usuario review: Tu revisión será visible luego de ser aprobada. sent_success: Enviado con éxito related_question: title: Related answers: respuestas linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Personas Preguntadas desc: Selecciona personas que creas que sepan la respuesta. invite: Invitar a responder add: Añadir personas search: Buscar personas question_detail: action: Acción created: Created Asked: Preguntada asked: preguntada update: Modificada Edited: Edited edit: editada commented: comentado Views: Visto Follow: Seguir Following: Siguiendo follow_tip: Sigue esta pregunta para recibir notificaciones answered: respondida closed_in: Cerrado el show_exist: Mostrar una pregunta existente. useful: Útil question_useful: Es útil y claro question_un_useful: Es poco claro o no es útil question_bookmark: Añadir esta pregunta a marcadores answer_useful: Es útil answer_un_useful: No es útil answers: title: Respuestas score: Puntuación newest: Más reciente oldest: Más antiguo btn_accept: Aceptar btn_accepted: Aceptada write_answer: title: Tu respuesta edit_answer: Editar mi respuesta existente btn_name: Publica tu respuesta add_another_answer: Añadir otra respuesta confirm_title: Continuar a pregunta continue: Continuar confirm_info: >-

¿Seguro que quieres añadir otra respuesta?

Puedes utilizar el enlace de edición para detallar y mejorar tu respuesta existente en su lugar.

empty: La respuesta no puede estar vacía. characters: el contenido debe tener al menos 6 caracteres. tips: header_1: Gracias por tu respuesta li1_1: Asegúrate de responder la pregunta. Proporciona detalles y comparte tu investigación. li1_2: Respalda cualquier declaración que hagas con referencias o experiencia personal. header_2: Pero evita ... li2_1: Pedir ayuda, pedir aclaraciones, o responder a otras respuestas. reopen: confirm_btn: Reabrir title: Reabrir esta publicación content: '¿Seguro que quieres reabrir esta publicación?' list: confirm_btn: Lista title: Listar esta publicación content: '¿Estás seguro de que quieres listar?' unlist: confirm_btn: Deslistar title: No listar esta publicación content: '¿Estás seguro de que quieres dejar de listar?' pin: title: Fijar esta publicación content: '¿Estás seguro de querer fijar esto globalmente? Esta publicación aparecerá por encima de todas las listas de publicaciones.' confirm_btn: Fijar delete: title: Eliminar esta publicación question: >- No recomendamos borrar preguntas con respuestas porque esto priva a los lectores futuros de este conocimiento.

El borrado repetido de preguntas respondidas puede resultar en que tu cuenta se bloquee para hacer preguntas. ¿Estás seguro de que deseas borrarlo? answer_accepted: >-

No recomendamos borrar la respuesta aceptada porque esto priva a los lectores futuros de este conocimiento.

El borrado repetido de respuestas aceptadas puede resultar en que tu cuenta se bloquee para responder. ¿Estás seguro de que deseas borrarlo? other: '¿Estás seguro de que deseas borrarlo?' tip_answer_deleted: Esta respuesta ha sido eliminada undelete_title: Restaurar esta publicación undelete_desc: '¿Estás seguro de querer restaurar?' btns: confirm: Confirmar cancel: Cancelar edit: Editar save: Guardar delete: Eliminar undelete: Restaurar list: Lista unlist: Deslistar unlisted: Sin enumerar login: Acceder signup: Registrarse logout: Cerrar sesión verify: Verificar create: Create approve: Aprobar reject: Rechazar skip: Omitir discard_draft: Descartar borrador pinned: Fijado all: Todo question: Pregunta answer: Respuesta comment: Comentario refresh: Actualizar resend: Reenviar deactivate: Desactivar active: Activar suspend: Suspender unsuspend: Quitar suspensión close: Cerrar reopen: Reabrir ok: Aceptar light: Claro dark: Oscuro system_setting: Ajuste de sistema default: Por defecto reset: Reiniciar tag: Etiqueta post_lowercase: publicación filter: Filtro ignore: Ignorar submit: Enviar normal: Normal closed: Cerrado deleted: Eliminado deleted_permanently: Deleted permanently pending: Pendiente more: Más view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Resultados de la búsqueda keywords: Palabras claves options: Opciones follow: Seguir following: Siguiendo counts: "{{count}} Resultados" counts_loading: "... Results" more: Más sort_btns: relevance: Relevancia newest: Más reciente active: Activas score: Puntuación more: Mas tips: title: Consejos de búsqueda avanzada tag: "<1>[tag] búsqueda por etiquetas" user: "<1>user:username búsqueda por autor" answer: "<1>answers:0 preguntas sin responder" score: "<1>score:3 Publicaciones con un puntaje de 3 o más" question: "<1>is:question buscar preguntas" is_answer: "<1>is:answer buscar respuestas" empty: No pudimos encontrar nada.
Prueba a buscar con palabras diferentes o menos específicas. share: name: Compartir copy: Copiar enlace via: Compartir vía... copied: Copiado facebook: Compartir en Facebook twitter: Share to X cannot_vote_for_self: No puedes votar tu propia publicación. modal_confirm: title: Error... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Tu nueva cuenta ha sido confirmada, serás redirigido a la página de inicio. link: Continuar a la página de inicio oops: '¡Ups!' invalid: El enlace que utilizaste ya no funciona. confirm_new_email: Tu email ha sido actualizado. confirm_new_email_invalid: >- Lo siento, este enlace de confirmación ya no es válido. ¿Quizás ya se haya cambiado tu correo electrónico? unsubscribe: page_title: Desuscribir success_title: Desuscrito con éxito success_desc: Ha sido eliminado con éxito de esta lista de suscriptores y no recibirá más correos electrónicos nuestros. link: Cambiar ajustes question: following_tags: Etiquetas seguidas edit: Editar save: Guardar follow_tag_tip: Sigue etiquetas para personalizar tu lista de preguntas. hot_questions: Preguntas del momento all_questions: Todas las preguntas x_questions: "{{ count }} Preguntas" x_answers: "{{ count }} respuestas" x_posts: "{{ count }} Posts" questions: Preguntas answers: Respuestas newest: Más reciente active: Activo hot: Popular frequent: Frecuente recommend: Recomendar score: Puntuación unanswered: Sin respuesta modified: modificada answered: respondida asked: preguntada closed: cerrada follow_a_tag: Seguir una etiqueta more: Más personal: overview: Información general answers: Respuestas answer: respuesta questions: Preguntas question: pregunta bookmarks: Guardadas reputation: Reputación comments: Comentarios votes: Votos badges: Insignias newest: Más reciente score: Puntuación edit_profile: Editar perfil visited_x_days: "Visitado {{ count }} días" viewed: Visto joined: Unido comma: "," last_login: Visto about_me: Sobre mí about_me_empty: "// ¡Hola Mundo!" top_answers: Mejores respuestas top_questions: Preguntas Principales stats: Estadísticas list_empty: No se encontraron publicaciones.
¿Quizás le gustaría seleccionar una pestaña diferente? content_empty: No se han encontrado publicaciones. accepted: Aceptada answered: respondida asked: preguntó downvoted: votado negativamente mod_short: MOD mod_long: Moderadores x_reputation: reputación x_votes: votos recibidos x_answers: respuestas x_questions: preguntas recent_badges: Insignias recientes install: title: Instalación next: Próximo done: Hecho config_yaml_error: No se puede crear el archivo config.yaml. lang: label: Elige un idioma db_type: label: Motor de base de datos db_username: label: Nombre de usuario placeholder: raíz msg: El nombre de usuario no puede estar vacío. db_password: label: Contraseña placeholder: raíz msg: La contraseña no puede estar vacía. db_host: label: Host de base de datos placeholder: "db:3306" msg: El host de base de datos no puede estar vacío. db_name: label: Nombre de base de datos placeholder: respuesta msg: El nombre de la base de datos no puede estar vacío. db_file: label: Archivo de base de datos placeholder: /data/respuesta.db msg: El archivo de la base de datos no puede estar vacío. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Crear config.yaml label: El archivo config.yaml creado. desc: >- Puede crear el archivo <1>config.yaml manualmente en el directorio <1>/var/www/xxx/ y pegar el siguiente texto en él. info: Después de haber hecho eso, haga clic en el botón "Siguiente". site_information: Información del sitio admin_account: Cuenta de administrador site_name: label: Nombre del sitio msg: El nombre del sitio no puede estar vacío. msg_max_length: El nombre del sitio tener como máximo 30 caracteres. site_url: label: Sitio URL text: La dirección de su sitio. msg: empty: La URL del sitio no puede estar vacía. incorrect: Formato incorrecto de la URL del sitio. max_length: El URL del sitio debe tener como máximo 512 caracteres. contact_email: label: Correo electrónico de contacto text: Dirección de correo electrónico del contacto clave responsable de este sitio. msg: empty: El correo electrónico de contacto no puede estar vacío. incorrect: Formato incorrecto de correo electrónico de contacto. login_required: label: Privado switch: Inicio de sesión requerido text: Solo usuarios conectados pueden acceder a esta comunidad. admin_name: label: Nombre msg: El nombre no puede estar vacío. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Contraseña text: >- Necesitará esta contraseña para iniciar sesión. Guárdela en un lugar seguro. msg: La contraseña no puede estar vacía. msg_min_length: La contraseña debe contener 8 caracteres como mínimo. msg_max_length: La contraseña debe contener como máximo 32 caracteres. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Correo electrónico text: Necesitará este correo electrónico para iniciar sesión. msg: empty: El correo electrónico no puede estar vacío. incorrect: Correo electrónico con formato incorrecto. ready_title: Tu sitio está listo ready_desc: >- Si alguna vez desea cambiar más configuraciones, visite la <1>sección de administración; encuéntrelo en el menú del sitio. good_luck: "¡Diviértete y buena suerte!" warn_title: Advertencia warn_desc: >- El archivo <1>config.yaml ya existe. Si necesita restablecer alguno de los elementos de configuración de este archivo, elimínelo primero. install_now: Puede intentar <1>instalar ahora. installed: Ya instalado installed_desc: >- Parece que ya lo has instalado. Para reinstalar, borre primero las tablas de la base de datos anterior. db_failed: La conexión a la base de datos falló db_failed_desc: >- Esto significa que la información de la base de datos en tu archivo <1>config.yaml es incorrecta o que no pudo establecerse contacto con el servidor de la base de datos. Esto podría significar que el host está caído. counts: views: puntos de vista votes: votos answers: respuestas accepted: Aceptado page_error: http_error: Error HTTP {{ code }} desc_403: No tienes permiso para acceder a esta página. desc_404: Desafortunadamente, esta página no existe. desc_50X: Se produjo un error en el servidor y no pudo completarse tu solicitud. back_home: Volver a la página de inicio page_maintenance: desc: "Estamos en mantenimiento, pronto estaremos de vuelta." nav_menus: dashboard: Panel contents: Contenido questions: Preguntas answers: Respuestas users: Usuarios badges: Insignias flags: Banderas settings: Ajustes general: General interface: Interfaz smtp: SMTP branding: Marca legal: Legal write: Escribir terms: Terms tos: Términos de servicio privacy: Privacidad seo: ESTE customize: Personalizar themes: Temas login: Iniciar sesión privileges: Privilegios plugins: Extensiones installed_plugins: Extensiones Instaladas apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Bienvenido a {{site_name}} user_center: login: Iniciar sesión qrcode_login_tip: Por favor utiliza {{ agentName }} para escanear el código QR e iniciar sesión. login_failed_email_tip: Error al iniciar sesión, por favor permite el acceso a tu información de correo de esta aplicación antes de intentar nuevamente. badges: modal: title: Enhorabuena content: Has ganado una nueva insignia. close: Cerrar confirm: Ver insignia title: Insignias awarded: Premiado earned_×: Obtenidos ×{{ number }} ×_awarded: "{{ number }} adjudicado" can_earn_multiple: Puedes ganar esto varias veces. earned: Ganado admin: admin_header: title: Administrador dashboard: title: Panel welcome: '¡Bienvenido a Admin!' site_statistics: Estadísticas del sitio questions: "Preguntas:" resolved: "Resuelto:" unanswered: "Sin respuesta:" answers: "Respuestas:" comments: "Comentarios:" votes: "Votos:" users: "Usuarios:" flags: "Banderas:" reviews: "Revisar:" site_health: Salud del sitio version: "Versión:" https: "HTTPS:" upload_folder: "Cargar carpeta:" run_mode: "Modo de ejecución:" private: Privado public: Público smtp: "SMTP:" timezone: "Zona horaria:" system_info: Información del sistema go_version: "Versión de Go:" database: "Base de datos:" database_size: "Tamaño de la base de datos:" storage_used: "Almacenamiento utilizado:" uptime: "Tiempo ejecutándose:" links: Enlaces plugins: Extensiones github: GitHub blog: Blog contact: Contacto forum: Foro documents: Documentos feedback: Comentario support: Soporte review: Revisar config: Configuración update_to: Actualizar para latest: Lo más nuevo check_failed: Comprobación fallida "yes": "Si" "no": "No" not_allowed: No permitido allowed: Permitido enabled: Activado disabled: Desactivado writable: Redactable not_writable: No redactable flags: title: Banderas pending: Pendiente completed: Terminado flagged: Marcado flagged_type: Reportado {{ type }} created: Creado action: Acción review: Revisar user_role_modal: title: Cambiar rol de usuario a... btn_cancel: Cancelar btn_submit: Entregar new_password_modal: title: Establecer nueva contraseña form: fields: password: label: Contraseña text: El usuario será desconectado y deberá iniciar sesión nuevamente. msg: La contraseña debe contener entre 8 y 32 caracteres de longitud. btn_cancel: Cancelar btn_submit: Enviar edit_profile_modal: title: Editar perfil form: fields: display_name: label: Nombre para mostrar msg_range: Display name must be 2-30 characters in length. username: label: Nombre de usuario msg_range: Username must be 2-30 characters in length. email: label: Correo electrónico msg_invalid: Dirección de correo inválida. edit_success: Editado exitosamente btn_cancel: Cancelar btn_submit: Enviar user_modal: title: Añadir nuevo usuario form: fields: users: label: Añadir usuarios en cantidad placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separe “nombre, correo electrónico, contraseña” con comas. Un usuario por línea. msg: "Por favor, introduzca el correo electrónico del usuario, uno por línea." display_name: label: Nombre público msg: El nombre de la pantalla debe tener entre 2 y 30 caracteres de longitud. email: label: Correo msg: El correo no es válido. password: label: Contraseña msg: La contraseña debe contener entre 8 y 32 caracteres de longitud. btn_cancel: Cancelar btn_submit: Enviar users: title: Usuarios name: Nombre email: Correo electrónico reputation: Reputación created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Estado role: Rol action: Acción change: Cambiar all: Todo staff: Personal more: Más inactive: Inactivo suspended: Suspendido deleted: Eliminado normal: Normal Moderator: Moderador Admin: Administrador User: Usuario filter: placeholder: "Filtrar por nombre, usuario:id" set_new_password: Establecer nueva contraseña edit_profile: Editar perfil change_status: Cambiar Estado change_role: Cambiar rol show_logs: Mostrar registros add_user: Agregar usuario deactivate_user: title: Desactivar usuario content: Un usuario inactivo debe revalidar su correo electrónico. delete_user: title: Eliminar este usuario content: '¿Estás seguro de que deseas eliminar este usuario? ¡Esto es permanente!' remove: Eliminar su contenido label: Eliminar todas las preguntas, respuestas, comentarios, etc. text: No marque esto si solo desea eliminar la cuenta del usuario. suspend_user: title: Suspender a este usuario content: Un usuario suspendido no puede iniciar sesión. label: How long will the user be suspended for? forever: Forever questions: page_title: Preguntas unlisted: No listado post: Correo votes: Votos answers: Respuestas created: Creado status: Estado action: Acción change: Cambiar pending: Pendiente filter: placeholder: "Filtrar por título, pregunta:id" answers: page_title: Respuestas post: Correo votes: Votos created: Creado status: Estado action: Acción change: Cambiar filter: placeholder: "Filtrar por título, respuesta: id" general: page_title: General name: label: Nombre del sitio msg: El nombre del sitio no puede estar vacío. text: "El nombre de este sitio, tal como se usa en la etiqueta del título." site_url: label: Sitio URL msg: La url del sitio no puede estar vacía. validate: Por favor introduzca un URL válido. text: La dirección de su sitio. short_desc: label: Descripción breve del sitio msg: La descripción breve del sitio no puede estar vacía. text: "Breve descripción, tal como se usa en la etiqueta del título en la página de inicio." desc: label: Descripción del sitio msg: La descripción del sitio no puede estar vacía. text: "Describa este sitio en una oración, como se usa en la etiqueta de meta descripción." contact_email: label: Correo electrónico de contacto msg: El correo electrónico de contacto no puede estar vacío. validate: El correo electrónico de contacto no es válido. text: Dirección de correo electrónico del contacto clave responsable de este sitio. check_update: label: Actualizaciones de software text: Comprobar actualizaciones automáticamente interface: page_title: Interfaz language: label: Idioma de Interfaz msg: El idioma de la interfaz no puede estar vacío. text: Idioma de la interfaz de usuario. Cambiará cuando actualice la página. time_zone: label: Zona horaria msg: El huso horario no puede estar vacío. text: Elija una ciudad en la misma zona horaria que usted. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: Desde correo msg: Desde el correo electrónico no puede estar vacío. text: La dirección de correo electrónico desde la que se envían los correos electrónicos. from_name: label: Desde nombre msg: Desde el nombre no puede estar vacío. text: El nombre desde el que se envían los correos electrónicos. smtp_host: label: Host SMTP msg: El host SMTP no puede estar vacío. text: Su servidor de correo. encryption: label: Cifrado msg: El cifrado no puede estar vacío. text: Para la mayoría de los servidores, SSL es la opción recomendada. ssl: SSL tls: TLS none: Ninguno smtp_port: label: Puerto SMTP msg: El puerto SMTP debe ser el número 1 ~ 65535. text: El puerto a su servidor de correo. smtp_username: label: Nombre de usuario SMTP msg: El nombre de usuario SMTP no puede estar vacío. smtp_password: label: Contraseña de SMTP msg: La contraseña SMTP no puede estar vacía. test_email_recipient: label: Destinatarios de correo electrónico de prueba text: Proporcione la dirección de correo electrónico que recibirá los envíos de prueba. msg: Los destinatarios de correo electrónico de prueba no son válidos smtp_authentication: label: Habilitar autenticación title: Autenticación SMTP msg: La autenticación SMTP no puede estar vacía. "yes": "Si" "no": "No" branding: page_title: Marca logo: label: Logo msg: El logotipo no puede estar vacío. text: La imagen del logotipo en la parte superior izquierda de su sitio. Utilice una imagen rectangular ancha con una altura de 56 y una relación de aspecto superior a 3:1. Si se deja en blanco, se mostrará el texto del título del sitio. mobile_logo: label: Logo Móvil text: El logotipo utilizado en la versión móvil de su sitio. Utilice una imagen rectangular ancha con una altura de 56. Si se deja en blanco, se utilizará la imagen de la configuración de "logotipo". square_icon: label: Icono cuadrado msg: El icono cuadrado no puede estar vacío. text: Imagen utilizada como base para los iconos de metadatos. Idealmente, debería ser más grande que 512x512. favicon: label: Icono de favoritos text: Un favicon para su sitio. Para que funcione correctamente sobre un CDN, debe ser un png. Se cambiará el tamaño a 32x32. Si se deja en blanco, se utilizará el "icono cuadrado". legal: page_title: Legal terms_of_service: label: Términos de servicio text: "Puede agregar términos de contenido de servicio aquí. Si ya tiene un documento alojado en otro lugar, proporcione la URL completa aquí." privacy_policy: label: Política de privacidad text: "Puede agregar contenido de política de privacidad aquí. Si ya tiene un documento alojado en otro lugar, proporcione la URL completa aquí." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Escribir respuesta label: Cada usuario solo puede escribir una respuesta por pregunta text: "Desactivar para permitir a los usuarios escribir múltiples respuestas a la misma pregunta, lo que puede causar que las respuestas no estén enfocadas." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Etiquetas recomendadas text: "Las etiquetas recomendadas se mostrarán en la lista desplegable por defecto." msg: contain_reserved: "las etiquetas recomendadas no pueden contener etiquetas reservadas" required_tag: title: Establecer etiquetas necesarias label: Establecer "Etiquetas recomendadas" como etiquetas requeridas text: "Cada nueva pregunta debe tener al menos una etiqueta de recomendación." reserved_tags: label: Etiquetas reservadas text: "Las etiquetas reservadas sólo pueden ser usadas por el moderador." image_size: label: Tamaño máximo de la imagen (MB) text: "Tamaño máximo de la imagen." attachment_size: label: Tamaño máximo del archivo adjunto (MB) text: "El tamaño máximo de subida de archivos adjuntos." image_megapixels: label: Megapixels de imagen máx text: "Número máximo de megapixels permitidos para una imagen." image_extensions: label: Extensiones de adjuntos autorizadas text: "Una lista de extensiones de archivo permitidas para la visualización de imágenes, separadas con comas." attachment_extensions: label: Extensiones de adjuntos autorizadas text: "Una lista de extensiones de archivo permitidas para subir, separadas con comas. ADVERTENCIA: Permitir subidas puede causar problemas de seguridad." seo: page_title: SEO permalink: label: Enlace permanente text: Las estructuras de URL personalizadas pueden mejorar la facilidad de uso y la compatibilidad futura de sus enlaces. robots: label: robots.txt text: Esto anulará permanentemente cualquier configuración del sitio relacionada. themes: page_title: Temas themes: label: Temas text: Seleccione un tema existente. color_scheme: label: Esquema de color navbar_style: label: Navbar background style primary_color: label: Color primario text: Modifica los colores usados por tus temas layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS y HTML custom_css: label: CSS personalizado text: > head: label: Cabeza text: > header: label: Encabezado text: > footer: label: Pie de página text: Esto se insertará antes . sidebar: label: Barra lateral text: Esto se añadirá en la barra lateral. login: page_title: Iniciar sesión membership: title: Membresía label: Permitir registro de nuevas ceuntas text: Desactiva esto para evitar que cualquier persona pueda crear una cuenta. email_registration: title: Registro de correo electrónico label: Permitir registro de correo electrónico text: Desactivar para evitar registros a través de correo electrónico. allowed_email_domains: title: Dominios de correo electrónico permitidos text: Dominios de correo electrónico con los que los usuarios deben registrar sus cuentas. Un dominio por línea. Ignorado cuando esté vacío. private: title: Privado label: Inicio de sesión requerido text: Sólo usuarios con sesión iniciada pueden acceder a esta comunidad. password_login: title: Inicio de sesión con contraseña label: Permitir inicio de sesión con correo y contraseña text: "ADVERTENCIA: Si se desactiva, es posible que no pueda iniciar sesión si no ha configurado previamente otro método de inicio de sesión." installed_plugins: title: Extensiones Instaladas plugin_link: Los plugins extienden y expanden la funcionalidad. Puede encontrar plugins en el <1>Repositorio de plugin. filter: all: Todos active: Activo inactive: Inactivo outdated: Desactualizado plugins: label: Extensiones text: Seleccione una extensión existente. name: Nombre version: Versión status: Estado action: Acción deactivate: Desactivar activate: Activar settings: Ajustes settings_users: title: Usuarios avatar: label: Avatar predeterminado text: Para usuarios sin un avatar personalizado propio. gravatar_base_url: label: Gravatar Base URL text: URL de la base API del proveedor Gravatar. Ignorado cuando esté vacío. profile_editable: title: Perfil editable allow_update_display_name: label: Permitir a usuarios cambiar su nombre público allow_update_username: label: Permitir a los usuarios cambiar su nombre de usuario allow_update_avatar: label: Permitir a los usuarios cambiar su foto de perfil allow_update_bio: label: Permitir a los usuarios cambiar su descripción allow_update_website: label: Permitir a los usuarios cambiar su sitio web allow_update_location: label: Permitir a los usuarios cambiar su ubicación privilege: title: Privilegios level: label: Nivel de reputación requerido text: Elegir reputación requerida para los privilegios msg: should_be_number: la entrada debe ser número number_larger_1: número debe ser igual o mayor que 1 badges: action: Accin active: Activo activate: Activación all: All awards: Premios deactivate: Desactivar filter: placeholder: Filtrar por nombre, insignia:id group: Grupo inactive: Inactivo name: Nombre show_logs: Mostrar logs status: Status title: Insignias apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (opcional) empty: no puede estar en blanco invalid: no es válido btn_submit: Guardar not_found_props: "La propiedad requerida {{ key }} no se ha encontrado." select: Seleccionar page_review: review: Revisar proposed: propuesto question_edit: Edición de preguntas answer_edit: Edición de respuestas tag_edit: Edición de etiquetas edit_summary: Editar resumen edit_question: Editar pregunta edit_answer: Editar respuesta edit_tag: Editar etiqueta empty: No quedan tareas de revisión. approve_revision_tip: '¿Aprueban ustedes esta revisión?' approve_flag_tip: '¿Aprueban ustedes esta bandera?' approve_post_tip: '¿Aprueban ustedes esta bandera?' approve_user_tip: '¿Apruebas a este usuario?' suggest_edits: Ediciones Sugeridas flag_post: Marcar publicación flag_user: Marcar usuario queued_post: Publicación en cola queued_user: Usuario en cola filter_label: Tipo reputation: reputación flag_post_type: Marcada esta publicación como {{ type }}. flag_user_type: Marcado este usuario como {{ type }}. edit_post: Editar publicación list_post: Listar publicación unlist_post: Deslistar publicación timeline: undeleted: recuperado deleted: eliminado downvote: voto negativo upvote: votar a favor accept: aceptar cancelled: cancelado commented: comentado rollback: retroceder edited: editada answered: contestada asked: preguntó closed: cerrado reopened: reabierto created: creado pin: fijado unpin: desfijado show: listado hide: deslistado title: "Historial para" tag_title: "Línea temporal para" show_votes: "Mostrar votos" n_or_a: N/A title_for_question: "Línea de tiempo para" title_for_answer: "Cronología de la respuesta a {{ title }} por {{ author }}" title_for_tag: "Cronología de la etiqueta" datetime: Fecha y hora type: Tipo by: Por comment: Comentario no_data: "No pudimos encontrar nada." users: title: Usuarios users_with_the_most_reputation: Usuarios con el mayor puntaje de reputación esta semana users_with_the_most_vote: Usuarios que más votaron esta semana staffs: Nuestor equipo de la comunidad reputation: reputación votes: votos prompt: leave_page: '¿Seguro que quieres salir de la página?' changes_not_save: Es posible que sus cambios no se guarden. draft: discard_confirm: '¿Está seguro de que desea descartar este borrador?' messages: post_deleted: Esta publicación ha sido eliminada. post_cancel_deleted: Este publicación ha sido restaurada. post_pin: Esta publicación ha sido fijada. post_unpin: Esta publicación ha sido desfijada. post_hide_list: Esta publicación ha sido ocultada de la lista. post_show_list: Esta publicación ha sido mostrada a la lista. post_reopen: Esta publicación ha sido reabierta. post_list: Esta publicación ha sido listada. post_unlist: Esta publicación ha sido retirado de la lista.. post_pending: Su publicación está pendiente de revisión. Esto es una vista previa, será visible después de que haya sido aprobado. post_closed: Esta publicación ha sido cerrada. answer_deleted: Esta respuesta ha sido eliminada. answer_cancel_deleted: Esta respuesta ha sido restaurada. change_user_role: El rol de este usuario ha sido cambiado. user_inactive: Este usuario ya esta inactivo. user_normal: Este usuario ya es normal. user_suspended: Este usuario ha sido suspendido. user_deleted: Este usuario ha sido eliminado. user_added: User has been added successfully. badge_activated: Esta insignia ha sido activada. badge_inactivated: Esta insignia ha sido desactivada. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/fa_IR.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: موفق. unknown: other: خطای ناشناخته. request_format_error: other: ساختار درخواست شناخته شده نیست. unauthorized_error: other: دسترسی غیر مجاز. database_error: other: خطای سرور داده. forbidden_error: other: عدم اجازه دسترسی. duplicate_request_error: other: ارسال تکراری. action: report: other: نشان edit: other: ویرایش delete: other: حذف close: other: بستن reopen: other: بازگشایی forbidden_error: other: عدم اجازه دسترسی. pin: other: سنجاق کردن hide: other: پنهان کردن unpin: other: برداشتن سنجاق show: other: فهرست invite_someone_to_answer: other: ویرایش undelete: other: بازگردانی حذف merge: other: Merge role: name: user: other: کاربر admin: other: ادمین moderator: other: مدير description: user: other: پیش فرض بدون دسترسی خاص. admin: other: تمامی دسترسی ها را داراست. moderator: other: دسترسی به تمامی پست هارا داراست بجز تنظیمات ادمین. privilege: level_1: description: other: سطح ۱ (شهرت کمی نیاز هست برای تیم/گروه های خصوصی) level_2: description: other: سطح ۲ (شهرت کمی نیاز هست برای انجمن های استارتاپی) level_3: description: other: سطح ۳ (شهرت بالایی برای نیاز هست برای انجمن های تکمیل) level_custom: description: other: سطح دلخواه rank_question_add_label: other: سوال بپرس rank_answer_add_label: other: جواب بده rank_comment_add_label: other: نظر بده rank_report_add_label: other: نشان rank_comment_vote_up_label: other: رای موافق rank_link_url_limit_label: other: بیشتر از دو لینک را هم زمان پست کنید rank_question_vote_up_label: other: رای موافق rank_answer_vote_up_label: other: رای موافق rank_question_vote_down_label: other: رای مخالف rank_answer_vote_down_label: other: رای مخالف rank_invite_someone_to_answer_label: other: فردی رو دعوت کنین تا جواب بدن rank_tag_add_label: other: ساخت تگ جدید rank_tag_edit_label: other: ویرایش توضیحات تگ (نیازمند بازبینی) rank_question_edit_label: other: ویرایش سوال دیگران (نیازمند بازبینی) rank_answer_edit_label: other: ویرایش جواب دیگران (نیازمند بازبینی) rank_question_edit_without_review_label: other: ویرایش سوال دیگران بدون نیاز به بازبینی rank_answer_edit_without_review_label: other: ویرایش جواب دیگران بدون نیاز به بازبینی rank_question_audit_label: other: بازبینی ویرایش های سوال rank_answer_audit_label: other: بازبینی ویرایش های جواب rank_tag_audit_label: other: بازبینی ویرایش های تگ rank_tag_edit_without_review_label: other: ویرایش توضیحات تگ بدون بازبینی rank_tag_synonym_label: other: مدیریت تگ های مترادف email: other: ایمیل e_mail: other: ایمیل password: other: رمز pass: other: رمز old_pass: other: Current password original_text: other: پست جاری email_or_password_wrong_error: other: ایمیل و رمز وارد شده صحیح نیست. error: common: invalid_url: other: Invalid URL. status_invalid: other: Invalid status. password: space_invalid: other: رمز عبور نمی تواند شامل فضای خالی باشد. admin: cannot_update_their_password: other: نمیتوانید رمز عبور خود را تغییر دهید. cannot_edit_their_profile: other: You cannot modify your profile. cannot_modify_self_status: other: نمیتوانید وضعیت خود را تغییر دهید. email_or_password_wrong: other: ایمیل و رمز وارد شده صحیح نیست. answer: not_found: other: جواب پیدا نشد. cannot_deleted: other: اجازه حذف ندارید. cannot_update: other: اجازه بروزرسانی ندارید. question_closed_cannot_add: other: سوالات بسته شده اند و نمیتوان سوالی اضافه کرد. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: نظرات قابل ویرایش نیستند. not_found: other: نظر پیدا نشد. cannot_edit_after_deadline: other: زمان زیادی برای ویرایش نظر گذشته است. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: ایمیل تکراری. need_to_be_verified: other: ایمیل باید تایید شود. verify_url_expired: other: لینک تایید ایمیل منقضی شده است،‌لطفا دوباره تلاش کنید. illegal_email_domain_error: other: دامنه ایمیل پیشتیبانی نمی شود، لطفا از ایمیل دیگری استفاده کنید. lang: not_found: other: فایل زبان یافت نشد. object: captcha_verification_failed: other: اشتباه در Captcha. disallow_follow: other: شما اجازه فالو کردن ندارید. disallow_vote: other: شما اجازه رای دادن ندارید. disallow_vote_your_self: other: شما نمی توانید به پست خودتان رای دهید. not_found: other: آبجکت مورد نظر پیدا نشد. verification_failed: other: تایید با خطا مواجه شد. email_or_password_incorrect: other: ایمیل و رمز وارد شده صحیح نیست. old_password_verification_failed: other: پسورد قدیمی تایید نشد new_password_same_as_previous_setting: other: پسورد جدید با پسورد قدیمی یکسان است. already_deleted: other: This post has been deleted. meta: object_not_found: other: Meta object not found question: already_deleted: other: این پست حذف شده است. under_review: other: Your post is awaiting review. It will be visible after it has been approved. not_found: other: سوال پیدا نشد. cannot_deleted: other: اجازه حذف ندارید. cannot_close: other: اجاره بستن ندارید. cannot_update: other: اجازه بروزرسانی ندارید. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: شهرت ناکافی. vote_fail_to_meet_the_condition: other: ممنون بابت بازخورد. شما حداقل به {{.Rank}} نیاز دارید برای رای دادن. no_enough_rank_to_operate: other: شما حداقل به {{.Rank}} نیاز دارید برای انجام این کار. report: handle_failed: other: گزارش دهی با مشکل مواجه شد. not_found: other: گزارش مورد نظر پیدا نشد. tag: already_exist: other: تگ از قبل موجود است. not_found: other: تگ پیدا نشد. recommend_tag_not_found: other: تگ پیشنهاد شده موجود نیست. recommend_tag_enter: other: لطفا حداقل یک تگ را وارد کنید. not_contain_synonym_tags: other: نباید تگ مترادف داشته باشد. cannot_update: other: اجازه بروزرسانی ندارید. is_used_cannot_delete: other: نمی توانید تگی که در حال استفاده است را حذف کنید. cannot_set_synonym_as_itself: other: شما نمی توانید مترادفی برای برچسب فعلی به عوان خودش تنظیم کنین. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: '"از طرفه" نمی تواند آدرس ایمیل باشد.' theme: not_found: other: تم پیدا نشد. revision: review_underway: other: فعلا امکان ویرایش وجود ندارد،‌این نسخه در صف بازینی قرار دارد. no_permission: other: اجاره بازبینی و اصلاح ندارید. user: external_login_missing_user_id: other: پلتفورم های سوم شخص نمی توانند نام کاربر خاصی را ارائه دهند، بنابر این شما نمی توانید وارد شوید، لطفا با مدیریت وبسایت تماس بگیرید. external_login_unbinding_forbidden: other: لطفاً یک رمز ورود برای حساب خود قبل از حذف تنظیم کنید. email_or_password_wrong: other: other: ایمیل و رمز وارد شده صحیح نیست. not_found: other: کاربر پیدا نشد. suspended: other: کاربر در حالت تعلیق قرار داده شده است. username_invalid: other: نام کاربری نامعتبر است. username_duplicate: other: این نام کاربری قبلا استفاده شده است. set_avatar: other: ست کردن آواتار با مشکل مواجه شد. cannot_update_your_role: other: شما نمی توانید وظیفه خود را تغییر دهید. not_allowed_registration: other: درحال حاضر سایت برای ثبت نام باز نیست. not_allowed_login_via_password: other: در حال حاضر سایت اجازه ورود از طریق رمز عبور ندارد. access_denied: other: دسترسی مجاز نیست page_access_denied: other: شما وجوز دسترسی به این صفحه را ندارید. add_bulk_users_format_error: other: "مشکل پیش آمده در فرمت {{.Field}} در کنار {{.Content}} در خط {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "تعداد کاربرانی که اضافه می کنید باید رنج بین ۱-{{.MaxAmount}} باشند." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: خواندن کافیگ با مشکل مواجه شد database: connection_failed: other: اتصال به دیتابیس موفقیت آمیز نبود create_table_failed: other: ایجاد کردن جدول موفقیت آمیز نبود install: create_config_failed: other: فایل config.yaml نمی تواند ایجاد شود. upload: unsupported_file_format: other: فرمت فایل پشتیبانی نمی شود. site_info: config_not_found: other: پیکربندی سایت پیدا نشد. badge: object_not_found: other: Badge object not found reason: spam: name: other: هرزنامه desc: other: این پست یک تبلیغ یا خرابکاری است. این پس مفید یا مربوط به این موضوع نمی باشد. rude_or_abusive: name: other: بی ادب یا توهین آمیز desc: other: "A reasonable person would find this content inappropriate for respectful discourse." a_duplicate: name: other: تکراری desc: other: این سوال قبلا پرسیده و جواب داده شده است. placeholder: other: لینک سوال مورد نظر را وارد کنید not_a_answer: name: other: این یک پاسخ نیست desc: other: "." no_longer_needed: name: other: دیگر نیازی نیست desc: other: این نظر منسوخ شده، مکلامه ای یا مربوط به این پس نیست. something: name: other: یک مورد دیگر desc: other: این پست به دلیل دیگری که در بالا ذکر نشده نیاز به توجه کارکنان دارد. placeholder: other: به طور خاص به ما اطلاع دهید که در مورد چه چیزی نگران هستید community_specific: name: other: یک دلیل خاص جامعه desc: other: این سوال با دستورالعمل جامعه مطابقت ندارد. not_clarity: name: other: نیاز به جزئیات یا واضح کردن دارد desc: other: این سوال درحال حاضر شامل چندتا سوال در یکی هست. باید فقط روی یک مشکل تمرکز کند. looks_ok: name: other: به نظر خوب میاد desc: other: این پست همانطور که هست خوب است و کیفیت پایینی ندارد. needs_edit: name: other: نیاز به ویرایش بود، من انجام دادم desc: other: مشکلات این پست را خودتان بهبود و اصلاح کنید. needs_close: name: other: نیاز است که بسته بشود desc: other: به یک سوال بسته شده نمیتوان جوابی ثبت کرد بلکه می توان ویرایش، رای و نظر داد. needs_delete: name: other: نیاز است که حذف بشود desc: other: این پست حذف خواهد شد. question: close: duplicate: name: other: هرزنامه desc: other: این سوال قبلا پرسیده و جواب داده شده است. guideline: name: other: یک دلیل خاص جامعه desc: other: این سوال با دستورالعمل جامعه مطابقت ندارد. multiple: name: other: نیاز به جزئیات یا واضح کردن دارد desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: یک مورد دیگر desc: other: این پست به دلیل دیگری نیاز دارد که در بالا ذکر نشده است. operation_type: asked: other: پرسیده شده answered: other: جواب داده modified: other: تغییر یافته deleted_title: other: سوال حذف شده questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: سوال بارگزاری شده answer_the_question: other: سؤال جواب داده شده update_answer: other: جواب بارگذاری شده accept_answer: other: جواب پذیرفته شده comment_question: other: سوال از کامنت comment_answer: other: جواب از کامنت reply_to_you: other: به شما پاسخ داد mention_you: other: به شما اشاره کرده your_question_is_closed: other: سوال شما بسته شده است your_question_was_deleted: other: سوال شما حذف شده است your_answer_was_deleted: other: جواب شما حذف شده است your_comment_was_deleted: other: نظر شما پاک شده است up_voted_question: other: رای موافق down_voted_question: other: سوال با رای منفی up_voted_answer: other: پاسخ موافق down_voted_answer: other: جواب مخالف up_voted_comment: other: نظر بدون رای invited_you_to_answer: other: برای جواب دادن دعوت شده اید earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "آدرس ایمیل جدید خود را تایید کنید{{.SiteName}}" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} به سؤال شما پاسخ داد" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} شما را به پاسخ دعوت کرد" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} روی پست شما نظر داد" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] سؤال جدید: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] گذرواژه بازنشانی شد" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] حساب کاربری جدید خود را تأیید کنید" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] ایمیل آزمایشی" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: رأی مثبت upvoted: other: رأی مثبت downvote: other: رأی منفی downvoted: other: رأی منفی accept: other: پذیرفتن accepted: other: پذیرفته شده edit: other: edit review: queued_post: other: Queued post flagged_post: other: Flagged post suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: Editor desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: نحوه فرمت کردن desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: قبلی next: بعدی page_title: question: سوال questions: سوالات tag: برچسب tags: برچسب ها tag_wiki: ویکی تگ create_tag: ایجاد برچسب edit_tag: ویرایش برچسب ask_a_question: Create Question edit_question: ویرایش سوال edit_answer: ویرایش پاسخ search: جستجو posts_containing: پست های شامل settings: تنظیمات notifications: اعلانات login: ورود sign_up: ثبت نام account_recovery: بازیابی حساب کاربری account_activation: فعالسازی حساب confirm_email: تایید ایمیل account_suspended: حساب تعلیق شد admin: ادمین change_email: نگارش ایمیل install: نصب Bepors upgrade: بروزرسانی بپرس maintenance: تعمیر و نگهداری وب سایت users: کاربرها oauth_callback: در حال پردازش http_404: خطای 404 HTTP http_50X: خطای 500 HTTP http_403: خطای 403 HTTP logout: خروج posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: اعلانات inbox: پیغام‌های دریافتی achievement: دستاوردها new_alerts: هشدار جدید all_read: علامتگذاری همه بعنوان خوانده شده show_more: نمایش بیشتر someone: کسی inbox_type: all: همه posts: پست ها invites: دعوت ها votes: آراء answer: Answer question: Question badge_award: Badge suspended: title: حساب شما معلق شده است until_time: "حساب شما تا تاریخ {{ time }} به حالت تعلیق درآمده است." forever: این کاربر برای همیشه به حالت تعلیق درآمده است. end: شما یک دستورالعمل انجمن را رعایت نمی کنید. contact_us: ارتباط با ما editor: blockquote: text: مسابقه bold: text: قوی chart: text: نمودار flow_chart: نمودار جریان sequence_diagram: نمودار توالی class_diagram: نمودار کلاس state_diagram: نمودار حالت entity_relationship_diagram: نمودار رابطه موجودیت user_defined_diagram: نمودار تعریف شده توسط کاربر gantt_chart: نمودار گانت pie_chart: نمودار دابره‌ای code: text: نمونه کد add_code: نمونه کد اضافه کنید form: fields: code: label: کد msg: empty: کد نمی تواند خالی باشد. language: label: زبان placeholder: تشخیص خودکار btn_cancel: لغو btn_confirm: اضافه کردن formula: text: فرمول options: inline: فرمول در خط block: بلاک کردن فرمول heading: text: سرفصل options: h1: سرفصل ۱ h2: سرفصل ۲ h3: سرفصل ۳ h4: سرفصل ۴ h5: سرفصل ۵ h6: سرفصل ۶ help: text: راهنما hr: text: خط افقی image: text: عکس add_image: افزودن عکس tab_image: آپلود عکس form_image: fields: file: label: فایل عکس btn: انتخاب عکس msg: empty: فایل نمی تواند خالی باشد. only_image: فقط فایل های تصویری مجاز هستند. max_size: File size cannot exceed {{size}} MB. desc: label: توضیحات tab_url: لینک عکس form_url: fields: url: label: لینک عکس msg: empty: آدرس عکس نمی‌تواند خالی باشد. name: label: توضیحات btn_cancel: لغو btn_confirm: اضافه کردن uploading: درحال ارسال indent: text: تورفتگی outdent: text: بیرون آمدگی italic: text: تاکید link: text: فراپیوند add_link: اضافه کردن فراپیوند form: fields: url: label: آدرس msg: empty: آدرس نمی‌تواند خالی باشد. name: label: توضیح btn_cancel: لغو btn_confirm: افزودن ordered_list: text: فهرست عددی unordered_list: text: لیست گلوله‌ای table: text: جدول heading: سرفصل cell: تلفن همراه file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: این پست را می بندم بدلیل... btn_cancel: لغو btn_submit: فرستادن remark: empty: نمی‌تواند خالی باشد. msg: empty: لطفا یک دلیل را انتخاب کنید. report_modal: flag_title: من پرچم گذاری می کنم تا این پست را به عنوان گزارش کنم... close_title: این پست را می بندم بدلیل... review_question_title: بازبینی سوال review_answer_title: بازبینی جواب review_comment_title: بازبینی نظر btn_cancel: لغو btn_submit: ثبت remark: empty: نمی‌تواند خالی باشد. msg: empty: لطفا یک دلیل را انتخاب کنید. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: ساخت تگ جدید form: fields: display_name: label: نام msg: empty: نام نمی تواند خالی باشد. range: نام باید نهایتا ۳۵ حرف داشته باشد. slug_name: label: نامک آدس desc: نامک آدرس باید نهایتا ۳۵ حرف داشته باشد. msg: empty: نامک آدرس نمی تواند خالی باشد. range: نامک آدرس باید نهایتا ۳۵ حرف داشته باشد. character: نامک آدرس شامل کلمات غیر مجاز می باشد. desc: label: توضیحات revision: label: تجدید نظر edit_summary: label: ویرایش خلاصه placeholder: >- تغییرات خود را به طور خلاصه توضیح دهید (املا صحیح، دستور زبان مناسب، قالب بندی بهبود یافته) btn_cancel: لغو btn_submit: ثبت btn_post: پست کردن تگ جدید tag_info: created_at: ایجاد شده edited_at: ویرایش شده history: تاریخچه synonyms: title: مترادف ها text: تگ های زیر مجدداً به آنها نگاشت می شوند empty: مترادفی پیدا نشد. btn_add: افزودن مترادف btn_edit: ویرایش btn_save: ذخیره synonyms_text: تگ های زیر مجدداً به آنها نگاشت می شوند delete: title: این برچسب حذف شود tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: مطمئنید که میخواهید حذف شود? close: بستن merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: ویرایش تگ‌ default_reason: ویرایش تگ‌ default_first_reason: برچسب اضافه کنید btn_save_edits: ذخیره ویرایشها btn_cancel: لغو dates: long_date: ماه ماه ماه روز long_date_with_year: "ماه روز، سال" long_date_with_time: "ماه روز، سال در ساعت:دقیقه" now: حالا x_seconds_ago: "{{count}} ثانیه پیش" x_minutes_ago: "{{count}} دقیقه پیش" x_hours_ago: "{{count}}ساعت پیش" hour: ساعت day: روز hours: ساعات days: روزها month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: افزودن نظر reply_to: پاسخ به btn_reply: پاسخ btn_edit: ویرایش btn_delete: حذف btn_flag: نشان btn_save_edits: ذخیره ویرایشها btn_cancel: لغو show_more: "{{count}} نظر بیشتر" tip_question: >- از نظرات برای درخواست اطلاعات بیشتر یا پیشنهاد بهبود استفاده کنید. از پاسخ دادن به سوالات در نظرات خودداری کنید. tip_answer: >- از نظرات برای پاسخ دادن به سایر کاربران یا اطلاع دادن آنها از تغییرات استفاده کنید. اگر اطلاعات جدیدی اضافه می کنید، به جای نظر دادن، پست خود را ویرایش کنید. tip_vote: چیز مفیدی به پست اضافه می کند edit_answer: title: ویرایش جواب default_reason: ویرایش جواب default_first_reason: پاسخ را اضافه کنید form: fields: revision: label: بازنگری answer: label: پاسخ feedback: characters: متن باید حداقل ۶ حرف داشته باشد. edit_summary: label: ویرایش خلاصه placeholder: >- بطور خلاصه تغییرات را توضیح دهید (اصلاح املایی، اصلاح دستورزبان،‌ بهبود فرمت دهی) btn_save_edits: ذخیره ویرایش ها btn_cancel: لغو tags: title: برچسب ها sort_buttons: popular: محبوب name: نام newest: جدیدترین button_follow: دنبال کردن button_following: دنبال می کنید tag_label: سوالات search_placeholder: فیلتر بر اساس اسم برچسب no_desc: برچسب هیچ توضیحی ندارد. more: بیشتر wiki: Wiki ask: title: Create Question edit_title: سوال را ویرایش کنید default_reason: سوال را ویرایش کنید default_first_reason: Create question similar_questions: سؤال های مشابه form: fields: revision: label: تجدید نظر title: label: عنوان placeholder: What's your topic? Be specific. msg: empty: عنوان نمی تواند خالی باشد. range: عنوان میتواند تا ۳۰ حرف باشد body: label: بدنه msg: empty: بدنه نمی تواند خالی باشد. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: برچسب ها msg: empty: برچسب ها نمی تواند خالی باشد. answer: label: پاسخ msg: empty: جواب نمی تواند خالی باشد. edit_summary: label: ویرایش خلاصه placeholder: >- بطور خلاصه تغییرات را توضیح دهید (اصلاح املایی، اصلاح دستورزبان،‌ بهبود فرمت دهی) btn_post_question: سوال خود را پست کنید btn_save_edits: ذخیره ویرایش ها answer_question: جواب دادن به سوال خودتان post_question&answer: سوال خود را پست و جواب دهید tag_selector: add_btn: اضافه کردن برچسب create_btn: ایجاد یک برچسب جدید search_tag: جست‌وجوی برچسب‌ hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: هیچ تگی مطابقت ندارد tag_required_text: تگ نیاز هست (حداقل یک مورد) header: nav: question: سوالات tag: تگ‌ها user: کاربران badges: Badges profile: پروفایل setting: تنظیمات logout: خروج admin: ادمین review: بازبینی bookmark: نشانک ها moderation: مدیریت search: placeholder: جستجو footer: build_on: Powered by <1> Apache Answer upload_img: name: تغییر loading: درحال بارگذاری... pic_auth_code: title: کپچا placeholder: متن بالا را تاپ کنید msg: empty: کپچا نمی تواند خالی باشد. inactive: first: >- شما تقریباً آماده شده اید! ما یک ایمیل فعال سازی به {{mail}} ارسال کردیم. لطفا دستورالعمل های ایمیل را برای فعال کردن حساب خود دنبال کنید. info: "اگر ایمیل ارسالی را دریافت نکردید، قسمت spam خود را چک کنید." another: >- ایمیل فعال‌سازی دیگری را به آدرس {{mail}} برای شما ارسال کردیم. ممکن است چند دقیقه طول بکشد تا دستتان برسد. پوشه هرزنامه خود را حتما چک کنید. btn_name: ارسال مجدد کد فعالسازی change_btn_name: تغییر ایمیل msg: empty: نمی‌تواند خالی باشد. resend_email: url_label: آیا مطمئن هستید که می خواهید ایمیل فعال سازی را دوباره ارسال کنید؟ url_text: همچنین می توانید لینک فعال سازی بالا را در اختیار کاربر قرار دهید. login: login_to_continue: برای ادامه وارد حساب کاربری شوید info_sign: حساب کاربری ندارید؟ ثبت نام<1> info_login: از قبل حساب کاربری دارید؟ <1>وارد شوید agreements: با ثبت نام، با <1>خط مشی رازداری و <3>شرایط خدمات موافقت می کنید. forgot_pass: رمزعبور را فراموش کردید? name: label: نام msg: empty: نام نمی‌تواند خالی باشد. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: ایمیل msg: empty: ایمیل نمی تواند خالی باشد. password: label: رمز عبور msg: empty: رمز عبور نمی تواند خالی باشد. different: پسوردهای وارد شده در هر دو طرف متناقض هستند account_forgot: page_title: رمزعبور را فراموش کردید btn_name: ارسال ایمیل بازیابی send_success: >- اگر یک حساب با {{mail}} مطابقت داشته باشد، باید به زودی ایمیلی حاوی دستورالعمل‌هایی درباره نحوه بازنشانی رمز عبور خود دریافت کنید. email: label: ایمیل msg: empty: ایمیل نمی تواند خالی باشد. change_email: btn_cancel: لغو btn_update: نشانی ایمیل را به روز کنید send_success: >- اگر یک حساب با {{mail}} مطابقت داشته باشد، باید به زودی ایمیلی حاوی دستورالعمل‌هایی درباره نحوه بازنشانی رمز عبور خود دریافت کنید. email: label: ایمیل جدید msg: empty: ایمیل نمی تواند خالی باشد. oauth: connect: ارتباط با {{ auth_name }} remove: حذف {{ auth_name }} oauth_bind_email: subtitle: یک ایمیل بازیابی به حساب خود اضافه کنید. btn_update: نشانی ایمیل را به روز کنید email: label: ایمیل msg: empty: ایمیل نمی تواند خالی باشد. modal_title: ایمیل تکراری. modal_content: این ایمیل قبلا ثبت نام کرده است. آیا مطمئن هستید که می خواهید به حساب ثبت نام کرده متصل شوید? modal_cancel: تغییر ایمیل modal_confirm: به حساب کاربری ثبت نام کرده متصل شوید password_reset: page_title: بازیابی کلمه عبور btn_name: رمز عبورم را بازنشانی کن reset_success: >- شما با موفقیت رمز عبور خود را تغییر دادید، به صفحه ورود هدایت می شوید. link_invalid: >- متاسفم،‌ لینک بازنشانی رمز عبور دیگر اعتبار ندارد. شاید رمز عبور شما قبلا تغییر کرده است? to_login: ادامه بدهید تا به صفحه ورود برسید password: label: رمز عبور msg: empty: رمز عبور نمی تواند خالی باشد. length: تعداد حروف باید بین ۸ تا ۳۲ باشد different: پسوردهای وارد شده در هر دو طرف متناقض هستند password_confirm: label: تأیید رمز عبور جديد settings: page_title: تنظیمات goto_modify: برای تغییر بروید nav: profile: پروفایل notification: اعلانات account: حساب کاربری interface: رابط کاربری profile: heading: پروفایل btn_name: ذخیره display_name: label: نام msg: نام نمی تواند خالی باشد. msg_range: Display name must be 2-30 characters in length. username: label: نام‌کاربری caption: دیگران میتوانند به شما به بصورت "@username" اشاره کنند. msg: نام کاربری نمی تواند خالی باشد. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: عکس پروفایل gravatar: Gravatar gravatar_text: می توانید تصویر را تغییر دهید custom: سفارشی custom_text: شما میتوانید عکس خود را بازگذاری کنید. default: سیستم msg: لطفا یک آواتار آپلود کنید bio: label: درباره من website: label: وب سایت placeholder: "https://example.com" msg: فرمت نادرست وب سایت location: label: موقعیت placeholder: "شهر، کشور" notification: heading: اعلان های ایمیلی turn_on: روشن کردن inbox: label: اعلانات ایمیل description: پاسخ به سوالات خود،‌ نظرات،‌ دعوت ها،‌و بیشتر. all_new_question: label: تمامی سوالات جدید description: درمورد تمامی سوالات جدید با خبر شوید. تا ۵۰ سوال در هفته. all_new_question_for_following_tags: label: تمامی سوالات جدید برای این تگ ها description: درمورد تمامی سوالات جدید در مورد این تگ ها باخبر شوید. account: heading: حساب کاربری change_email_btn: تغییر ایمیل change_pass_btn: تغییر رمز عبور change_email_info: >- ما یک ایمیل به آن آدرس ارسال کردیم. لطفا مراحل تایید را طی کنید. email: label: Email new_email: label: New email msg: New email cannot be empty. pass: label: رمز عبور فعلی msg: رمز عبور نمی تواند خالی باشد. password_title: رمز عبور current_pass: label: رمز عبور فعلی msg: empty: رمز عبور نمی تواند خالی باشد. length: تعداد حروف باید بین ۸ تا ۳۲ باشد. different: دو رمز عبور وارد شده همخوانی ندارند. new_pass: label: رمز عبور جدید pass_confirm: label: تأیید رمز عبور جديد interface: heading: رابط کاربری lang: label: زبان رابط کاربری text: زبان رابط کاربری. زمانی تغییر می کند که صفحه را دوباره بارگذاری کنید. my_logins: title: ورود های من label: با استفاده از این حساب ها وارد این سایت شوید یا ثبت نام کنید. modal_title: حذف ورود modal_content: آیا مطمئن هستید که می خواهید این ورود را از حساب خود حذف کنید؟ modal_confirm_btn: حذف remove_success: با موفقیت حذف شد toast: update: بروز رسانی موفق بود update_password: رمز عبور با موفقیت تغییر کرد. flag_success: ممنون بابت اطلاع دادن. forbidden_operate_self: عملیات غیر مجاز review: بازبینی شما پس از بررسی نشان داده خواهد شد. sent_success: با موفقيت ارسال شد related_question: title: Related answers: جواب ها linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: مردم پرسیدند desc: افرادی را دعوت کنید که فکر می کنید ممکن است پاسخ را بدانند. invite: دعوت به پاسخ add: افزودن افراد search: جستجوی افراد question_detail: action: عملیات created: Created Asked: پرسیده شده asked: پرسیده شده update: تغییر یافته Edited: Edited edit: ویرایش شده commented: commented Views: مشاهده شده Follow: دنبال کردن Following: دنبال می کنید follow_tip: برای دریافت اعلان ها این سوال را دنبال کنید answered: جواب داده closed_in: بسته شده د show_exist: نمایش سوال موجود. useful: مفید question_useful: مفید و واضح است question_un_useful: نامشخص یا مفید نیست question_bookmark: این سوال را نشانه گذاری کنید answer_useful: مفید است answer_un_useful: مفید نیست answers: title: پاسخ ها score: امتیاز newest: جدیدترین oldest: Oldest btn_accept: پذیرفتن btn_accepted: پذیرفته شده write_answer: title: پاسخ شما edit_answer: پاسخ فعلی من را ویرایش کنید btn_name: پاسخ خود را ارسال کنید add_another_answer: پاسخ دیگری اضافه کنید confirm_title: به پاسخ دادن ادامه دهید continue: ادامه دهید confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: جواب نمی تواند خالی باشد. characters: متن باید حداقل ۶ حرف داشته باشد. tips: header_1: از جواب شما متشکریم li1_1: لطفا مطمئن شوید که جواب دهید سوال را. جزئیات بیشتری ارائه دهید و تحقیقات خود را به اشتراک بگذارید. li1_2: از هر اظهاراتی که می کنید با ارجاعات یا تجربه شخصی پشتیبان بگیرید. header_2: اما دوری کنید ... li2_1: درخواست کمک، به دنبال شفاف سازی، یا پاسخ به پاسخ های دیگر. reopen: confirm_btn: بازگشایی مجدد title: بازگشایی مجدد این پست content: آیا مطمئن هستید که می‌خواهید بازگشایی مجدد انجام دهید? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: پست را پین کن content: آیا مطمئن هستید میخواهید پست بصورت عمومی پین شود؟ پست در بالای تمامی پست ها نشان داده خواهد شد. confirm_btn: پین کردن delete: title: حذف این پست question: >- ما حذف سوالات با جواب را پیشنهاد نمی کنیم، زیرا انجام این کار خوانندگان آینده را از این دانش محروم می کند.

حذف مکرر سؤالات پاسخ داده شده می تواند منجر به مسدود شدن حساب شما از سؤال شود. آیا مطمئن هستید که می خواهید حذف کنید? answer_accepted: >- ما حذف جواب تایید شده را پیشنهاد نمی کنیم، زیرا انجام این کار خوانندگان آینده را از این دانش محروم می کند.

حذف مکرر سؤالات پاسخ داده شده می تواند منجر به مسدود شدن حساب شما از سؤال شود. آیا مطمئن هستید که می خواهید حذف کنید? other: مطمئنید که میخواهید حذف شود? tip_answer_deleted: جواب شما حذف شده است undelete_title: حذف این پست undelete_desc: آیا مطمئن به بازگردانی هستید؟ btns: confirm: تایید cancel: لغو edit: ویرایش save: ذخیره delete: حذف undelete: بازگردانی list: List unlist: Unlist unlisted: Unlisted login: ورود signup: عضويت logout: خروج verify: تایید create: Create approve: تایید reject: رد کردن skip: بعدی discard_draft: دور انداختن پیش‌نویس‌ pinned: پین شد all: همه question: سوال answer: پاسخ comment: نظر refresh: بارگذاری مجدد resend: ارسال مجدد deactivate: غیرفعال کردن active: فعال suspend: تعلیق unsuspend: لغو تعلیق close: بستن reopen: بازگشایی ok: تأیید light: روشن dark: تیره system_setting: تنظیمات سامانه default: Default reset: Reset tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: نتایج جستجو keywords: کلیدواژه ها options: گزینه‌ها follow: دنبال کردن following: دنبال میکنید counts: "{{count}} نتیجه" counts_loading: "... Results" more: بیشتر sort_btns: relevance: مرتبط newest: جدیدترین active: فعال score: امتیاز more: بیشتر tips: title: گزینه های پیشرفته جستجو tag: "<1>[tag] search with a tag" user: "<1>user:username جستجو براساس نویسنده" answer: "<1>answers:0 سوال بی جواب" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: چیزی پیدا نکردیم
کلمات کلیدی متفاوت یا کمتر خاص را امتحان کنید. share: name: اشتراک‌گذاری copy: کپی کردن لینک via: اشتراک گذاری پست با... copied: کپی انجام شد facebook: اشتراک گذاری در فیس بوک twitter: Share to X cannot_vote_for_self: شما نمی توانید به پست خودتان رای دهید. modal_confirm: title: خطا... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: اکانت جدید شما تایید شده است، به صفحه خانه منتقل خواهید شد. link: ادامه بدهید تا به صفحه خانه برسید oops: Oops! invalid: The link you used no longer works. confirm_new_email: ایمیل شما به‌روز شده است. confirm_new_email_invalid: >- متاسفیم، لینک تایید دیگر مجاز نیست. شاید حساب شما از قبل فعال شده است? unsubscribe: page_title: قطع عضویت success_title: لغو عضویت موفق (Automatic Translation) success_desc: شما با موفقیت از این لیست مشترک حذف شده اید و دیگر ایمیلی از ما دریافت نخواهید کرد. link: تغییر تنظیمات question: following_tags: تگهای مورد نظر edit: ویرایش save: ذخیره follow_tag_tip: برچسب ها را دنبال کنید تا لیست سوالات خود را تنظیم کنید. hot_questions: سوالات داغ all_questions: تمام سوالات x_questions: "{{ count }} سوال" x_answers: "{{ count }} جواب" x_posts: "{{ count }} Posts" questions: سوالات answers: پاسخ ها newest: جدیدترین active: فعال hot: Hot frequent: Frequent recommend: Recommend score: امتیاز unanswered: بدون پاسخ modified: تغییر یافته answered: جواب داده asked: پرسیده شده closed: بسته follow_a_tag: یک برچسب را دنبال کنید more: بیشتر personal: overview: خلاصه answers: پاسخ ها answer: پاسخ questions: سوالات question: سوال bookmarks: نشان ها reputation: محبوبیت comments: نظرات votes: آراء badges: Badges newest: جدیدترین score: امتیاز edit_profile: ویرایش پروفایل visited_x_days: "Visited {{ count }} days" viewed: مشاهده شده joined: عضو شد comma: "," last_login: مشاهده شده about_me: درباره من about_me_empty: "// Hello, World !" top_answers: پاسخ های برتر top_questions: سوالات برتر stats: آمار list_empty: No posts found.
Perhaps you'd like to select a different tab? content_empty: No posts found. accepted: پذیرفته شده answered: جواب داده asked: پرسیده شده downvoted: رأی منفی mod_short: MOD mod_long: Moderators x_reputation: محبوبیت x_votes: آرای دریافت شد x_answers: جواب ها x_questions: سوالات recent_badges: Recent Badges install: title: Installation next: بعدی done: انجام شده config_yaml_error: نمیتوان فایل config.yaml را ایجاد کرد. lang: label: لطفا زبان خود را انتخاب کنید db_type: label: موتور پایگاه‌داده db_username: label: نام‌کاربری placeholder: روت msg: نام کاربری نمی تواند خالی باشد. db_password: label: رمز عبور placeholder: روت msg: رمز عبور نمی تواند خالی باشد. db_host: label: میزبان پایگاه داده placeholder: "db:3306" msg: پایگاه داده میزبان نمیتواند خالی باشد. db_name: label: نام پایگاه‌داده placeholder: پاسخ msg: نام پایگاه داده میزبان نمیتواند خالی باشد. db_file: label: فایل پایگاه داده placeholder: /data/answer.db msg: فایل پایگاه داده نمیتواند خالی باشد. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Config.yaml را بسازید label: فایل config.yaml ساخته شد. desc: >- شما بصورت دستی میتوانید فایل <1>config.yaml را در پوشه <1>/var/wwww/xxx/ ایجاد و متن را در داخل آن جایگذاری کنید. info: بعد از اتمام،‌ بر روی "بعدی" کلیک کنید. site_information: اطلاعات سایت admin_account: حساب ادمین site_name: label: نام سایت msg: نام سایت نمی تواند خالی باشد. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: آدرس سایت text: آدرس سایت شما msg: empty: آدرس سایت نمی تواند خالی باشد. incorrect: فرمت آدرس سایت نادرست است. max_length: Site URL must be at maximum 512 characters in length. contact_email: label: ایمیل ارتباطی text: آدرس ایمیل مخاطب کلیدی پاسخگو برای این سایت. msg: empty: ایمیل مخاطب نمی تواند خالی باشد. incorrect: فرمت ایمیل مخاطب نادرست است. login_required: label: خصوصی switch: ورود الزامی است text: تنها افرادی که وارد سایت شده اند میتوانند به این انجمن دسترسی پیدا کنند. admin_name: label: نام msg: نام نمی‌تواند خالی باشد. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: رمز عبور text: >- شما برای ورود نیازمند این پسورد خواهید بود. لطفا در محل امنی ذخیره کنید. msg: رمز عبور نمی تواند خالی باشد. msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: ایمیل text: شما به این ایمیل برای ورود نیاز خواهید داشت. msg: empty: ایمیل نمی تواند خالی باشد. incorrect: فرمت ایمیل نادرست است. ready_title: Your site is ready ready_desc: >- اگر به تغییر بیشتر تنظیمات نیاز دارید،‌به <1> قسمت ادمین مراجعه کنید،‌میتوانید این گزینه را در منو سایت مشاهده کنید. good_luck: "خوش بگذره و موفق باشید!" warn_title: هشدار warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: قبلاً نصب شده است installed_desc: >- شما پیش از‌این وردپرس را برپا نموده‌اید. برای راه‌اندازی دوباره ابتدا جدول‌های کهنه در پایگاه‌داده را پاک نمایید. db_failed: اتصال به دیتابیس موفقیت آمیز نبود db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: مشاهده votes: آراء answers: جواب ها accepted: پذیرفته شده page_error: http_error: HTTP Error {{ code }} desc_403: شما مجوز دسترسی به این صفحه را ندارید. desc_404: متاسفانه این صفحه وجود ندارد. desc_50X: The server encountered an error and could not complete your request. back_home: بازگشت به صفحه اصلی page_maintenance: desc: "ما درحال تعمیر هستیم، به زودی برمی گردیم." nav_menus: dashboard: داشبرد contents: محتوا questions: سوالات answers: پاسخ ها users: کاربران badges: Badges flags: پرچم settings: تنظیمات general: عمومی interface: رابط کاربری smtp: SMTP branding: نام تجاری legal: قانونی write: نوشتن terms: Terms tos: قوانین privacy: حریم خصوصی seo: سئو customize: شخصی‌سازی themes: پوسته ها login: ورود privileges: مجوزها plugins: افزونه‌ها installed_plugins: پلاگین های نصب شده apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: به {{site_name}} خوش آمدید user_center: login: ورود qrcode_login_tip: لطفاً از {{ agentName }} برای اسکن کد QR و ورود به سیستم استفاده کنید. login_failed_email_tip: ورود ناموفق بود، لطفاً قبل از امتحان مجدد به این برنامه اجازه دهید به اطلاعات ایمیل شما دسترسی داشته باشد. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: ادمین dashboard: title: داشبرد welcome: Welcome to Admin! site_statistics: Site statistics questions: "سوالات:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "جواب ها:" comments: "نظرات:" votes: "آرا:" users: "Users:" flags: "علامت‌ها:" reviews: "Reviews:" site_health: Site health version: "نسخه:" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Private public: در دسترس عموم smtp: "SMTP:" timezone: "منطقه زمانی:" system_info: اطلاعات سامانه go_version: "نسخه Go:" database: "پایگاه داده:" database_size: "اندازه پایگاه داده :" storage_used: "فضای استفاده شده:" uptime: "پایداری:" links: Links plugins: افزونه‌ها github: GitHub blog: بلاگ contact: تماس forum: Forum documents: اسناد feedback: ﺑﺎﺯﺧﻮﺭﺩ support: پشتیبانی review: بازبینی config: کانفینگ update_to: آپدیت کردن به latest: آخرین check_failed: بررسی ناموفق بود "yes": "بله" "no": "نه" not_allowed: غیر مجاز allowed: مجاز enabled: فعال disabled: غیرفعال writable: قابل نوشتن not_writable: غیرقابل کُپی flags: title: علامت‌ها pending: در حالت انتظار completed: تکمیل شده flagged: علامت گذاری شده flagged_type: پرچم گذاری شده {{ type }} created: ایجاد شده action: عمل review: بازبینی user_role_modal: title: تغییر نقش کاربر به ... btn_cancel: لغو btn_submit: ثبت new_password_modal: title: تعیین رمزعبور جدید form: fields: password: label: رمز عبور text: کاربر از سیستم خارج می شود و باید دوباره وارد سیستم شود. msg: رمز عبور باید 8 تا 32 کاراکتر باشد. btn_cancel: لغو btn_submit: ثبت edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: افزودن کاربر جدید form: fields: users: label: اضافه کردن انبوه کاربر placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: '"نام، ایمیل، رمز عبور" را با کاما جدا کنید. یک کاربر در هر خط.' msg: "لطفا ایمیل کاربر را در هر خط یکی وارد کنید." display_name: label: نمایش نام msg: Display name must be 2-30 characters in length. email: label: ایمیل msg: ایمیل معتبر نمی‌باشد password: label: رمز عبور msg: رمز عبور باید 8 تا 32 کاراکتر باشد. btn_cancel: لغو btn_submit: ثبت users: title: کاربرها name: نام email: ایمیل reputation: محبوبیت created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: وضعیت role: نقش action: عمل change: تغییر all: همه staff: کارکنان more: بیشتر inactive: غیرفعال suspended: تعلیق شده deleted: حذف شده normal: عادي Moderator: مدير Admin: ادمین User: کاربر filter: placeholder: "فیلتر براساس، user:id" set_new_password: تعیین رمزعبور جدید edit_profile: Edit profile change_status: تغییر وضعیت change_role: تغییر نقش show_logs: نمایش ورود ها add_user: افزودن کاربر deactivate_user: title: غیر فعال کردن کاربر content: یک کاربر غیرفعال باید ایمیل خود را دوباره تایید کند. delete_user: title: این کاربر حذف شود content: آیا مطمئن هستید که میخواهید این کابر را حذف نمایید؟ این اقدام دائمی است! remove: محتوای آنها را حذف کنید label: تمام سوالات، پاسخ ها، نظرات و غیره را حذف کن. text: اگر می‌خواهید فقط حساب کاربر را حذف کنید، این را بررسی نکنید. suspend_user: title: تعلیق این کاربر content: کاربر تعلیق شده نمی‌تواند وارد شود. label: How long will the user be suspended for? forever: Forever questions: page_title: سوالات unlisted: Unlisted post: پست votes: آراء answers: پاسخ ها created: ایجاد شده status: وضعیت action: عمل change: تغییر pending: Pending filter: placeholder: "فیلتر براساس، user:id" answers: page_title: پاسخ ها post: پست votes: آراء created: ایجاد شده status: وضعیت action: اقدام change: تغییر filter: placeholder: "فیلتر براساس، user:id" general: page_title: عمومی name: label: نام سایت msg: نام سایت نمی تواند خالی باشد. text: "نام این سایت همانطور که در تگ عنوان استفاده شده است." site_url: label: آدرس سایت msg: آدرس سایت نمی تواند خالی باشد. validate: لطفا یک url معتبر وارد کنید. text: آدرس سایت شما. short_desc: label: نام این سایت مورد استفاده قرار گرفته است msg: توضیحات کوتاه سایت نمی تواند خالی باشد. text: "شرح کوتاه، همانطور که در تگ عنوان در صفحه اصلی استفاده شده است." desc: label: توضیحات سایت msg: توضیحات کوتاه سایت نمی تواند خالی باشد. text: "همانطور که در تگ توضیحات متا استفاده شده است، این سایت را در یک جمله توصیف کنید." contact_email: label: ایمیل ارتباطی msg: ایمیل مخاطب نمی تواند خالی باشد. validate: ایمیل تماس معتبر نیست. text: آدرس ایمیل مخاطب کلیدی مسئول این سایت. check_update: label: Software updates text: Automatically check for updates interface: page_title: رابط کاربری language: label: زبان رابط کاربری msg: زبان رابط نمی تواند خالی باشد. text: زبان رابط کاربری. زمانی تغییر می کند که صفحه را دوباره بارگذاری کنید. time_zone: label: منطقه زمانی msg: منطقه زمانی نمی تواند خالی باشد. text: شهری را در همان منطقه زمانی خود انتخاب کنید. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: از ایمیل msg: ایمیل نباید خالی باشد. text: آدرس ایمیلی که ایمیل ها از آن ارسال می شوند. from_name: label: نام فرستنده msg: ایمیل نباید خالی باشد. text: آدرس ایمیلی که ایمیل ها از آن ارسال می شوند. smtp_host: label: ميزبان SMTP msg: میزبان SMTP نمی تواند خالی باشد. text: سرور ایمیل شما. encryption: label: رمزگذاری msg: کلید رمزگذاری نمی تواند خالی باشد. text: برای اکثر سرورها SSL گزینه پیشنهادی است. ssl: SSL tls: TLS none: هیچ‌کدام smtp_port: label: پورت SMTP msg: پورت SMTP باید شماره 1 ~ 65535 باشد. text: پورت سرور ایمیل شما. smtp_username: label: نام کاربری SMTP msg: نام کاربری SMTP نمی تواند خالی باشد. smtp_password: label: رمزعبور SMTP msg: رمز عبور مدیر نمی‌تواند خالی باشد. test_email_recipient: label: گیرندگان ایمیل را آزمایش کنید text: آدرس ایمیلی را ارائه دهید که ارسال های آزمایشی را دریافت می کند. msg: گیرندگان ایمیل آزمایشی نامعتبر است smtp_authentication: label: فعال کردن احراز هویت title: تأیید هویت SMTP msg: احراز هویت SMTP نمی تواند خالی باشد. "yes": "بله" "no": "نه" branding: page_title: نام تجاری logo: label: لوگو msg: کد نمی تواند خالی باشد. text: تصویر لوگو در سمت چپ بالای سایت شما. از یک تصویر مستطیلی عریض با ارتفاع 56 و نسبت تصویر بیشتر از 3:1 استفاده کنید. اگر خالی بماند، متن عنوان سایت نشان داده می شود. mobile_logo: label: لوگوی موبایل text: لوگوی مورد استفاده در نسخه موبایلی سایت شما. از یک تصویر مستطیلی عریض با ارتفاع 56 استفاده کنید. اگر خالی بماند، تصویر از تنظیمات "لوگو" استفاده خواهد شد. square_icon: label: نماد مربعی msg: نماد مربعی نمی تواند خالی باشد. text: تصویر به عنوان پایه نمادهای متادیتا استفاده می شود. در حالت ایده آل باید بزرگتر از 512x512 باشد. favicon: label: نمادک text: فاویکون برای سایت شما. برای اینکه روی CDN به درستی کار کند باید یک png باشد. به 32x32 تغییر اندازه خواهد شد. اگر خالی بماند، از "نماد مربع" استفاده می شود. legal: page_title: قانونی terms_of_service: label: شرایط خدمات text: "می توانید محتوای شرایط خدمات را در اینجا اضافه کنید. اگر قبلاً سندی دارید که در جای دیگری میزبانی شده است، URL کامل را در اینجا ارائه دهید." privacy_policy: label: حریم خصوصی text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for each question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: بهینه‌سازی عملیات موتورهای جستجو permalink: label: پیوند ثابت text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. color_scheme: label: Color scheme navbar_style: label: Navbar background style primary_color: label: Primary color text: Modify the colors used by your themes layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: > head: label: Head text: > header: label: Header text: > footer: label: Footer text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: دامنه های مجاز ایمیل text: دامنه های ایمیلی که کاربران باید با آنها حساب ثبت کنند. یک دامنه در هر خط. وقتی خالی است نادیده گرفته می شود. private: title: Private label: Login required text: Only logged in users can access this community. password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: All active: Active inactive: Inactive outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Name version: Version status: وضعیت action: اقدام deactivate: Deactivate activate: فعال سازی settings: تنظیمات settings_users: title: کاربران avatar: label: آواتار پیش فرض text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar Base URL text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Allow users to change their display name allow_update_username: label: Allow users to change their username allow_update_avatar: label: Allow users to change their profile image allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optional) empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." select: Select page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: پست فهرست نشده timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created pin: pinned unpin: unpinned show: listed hide: unlisted title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: Our community staff reputation: reputation votes: votes prompt: leave_page: Are you sure you want to leave the page? changes_not_save: Your changes may not be saved. draft: discard_confirm: Are you sure you want to discard your draft? messages: post_deleted: This post has been deleted. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/fi_FI.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. email: other: Email password: other: Password email_or_password_wrong_error: other: Email and password do not match. error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: not_found: other: Tag not found. recommend_tag_not_found: other: Recommend Tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: other: The From Name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to Revision. user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. report: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude: name: other: rude or abusive desc: other: A reasonable person would find this content inappropriate for respectful discourse. duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. not_answer: name: other: not an answer desc: other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. not_need: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. other: name: other: something else desc: other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comments tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to {{site_name}} btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to {{site_name}} success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/fr_FR.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Succès. unknown: other: Erreur inconnue. request_format_error: other: Format de fichier incorrect. unauthorized_error: other: Non autorisé. database_error: other: Erreur du serveur de données. forbidden_error: other: Interdit. duplicate_request_error: other: Soumission en double. action: report: other: Signaler edit: other: Éditer delete: other: Supprimer close: other: Fermer reopen: other: Rouvrir forbidden_error: other: Interdit. pin: other: Épingler hide: other: Délister unpin: other: Désépingler show: other: Liste invite_someone_to_answer: other: Modifier undelete: other: Annuler la suppression merge: other: Fusionner role: name: user: other: Utilisateur admin: other: Administrateur moderator: other: Modérateur description: user: other: Par défaut, sans accès spécial. admin: other: Possède tous les droits pour accéder au site. moderator: other: Possède les accès à tous les messages sauf aux paramètres d'administration. privilege: level_1: description: other: Niveau 1 (moins de réputation requise pour une équipe privée, un groupe) level_2: description: other: Niveau 2 (faible réputation requise pour la communauté des startups) level_3: description: other: Niveau 3 (haute réputation requise pour une communauté mature) level_custom: description: other: Niveau personnalisé rank_question_add_label: other: Poser une question rank_answer_add_label: other: Écrire une réponse rank_comment_add_label: other: Ajouter un commentaire rank_report_add_label: other: Signaler rank_comment_vote_up_label: other: Voter favorablement le commentaire rank_link_url_limit_label: other: Poster plus de 2 liens à la fois rank_question_vote_up_label: other: Voter favorablement la question rank_answer_vote_up_label: other: Voter favorablement la réponse rank_question_vote_down_label: other: Voter contre la question rank_answer_vote_down_label: other: Voter contre la réponse rank_invite_someone_to_answer_label: other: Inviter quelqu'un à répondre rank_tag_add_label: other: Créer une nouvelle étiquette rank_tag_edit_label: other: Modifier la description de la balise (à réviser) rank_question_edit_label: other: Modifier la question des autres (à revoir) rank_answer_edit_label: other: Modifier la réponse d'un autre (à revoir) rank_question_edit_without_review_label: other: Modifier la question d'un autre utilisateur sans révision rank_answer_edit_without_review_label: other: Modifier la réponse d'un autre utilisateur sans révision rank_question_audit_label: other: Vérifier la question rank_answer_audit_label: other: Revoir les modifications de la réponse rank_tag_audit_label: other: Évaluer les modifications des tags rank_tag_edit_without_review_label: other: Modifier la description du tag sans révision rank_tag_synonym_label: other: Gérer les tags synonyme email: other: Email e_mail: other: Email password: other: Mot de passe pass: other: Mot de passe old_pass: other: Mot de passe actuel original_text: other: Ce post email_or_password_wrong_error: other: L'email et le mot de passe ne correspondent pas. error: common: invalid_url: other: URL invalide. status_invalid: other: Statut invalide. password: space_invalid: other: Le mot de passe ne doit pas comporter d'espaces. admin: cannot_update_their_password: other: Vous ne pouvez pas modifier votre mot de passe. cannot_edit_their_profile: other: Vous ne pouvez pas modifier votre profil. cannot_modify_self_status: other: Vous ne pouvez pas modifier votre statut. email_or_password_wrong: other: L'email et le mot de passe ne correspondent pas. answer: not_found: other: Réponse introuvable. cannot_deleted: other: Pas de permission pour supprimer. cannot_update: other: Pas de permission pour mettre à jour. question_closed_cannot_add: other: Les questions sont fermées et ne peuvent pas être ajoutées. content_cannot_empty: other: La réponse ne peut être vide. comment: edit_without_permission: other: Les commentaires ne sont pas autorisés à être modifiés. not_found: other: Commentaire non trouvé. cannot_edit_after_deadline: other: Le commentaire a été posté il y a trop longtemps pour être modifié. content_cannot_empty: other: Le commentaire ne peut être vide. email: duplicate: other: L'adresse e-mail existe déjà. need_to_be_verified: other: L'adresse e-mail doit être vérifiée. verify_url_expired: other: L'URL de vérification de l'email a expiré, veuillez renvoyer l'email. illegal_email_domain_error: other: L'e-mail n'est pas autorisé à partir de ce domaine de messagerie. Veuillez en utiliser un autre. lang: not_found: other: Fichier de langue non trouvé. object: captcha_verification_failed: other: Le Captcha est incorrect. disallow_follow: other: Vous n’êtes pas autorisé à suivre. disallow_vote: other: Vous n’êtes pas autorisé à voter. disallow_vote_your_self: other: Vous ne pouvez pas voter pour votre propre message. not_found: other: Objet non trouvé. verification_failed: other: La vérification a échoué. email_or_password_incorrect: other: L'e-mail et le mot de passe ne correspondent pas. old_password_verification_failed: other: La vérification de l'ancien mot de passe a échoué new_password_same_as_previous_setting: other: Le nouveau mot de passe est le même que le précédent. already_deleted: other: Ce post a été supprimé. meta: object_not_found: other: Méta objet introuvable question: already_deleted: other: Ce message a été supprimé. under_review: other: Votre message est en attente de révision. Il sera visible une fois approuvé. not_found: other: Question non trouvée. cannot_deleted: other: Pas de permission pour supprimer. cannot_close: other: Pas de permission pour fermer. cannot_update: other: Pas de permission pour mettre à jour. content_cannot_empty: other: Le contenu ne peut pas être vide. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Le rang de réputation ne remplit pas la condition. vote_fail_to_meet_the_condition: other: Merci pour vos commentaires. Vous avez besoin d'au moins {{.Rank}} de réputation pour voter. no_enough_rank_to_operate: other: Vous avez besoin d'au moins {{.Rank}} de réputation pour faire cela. report: handle_failed: other: La gestion du rapport a échoué. not_found: other: Rapport non trouvé. tag: already_exist: other: Le tag existe déjà. not_found: other: Tag non trouvé. recommend_tag_not_found: other: Le tag Recommandé n'existe pas. recommend_tag_enter: other: Veuillez saisir au moins un tag. not_contain_synonym_tags: other: Ne dois pas contenir de tags synonymes. cannot_update: other: Pas de permission pour mettre à jour. is_used_cannot_delete: other: Vous ne pouvez pas supprimer un tag utilisé. cannot_set_synonym_as_itself: other: Vous ne pouvez pas définir le synonyme de la balise actuelle comme elle-même. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: Le nom d'expéditeur ne peut pas être une adresse e-mail. theme: not_found: other: Thème non trouvé. revision: review_underway: other: Impossible d'éditer actuellement, il y a une version dans la file d'attente des revues. no_permission: other: Aucune autorisation de réviser. user: external_login_missing_user_id: other: La plateforme tierce ne fournit pas un identifiant d'utilisateur unique, vous ne pouvez donc pas vous connecter, veuillez contacter l'administrateur du site. external_login_unbinding_forbidden: other: Veuillez définir un mot de passe de connexion pour votre compte avant de supprimer ce login. email_or_password_wrong: other: other: L'email et le mot de passe ne correspondent pas. not_found: other: Utilisateur non trouvé. suspended: other: L'utilisateur a été suspendu. username_invalid: other: Le nom d'utilisateur est invalide. username_duplicate: other: Nom d'utilisateur déjà utilisé. set_avatar: other: La configuration de l'avatar a échoué. cannot_update_your_role: other: Vous ne pouvez pas modifier votre rôle. not_allowed_registration: other: Actuellement, le site n'est pas ouvert aux inscriptions. not_allowed_login_via_password: other: Actuellement le site n'est pas autorisé à se connecter par mot de passe. access_denied: other: Accès refusé page_access_denied: other: Vous n'avez pas accès à cette page. add_bulk_users_format_error: other: "Erreur format {{.Field}} près de '{{.Content}}' à la ligne {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Le nombre d'utilisateurs que vous ajoutez simultanément doit être compris entre 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: La lecture de la configuration a échoué database: connection_failed: other: La connexion à la base de données a échoué create_table_failed: other: La création de la table a échoué install: create_config_failed: other: Impossible de créer le fichier config.yaml. upload: unsupported_file_format: other: Format de fichier non supporté. site_info: config_not_found: other: Configuration du site introuvable. badge: object_not_found: other: Objet badge introuvable reason: spam: name: other: Courrier indésirable desc: other: Ce message est une publicité ou un vandalisme. Il n'est pas utile ou pertinent pour le sujet actuel. rude_or_abusive: name: other: grossier ou abusif desc: other: "Une personne raisonnable trouverait ce contenu inapproprié pour un discours respectueux." a_duplicate: name: other: un doublon desc: other: Cette question a déjà été posée et a déjà une réponse. placeholder: other: Entrez le lien de la question existante not_a_answer: name: other: n'est pas une réponse desc: other: "Cela a été posté comme une réponse, mais il n'essaie pas de répondre à la question. Il devrait s'agir d'un commentaire, d'une autre question, ou devrait être supprimé totalement." no_longer_needed: name: other: ce n’est plus nécessaire desc: other: Ce commentaire est obsolète, conversationnel ou non pertinent pour ce post. something: name: other: quelque chose d'autre desc: other: Ce message nécessite l'attention de l'équipe de modération pour une autre raison non listée ci-dessus. placeholder: other: Faites-nous savoir précisément ce qui vous préoccupe community_specific: name: other: une raison spécifique à la communauté desc: other: Cette question ne répond pas à une directive de la communauté. not_clarity: name: other: nécessite plus de détails ou de clarté desc: other: Cette question comprend actuellement plusieurs questions en une seule. Elle ne devrait se concentrer que sur un seul problème. looks_ok: name: other: semble bien desc: other: Ce poste est bon en tant que tel et n'est pas de mauvaise qualité. needs_edit: name: other: a besoin d'être modifié, et je l'ai fait desc: other: Améliorez et corrigez vous-même les problèmes liés à ce message. needs_close: name: other: a besoin de fermer desc: other: Une question fermée ne peut pas être répondue, mais peut-être quand même modifiée, votée et commentée. needs_delete: name: other: a besoin d'être supprimé desc: other: Ce message sera supprimé. question: close: duplicate: name: other: courrier indésirable desc: other: Cette question a déjà été posée auparavant et a déjà une réponse. guideline: name: other: une raison spécifique à la communauté desc: other: Cette question ne répond pas à une directive de la communauté. multiple: name: other: a besoin de détails ou de clarté desc: other: Cette question comprend actuellement plusieurs questions en une seule. Elle ne devrait se concentrer que sur un seul problème. other: name: other: quelque chose d'autre desc: other: Ce message nécessite l'attention du personnel pour une autre raison non listée ci-dessus. operation_type: asked: other: demandé answered: other: répondu modified: other: modifié deleted_title: other: Question supprimée questions_title: other: Questions tag: tags_title: other: Étiquettes no_description: other: L'étiquette n'a pas de description. notification: action: update_question: other: question mise à jour answer_the_question: other: question répondue update_answer: other: réponse mise à jour accept_answer: other: réponse acceptée comment_question: other: a commenté la question comment_answer: other: a commenté la réponse reply_to_you: other: vous a répondu mention_you: other: vous a mentionné your_question_is_closed: other: Une réponse a été publiée pour votre question your_question_was_deleted: other: Une réponse a été publiée pour votre question your_answer_was_deleted: other: Votre réponse a bien été supprimée your_comment_was_deleted: other: Votre commentaire a été supprimé up_voted_question: other: question approuvée down_voted_question: other: question défavorisée up_voted_answer: other: voter favorablement la réponse down_voted_answer: other: réponse défavorisée up_voted_comment: other: commentaire approuvé invited_you_to_answer: other: vous invite à répondre earned_badge: other: Vous avez gagné le badge "{{.BadgeName}}" email_tpl: change_email: title: other: "[{{.SiteName}}] Confirmez votre nouvelle adresse e-mail" body: other: "Confirmez votre nouvelle adresse électronique pour {{.SiteName}} en cliquant sur le lien suivant :
\\n{{.ChangeEmailUrl}}

\\n\\nSi vous n'avez pas demandé ce changement, veuillez ignorer cet e-mail.

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} a répondu à votre question" body: other: "{{.QuestionTitle}}

\\n\\n{{.DisplayName}}:
\\n
{{.AnswerSummary}}

\\nVoir sur {{.SiteName}}

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.

\\n\\nDésabonner" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} vous a invité à répondre" body: other: "{{.QuestionTitle}}

\\n\\n{{.DisplayName}}:
\\n
Je pense que vous pourriez connaître la réponse.

\\nVoir sur {{.SiteName}}

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.

\\n\\nDésabonner" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} a commenté votre message" body: other: "{{.QuestionTitle}}

\\n\\n{{.DisplayName}}:
\\n
{{.CommentSummary}}

\\nVoir sur {{.SiteName}}

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.

\\n\\nDésabonner" new_question: title: other: "[{{.SiteName}}] Nouvelle question : {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote : Il s'agit d'un e-mail automatique, merci de ne pas répondre à ce message, votre réponse ne pourra être considérée.

\n\nSe désabonner" pass_reset: title: other: "[{{.SiteName }}] Réinitialisation du mot de passe" body: other: "Quelqu'un a demandé à réinitialiser votre mot de passe sur {{.SiteName}}.

\\n\\nSi ce n'était pas vous, vous pouvez ignorer cet e-mail en toute sécurité.

\\n\\nCliquez sur le lien suivant pour choisir un nouveau mot de passe :
\\n{{.PassResetUrl}}\\n

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue." register: title: other: "[{{.SiteName}}] Confirmez la création de votre compte" body: other: "Bienvenue sur {{.SiteName}}!

\\n\\nCliquez sur le lien suivant pour confirmer et activer votre nouveau compte :
\\n{{.RegisterUrl}}

\\n\\nSi le lien ci-dessus n'est pas cliquable, essayez de le copier et de le coller dans la barre d'adresse de votre navigateur web.

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue." test: title: other: "[{{.SiteName}}] Email de test" body: other: "Ceci est un e-mail de test.

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue." action_activity_type: upvote: other: vote positif upvoted: other: voté pour downvote: other: voter contre downvoted: other: voté contre accept: other: accepter accepted: other: accepté edit: other: éditer review: queued_post: other: Post en file d'attente flagged_post: other: Signaler post suggested_post_edit: other: Modifications suggérées reaction: tooltip: other: "{{ .Names }} et {{ .Count }} de plus..." badge: default_badges: autobiographer: name: other: Autobiographe desc: other: Informations sur le profil. certified: name: other: Certifié desc: other: Nous avons terminé notre nouveau tutoriel d'utilisation. editor: name: other: Éditeur desc: other: Première modification du post. first_flag: name: other: Premier drapeau desc: other: Premier a signalé un post. first_upvote: name: other: Premier vote positif desc: other: Premier a signalé un post. first_link: name: other: Premier lien desc: other: A ajouté un lien vers un autre message. first_reaction: name: other: Première réaction desc: other: Première réaction au post. first_share: name: other: Premier partage desc: other: Premier post partagé. scholar: name: other: Érudit desc: other: A posé une question et accepté une réponse. commentator: name: other: Commentateur desc: other: Laissez 5 commentaires. new_user_of_the_month: name: other: Nouvel utilisateur du mois desc: other: Contributions en suspens au cours de leur premier mois. read_guidelines: name: other: Lire les lignes de conduite desc: other: Lisez les [lignes directrices de la communauté]. reader: name: other: Lecteur desc: other: Lisez toutes les réponses dans un sujet avec plus de 10 réponses. welcome: name: other: Bienvenue desc: other: A reçu un vote positif. nice_share: name: other: Bien partagé desc: other: A partagé un poste avec 25 visiteurs uniques. good_share: name: other: Bon partage desc: other: A partagé un poste avec 300 visiteurs uniques. great_share: name: other: Super Partage desc: other: A partagé un poste avec 1000 visiteurs uniques. out_of_love: name: other: Amoureux desc: other: A donné 50 likes dans une journée. higher_love: name: other: Amour plus grand desc: other: A donné 50 likes dans une journée 5 fois. crazy_in_love: name: other: Fou d'amour desc: other: A recueilli 50 votes positifs par jour 20 fois. promoter: name: other: Promoteur desc: other: Inviter un utilisateur. campaigner: name: other: Propagandiste desc: other: A invité 3 utilisateurs de base. champion: name: other: Champion desc: other: A invité 5 membres. thank_you: name: other: Merci desc: other: A 20 postes votés et a donné 10 votes. gives_back: name: other: Redonne desc: other: A 100 postes votés et a donné 100 votes. empathetic: name: other: Empathique desc: other: A 500 postes votés et a donné 1000 votes. enthusiast: name: other: Enthousiaste desc: other: Visite de 10 jours consécutifs. aficionado: name: other: Aficionado desc: other: Visite de 100 jours consécutifs. devotee: name: other: Devotee desc: other: Visite de 365 jours consécutifs. anniversary: name: other: Anniversaire desc: other: Membre actif pour une année, affiché au moins une fois. appreciated: name: other: Apprécié desc: other: A reçu 1 vote positif sur 20 posts. respected: name: other: Respecté desc: other: A reçu 2 vote positif sur 100 posts. admired: name: other: Admirée desc: other: A reçu 5 vote positif sur 300 messages. solved: name: other: Résolu desc: other: Une réponse a été acceptée. guidance_counsellor: name: other: Conseiller d'orientation desc: other: 10 réponses sont acceptées. know_it_all: name: other: Tout-savoir desc: other: 50 réponses sont acceptées. solution_institution: name: other: Institution de solution desc: other: 150 réponses sont acceptées. nice_answer: name: other: Belle réponse desc: other: Réponse a obtenu un score de 10 ou plus. good_answer: name: other: Bonne répone desc: other: Réponse a obtenu un score de 25 ou plus. great_answer: name: other: Super Réponse desc: other: Réponse a obtenu un score de 50 ou plus. nice_question: name: other: Belle Question desc: other: Question a obtenu un score de 10 ou plus. good_question: name: other: Bonne Question desc: other: Question a obtenu un score de 25 ou plus. great_question: name: other: Super Question desc: other: Question a obtenu un score de 50 ou plus. popular_question: name: other: Question Populaire desc: other: Question avec 500 points de vue. notable_question: name: other: Question notable desc: other: Question avec 1,000 points de vue. famous_question: name: other: Question célèbre desc: other: Question avec 5000 points de vue. popular_link: name: other: Lien populaire desc: other: A posté un lien externe avec 50 clics. hot_link: name: other: Lien chaud desc: other: A posté un lien externe avec 300 clics. famous_link: name: other: Célèbre lien desc: other: A posté un lien externe avec 100 clics. default_badge_groups: getting_started: name: other: Initialisation complète community: name: other: Communauté posting: name: other: Publication # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Comment mettre en forme desc: >-
  • mentionner un post: #post_id

  • Pour faire des liens

    <https://url.com>

    [Title](https://url.com)
  • mettre des retour entre les paragraphes

  • _italic_ or **gras**

  • indenter le code par 4 espaces

  • citation en plaçant > au début de la ligne

  • guillemets inversés `comme_ca_`

  • créer une banière de code avec les guillemets inversés `

    ```
    code ici
    ```
pagination: prev: Préc next: Suivant page_title: question: Question questions: Questions tag: Étiquette tags: Étiquettes tag_wiki: tag wiki create_tag: Créer un tag edit_tag: Modifier l'étiquette ask_a_question: Create Question edit_question: Modifier la question edit_answer: Modifier la réponse search: Rechercher posts_containing: Messages contenant settings: Paramètres notifications: Notifications login: Se connecter sign_up: S'inscrire account_recovery: Récupération de compte account_activation: Activation du compte confirm_email: Confirmer l'email account_suspended: Compte suspendu admin: Admin change_email: Modifier l'e-mail install: Installation d'Answer upgrade: Mise à jour d'Answer maintenance: Maintenance du site users: Utilisateurs oauth_callback: Traitement http_404: Erreur HTTP 404 http_50X: Erreur HTTP 500 http_403: Erreur HTTP 403 logout: Se déconnecter posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notifications inbox: Boîte de réception achievement: Accomplissements new_alerts: Nouvelles notifications all_read: Tout marquer comme lu show_more: Afficher plus someone: Quelqu'un inbox_type: all: Tous posts: Publications invites: Invitations votes: Votes answer: Réponse question: Question badge_award: Badge suspended: title: Votre compte a été suspendu until_time: "Votre compte a été suspendu jusqu'au {{ time }}." forever: Cet utilisateur a été suspendu pour toujours. end: Vous ne respectez pas les directives de la communauté. contact_us: Contactez-nous editor: blockquote: text: Bloc de citation bold: text: Gras chart: text: Diagramme flow_chart: Organigramme sequence_diagram: Diagramme de séquence class_diagram: Diagramme de classe state_diagram: Diagramme d'état entity_relationship_diagram: Diagramme entité-association user_defined_diagram: Diagramme défini par l'utilisateur gantt_chart: Diagramme de Gantt pie_chart: Camembert code: text: Exemple de Code add_code: Ajouter un exemple de code form: fields: code: label: Code msg: empty: Le code ne peut pas être vide. language: label: Langage placeholder: Détection automatique btn_cancel: Annuler btn_confirm: Ajouter formula: text: Formule options: inline: Formule en ligne block: Bloc de formule heading: text: Titre options: h1: Titre de niveau 1 h2: Titre de niveau 2 h3: Titre de niveau 3 h4: Titre de niveau 4 h5: Titre de niveau 5 h6: Titre de niveau 6 help: text: Aide hr: text: Ligne horizontale image: text: Image add_image: Ajouter une image tab_image: Téléverser une image form_image: fields: file: label: Fichier image btn: Sélectionner une image msg: empty: Le fichier ne doit pas être vide. only_image: Seules les images sont autorisées. max_size: La taille du fichier ne doit pas dépasser {{size}} Mo. desc: label: Description tab_url: URL de l'image form_url: fields: url: label: URL de l'image msg: empty: L'URL de l'image ne peut pas être vide. name: label: Description btn_cancel: Annuler btn_confirm: Ajouter uploading: Téléversement en cours indent: text: Indentation outdent: text: Désindenter italic: text: Mise en valeur link: text: Hyperlien add_link: Ajouter un lien hypertexte form: fields: url: label: URL msg: empty: L'URL ne peut pas être vide. name: label: Description btn_cancel: Annuler btn_confirm: Ajouter ordered_list: text: Liste numérotée unordered_list: text: Liste à puces table: text: Tableau heading: Titre cell: Cellule file: text: Joindre des fichiers not_supported: "Ne prenez pas en charge ce type de fichier. Réessayez avec {{file_type}}." max_size: "La taille du fichier ne doit pas dépasser {{size}} Mo." close_modal: title: Je ferme ce post comme... btn_cancel: Annuler btn_submit: Valider remark: empty: Ne peut pas être vide. msg: empty: Veuillez sélectionner une raison. report_modal: flag_title: Je suis en train de signaler ce post comme... close_title: Je ferme ce post comme... review_question_title: Vérifier la question review_answer_title: Vérifier la réponse review_comment_title: Revoir le commentaire btn_cancel: Annuler btn_submit: Envoyer remark: empty: Ne peut pas être vide. msg: empty: Veuillez sélectionner une raison s'il vous plaît. not_a_url: Le format de l'URL est incorrect. url_not_match: L'origine de l'URL ne correspond pas au site web actuel. tag_modal: title: Créer un nouveau tag form: fields: display_name: label: Nom Affiché msg: empty: Le nom d'affichage ne peut être vide. range: Le nom doit contenir moins de 35 caractères. slug_name: label: Limace d'URL desc: Titre de 35 caractères maximum. msg: empty: L'URL ne peut pas être vide. range: Titre de 35 caractères maximum. character: Le slug d'URL contient un jeu de caractères non autorisé. desc: label: Description revision: label: Révision edit_summary: label: Modifier le résumé placeholder: >- Expliquez brièvement vos modifications (orthographe corrigée, grammaire corrigée, mise en forme améliorée) btn_cancel: Annuler btn_submit: Valider btn_post: Publier un nouveau tag tag_info: created_at: Créé edited_at: Modifié history: Historique synonyms: title: Synonymes text: Les tags suivants seront redistribués vers empty: Aucun synonyme trouvé. btn_add: Ajouter un synonyme btn_edit: Modifier btn_save: Enregistrer synonyms_text: Les balises suivantes seront remappées en delete: title: Supprimer cette étiquette tip_with_posts: >-

Nous ne permettons pas la suppression d'un tag avec des posts

Veuillez d'abord supprimer ce tag des posts.

tip_with_synonyms: >-

Nous ne permettons pas de supprimer un tag avec des synonymes.

Veuillez d'abord supprimer les synonymes de ce tag.

tip: Êtes-vous sûr de vouloir supprimer ? close: Fermer merge: title: Étiquette de fusion source_tag_title: Étiquette de source source_tag_description: Cette étiquette de source et ses données associées seront réorganisées vers l'étiquette cible. target_tag_title: Étiquette cible target_tag_description: Un synonyme entre ces deux étiquettes sera créé après la fusion. no_results: Aucune étiquette correspondante btn_submit: Valider btn_close: Fermer edit_tag: title: Editer le tag default_reason: Éditer le tag default_first_reason: Ajouter un tag btn_save_edits: Enregistrer les modifications btn_cancel: Annuler dates: long_date: D MMM long_date_with_year: "D MMMM YYYY" long_date_with_time: "D MMM YYYY [at] HH:mm" now: maintenant x_seconds_ago: "il y a {{count}}s" x_minutes_ago: "il y a {{count}}m" x_hours_ago: "il y a {{count}}h" hour: heure day: jour hours: heures days: jours month: month months: months year: year reaction: heart: cœur smile: sourire frown: froncer les sourcils btn_label: ajout et suppression de réactions undo_emoji: annuler la réaction {{ emoji }} react_emoji: réagir à {{ emoji }} unreact_emoji: annuler la réaction avec {{ emoji }} comment: btn_add_comment: Ajoutez un commentaire reply_to: Répondre à btn_reply: Répondre btn_edit: Éditer btn_delete: Supprimer btn_flag: Balise btn_save_edits: Enregistrer les modifications btn_cancel: Annuler show_more: "{{count}} commentaires restants" tip_question: >- Utilisez les commentaires pour demander plus d'informations ou suggérer des améliorations. Évitez de répondre aux questions dans les commentaires. tip_answer: >- Utilisez des commentaires pour répondre à d'autres utilisateurs ou leur signaler des modifications. Si vous ajoutez de nouvelles informations, modifiez votre message au lieu de commenter. tip_vote: Il ajoute quelque chose d'utile au post edit_answer: title: Modifier la réponse default_reason: Modifier la réponse default_first_reason: Ajouter une réponse form: fields: revision: label: Modification answer: label: Réponse feedback: characters: le contenu doit comporter au moins 6 caractères. edit_summary: label: Modifier le résumé placeholder: >- Expliquez brièvement vos changements (correction orthographique, correction grammaticale, mise en forme améliorée) btn_save_edits: Enregistrer les modifications btn_cancel: Annuler tags: title: Étiquettes sort_buttons: popular: Populaire name: Nom newest: Le plus récent button_follow: Suivre button_following: Abonnements tag_label: questions search_placeholder: Filtrer par étiquette no_desc: L'étiquette n'a pas de description. more: Plus wiki: Wiki ask: title: Create Question edit_title: Modifier la question default_reason: Modifier la question default_first_reason: Create question similar_questions: Questions similaires form: fields: revision: label: Modification title: label: Titre placeholder: What's your topic? Be specific. msg: empty: Le titre ne peut pas être vide. range: Titre de 150 caractères maximum body: label: Corps msg: empty: Le corps ne peut pas être vide. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Étiquettes msg: empty: Les étiquettes ne peuvent pas être vides. answer: label: Réponse msg: empty: La réponse ne peut être vide. edit_summary: label: Modifier le résumé placeholder: >- Expliquez brièvement vos changements (correction orthographique, correction grammaticale, mise en forme améliorée) btn_post_question: Publier votre question btn_save_edits: Enregistrer les modifications answer_question: Répondre à votre propre question post_question&answer: Publiez votre question et votre réponse tag_selector: add_btn: Ajouter une étiquette create_btn: Créer une nouvelle étiquette search_tag: Rechercher une étiquette hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Aucune étiquette correspondante tag_required_text: Étiquette requise (au moins une) header: nav: question: Questions tag: Étiquettes user: Utilisateurs badges: Badges profile: Profil setting: Paramètres logout: Se déconnecter admin: Administration review: Vérifier bookmark: Favoris moderation: Modération search: placeholder: Rechercher footer: build_on: Powered by <1> Apache Answer upload_img: name: Remplacer loading: chargement en cours... pic_auth_code: title: Captcha placeholder: Saisissez le texte ci-dessus msg: empty: Le captcha ne peut pas être vide. inactive: first: >- Vous avez presque fini ! Un mail de confirmation a été envoyé à {{mail}}. Veuillez suivre les instructions dans le mail pour activer votre compte. info: "S'il n'arrive pas, vérifiez dans votre dossier spam." another: >- Nous vous avons envoyé un autre e-mail d'activation à {{mail}}. Cela peut prendre quelques minutes pour arriver ; assurez-vous de vérifier votre dossier spam. btn_name: Renvoyer le mail d'activation change_btn_name: Modifier l'e-mail msg: empty: Ne peut pas être vide. resend_email: url_label: Êtes-vous sûr de vouloir renvoyer l'email d'activation ? url_text: Vous pouvez également donner le lien d'activation ci-dessus à l'utilisateur. login: login_to_continue: Connectez-vous pour continuer info_sign: Vous n'avez pas de compte ? <1>Inscrivez-vous info_login: Vous avez déjà un compte ? <1>Connectez-vous agreements: En vous inscrivant, vous acceptez la <1>politique de confidentialité et les <3>conditions de service. forgot_pass: Mot de passe oublié ? name: label: Nom msg: empty: Le nom ne peut pas être vide. range: Le nom doit contenir entre 2 et 30 caractères. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Email msg: empty: L'email ne peut pas être vide. password: label: Mot de passe msg: empty: Le mot de passe ne peut pas être vide. different: Les mots de passe saisis ne sont pas identiques account_forgot: page_title: Mot de passe oublié btn_name: Envoyer un e-mail de récupération send_success: >- Si un compte est associé à {{mail}}, vous recevrez un email contenant les instructions pour réinitialiser votre mot de passe. email: label: E-mail msg: empty: L'e-mail ne peut pas être vide. change_email: btn_cancel: Annuler btn_update: Mettre à jour l'adresse e-mail send_success: >- Si un compte est associé à {{mail}}, vous recevrez un email contenant les instructions pour réinitialiser votre mot de passe. email: label: Nouvel e-mail msg: empty: L'email ne peut pas être vide. oauth: connect: Se connecter avec {{ auth_name }} remove: Retirer {{ auth_name }} oauth_bind_email: subtitle: Ajoutez un e-mail de récupération à votre compte. btn_update: Mettre à jour l'adresse e-mail email: label: Email msg: empty: L'email ne peut pas être vide. modal_title: L'email existe déjà. modal_content: Cette adresse e-mail est déjà enregistrée. Êtes-vous sûr de vouloir vous connecter au compte existant ? modal_cancel: Modifier l'adresse e-mail modal_confirm: Se connecter au compte existant password_reset: page_title: Réinitialiser le mot de passe btn_name: Réinitialiser mon mot de passe reset_success: >- Vous avez modifié votre mot de passe avec succès ; vous allez être redirigé vers la page de connexion. link_invalid: >- Désolé, ce lien de réinitialisation de mot de passe n'est plus valide. Peut-être que votre mot de passe est déjà réinitialisé ? to_login: Continuer vers la page de connexion password: label: Mot de passe msg: empty: Le mot de passe ne peut pas être vide. length: La longueur doit être comprise entre 8 et 32 different: Les mots de passe saisis ne sont pas identiques password_confirm: label: Confirmer le nouveau mot de passe settings: page_title: Paramètres goto_modify: Aller modifier nav: profile: Profil notification: Notifications account: Compte interface: Interface profile: heading: Profil btn_name: Enregistrer display_name: label: Nom affiché msg: Le nom ne peut être vide. msg_range: Le nom d'affichage doit contenir entre 2 et 30 caractères. username: label: Nom d'utilisateur caption: Les gens peuvent vous mentionner avec "@username". msg: Le nom d'utilisateur ne peut pas être vide. msg_range: Le nom d'utilisateur doit contenir entre 2 et 30 caractères. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Photo de profil gravatar: Gravatar gravatar_text: Vous pouvez modifier l'image sur custom: Personnaliser custom_text: Vous pouvez charger votre image. default: Système msg: Veuillez charger un avatar bio: label: Biographie website: label: Site Web placeholder: "https://example.com" msg: Format du site web incorrect location: label: Position placeholder: "Ville, Pays" notification: heading: Notifications turn_on: Activer inbox: label: Notifications par e-mail description: Réponses à vos questions, commentaires, invitaitons et plus. all_new_question: label: Toutes les nouvelles questions description: Recevez une notification pour toutes les nouvelles questions. Jusqu'à 50 questions par semaine. all_new_question_for_following_tags: label: Toutes les nouvelles questions pour les tags suivants description: Recevez une notification pour toutes les nouvelles questions avec les tags suivants. account: heading: Compte change_email_btn: Modifier l'adresse e-mail change_pass_btn: Changer le mot de passe change_email_info: >- Nous vous avons envoyé un mail à cette adresse. Merci de suivre les instructions. email: label: Email new_email: label: Nouvel e-mail msg: La nouvelle adresse e-mail ne peut pas être vide. pass: label: Mot de passe actuel msg: Le mot de passe ne peut pas être vide. password_title: Mot de passe current_pass: label: Mot de passe actuel msg: empty: Le mot de passe actuel ne peut pas être vide. length: La longueur doit être comprise entre 8 et 32. different: Le mot de passe saisi ne correspond pas. new_pass: label: Nouveau mot de passe pass_confirm: label: Confirmer le nouveau mot de passe interface: heading: Interface lang: label: Langue de l'interface text: Langue de l'interface utilisateur. Cela changera lorsque vous rafraîchissez la page. my_logins: title: Mes identifiants label: Connectez-vous ou inscrivez-vous sur ce site en utilisant ces comptes. modal_title: Supprimer la connexion modal_content: Confirmez-vous vouloir supprimer cette connexion de votre compte ? modal_confirm_btn: Supprimer remove_success: Supprimé avec succès toast: update: mise à jour effectuée update_password: Mot de passe changé avec succès. flag_success: Merci pour votre signalement. forbidden_operate_self: Interdit d'opérer sur vous-même review: Votre révision s'affichera après vérification. sent_success: Envoyé avec succès related_question: title: Related answers: réponses linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Personnes interrogées desc: Invite people who you think might know the answer. invite: Inviter à répondre add: Ajouter des personnes search: Rechercher des personnes question_detail: action: Action created: Created Asked: Demandé asked: demandé update: Modifié Edited: Edited edit: modifié commented: commenté Views: Consultée Follow: S’abonner Following: Abonné(s) follow_tip: Suivre cette question pour recevoir des notifications answered: répondu closed_in: Fermé dans show_exist: Afficher la question existante. useful: Utile question_useful: C'est utile et clair question_un_useful: Ce n'est pas clair ou n'est pas utile question_bookmark: Ajouter cette question à vos favoris answer_useful: C'est utile answer_un_useful: Ce n'est pas utile answers: title: Réponses score: Score newest: Les plus récents oldest: Le plus ancien btn_accept: Accepter btn_accepted: Accepté write_answer: title: Votre réponse edit_answer: Modifier ma réponse existante btn_name: Poster votre réponse add_another_answer: Ajouter une autre réponse confirm_title: Continuer à répondre continue: Continuer confirm_info: >-

Êtes-vous sûr de vouloir ajouter une autre réponse ?

Vous pouvez utiliser le lien d'édition pour affiner et améliorer votre réponse existante.

empty: La réponse ne peut être vide. characters: le contenu doit comporter au moins 6 caractères. tips: header_1: Merci pour votre réponse li1_1: N’oubliez pas de répondre à la question. Fournissez des détails et partagez vos recherches. li1_2: Sauvegardez toutes les déclarations que vous faites avec des références ou une expérience personnelle. header_2: Mais évitez... li2_1: Demander de l'aide, chercher des éclaircissements ou répondre à d'autres réponses. reopen: confirm_btn: Rouvrir title: Rouvrir ce message content: Êtes-vous sûr de vouloir rouvrir ? list: confirm_btn: Liste title: Lister ce message content: Êtes-vous sûr de vouloir lister ? unlist: confirm_btn: Délister title: Masquer ce message de la liste content: Êtes-vous sûr de vouloir masquer ce message de la liste ? pin: title: Épingler cet article content: Êtes-vous sûr de vouloir l'épingler globalement ? Ce message apparaîtra en haut de toutes les listes de messages. confirm_btn: Épingler delete: title: Supprimer la publication question: >- Nous ne recommandons pas de supprimer des questions avec des réponses car cela prive les futurs lecteurs de cette connaissance.

Suppression répétée des questions répondues peut empêcher votre compte de poser. Êtes-vous sûr de vouloir supprimer ? answer_accepted: >-

Nous ne recommandons pas de supprimer la réponse acceptée car cela prive les futurs lecteurs de cette connaissance.

La suppression répétée des réponses acceptées peut empêcher votre compte de répondre. Êtes-vous sûr de vouloir supprimer ? other: Êtes-vous sûr de vouloir supprimer ? tip_answer_deleted: Cette réponse a été supprimée undelete_title: Annuler la suppression de ce message undelete_desc: Êtes-vous sûr de vouloir annuler la suppression ? btns: confirm: Confimer cancel: Annuler edit: Modifier save: Enregistrer delete: Supprimer undelete: Annuler la suppression list: Liste unlist: Délister unlisted: Non listé login: Se connecter signup: S'inscrire logout: Se déconnecter verify: Vérifier create: Créer approve: Approuver reject: Rejeter skip: Ignorer discard_draft: Abandonner le brouillon pinned: Épinglé all: Tous question: Question answer: Réponse comment: Commentaire refresh: Actualiser resend: Renvoyer deactivate: Désactiver active: Actif suspend: Suspendre unsuspend: Lever la suspension close: Fermer reopen: Rouvrir ok: OK light: Clair dark: Sombre system_setting: Paramètres système default: Défaut reset: Réinitialiser tag: Étiquette post_lowercase: publier filter: Filtre ignore: Ignore submit: Soumettre normal: Normal closed: Fermé deleted: Supprimé deleted_permanently: Supprimé définitivement pending: En attente de traitement more: Plus view: Vue card: Carte compact: Compact display_below: Afficher dessous always_display: Toujours afficher or: ou back_sites: Retour aux sites search: title: Résultats de la recherche keywords: Mots-clés options: Options follow: Suivre following: Abonnements counts: "{{count}} Résultats" counts_loading: "... Results" more: Plus sort_btns: relevance: Pertinence newest: Les plus récents active: Actif score: Score more: Plus tips: title: Astuces de recherche avancée tag: "<1>[tag] recherche à l'aide d'un tag" user: "<1>utilisateur:username recherche par auteur" answer: "<1>réponses:0 questions sans réponses" score: "<1>score:3 messages avec plus de 3 points" question: "<1>est:question rechercher des questions" is_answer: "<1>est :réponse réponses de recherche" empty: Nous n'avons rien trouvé.
Essayez des mots-clés différents ou moins spécifiques. share: name: Partager copy: Copier le lien via: Partager via... copied: Copié facebook: Partager sur Facebook twitter: Partager sur X cannot_vote_for_self: Vous ne pouvez pas voter pour votre propre message. modal_confirm: title: Erreur... delete_permanently: title: Supprimer définitivement content: Êtes-vous sûr de vouloir supprimer définitivement ? account_result: success: Votre nouveau compte est confirmé; vous serez redirigé vers la page d'accueil. link: Continuer vers la page d'accueil oops: Oups ! invalid: Le lien que vous utilisez ne fonctionne plus. confirm_new_email: Votre adresse email a été mise à jour. confirm_new_email_invalid: >- Désolé, ce lien de confirmation n'est plus valide. Votre email est peut-être déjà modifié ? unsubscribe: page_title: Se désabonner success_title: Désabonnement réussi success_desc: Vous avez été supprimé de cette liste d'abonnés avec succès et ne recevrez plus d'e-mails. link: Modifier les paramètres question: following_tags: Hashtags suivis edit: Éditer save: Enregistrer follow_tag_tip: Suivez les tags pour organiser votre liste de questions. hot_questions: Questions populaires all_questions: Toutes les questions x_questions: "{{ count }} questions" x_answers: "{{ count }} réponses" x_posts: "{{ count }} Posts" questions: Questions answers: Réponses newest: Les plus récents active: Actif hot: Populaires frequent: Fréquent recommend: Recommandé score: Score unanswered: Sans réponse modified: modifié answered: répondu asked: demandé closed: fermé follow_a_tag: Suivre ce tag more: Plus personal: overview: Aperçu answers: Réponses answer: réponse questions: Questions question: question bookmarks: Favoris reputation: Réputation comments: Commentaires votes: Votes badges: Badges newest: Les plus récents score: Score edit_profile: Éditer le profil visited_x_days: "Visité {{ count }} jours" viewed: Vu joined: Inscrit comma: "," last_login: Vu about_me: À propos de moi about_me_empty: "// Hello, World !" top_answers: Les meilleures réponses top_questions: Questions les plus populaires stats: Statistiques list_empty: Aucune publication trouvée.
Peut-être souhaiteriez-vous sélectionner un autre onglet ? content_empty: Aucun post trouvé. accepted: Accepté answered: a répondu asked: a demandé downvoted: voté contre mod_short: MOD mod_long: Modérateurs x_reputation: réputation x_votes: votes reçus x_answers: réponses x_questions: questions recent_badges: Badges récents install: title: Installation next: Suivant done: Terminé config_yaml_error: Impossible de créer le fichier config.yaml. lang: label: Veuillez choisir une langue db_type: label: Moteur de base de données db_username: label: Nom d'utilisateur placeholder: root msg: Le nom d'utilisateur ne peut pas être vide. db_password: label: Mot de passe placeholder: root msg: Le mot de passe ne peut pas être vide. db_host: label: Hôte de la base de données placeholder: "db:3306" msg: L'hôte de la base de données ne peut pas être vide. db_name: label: Nom de la base de données placeholder: réponse msg: Le nom de la base de données ne peut pas être vide. db_file: label: Fichier de base de données placeholder: /data/answer.db msg: Le fichier de base de données ne doit pas être vide. ssl_enabled: label: Activer SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: Mode SSL ssl_root_cert: placeholder: Chemin du fichier sslrootcert msg: Le chemin vers le fichier sslrootcert ne peut pas être vide ssl_cert: placeholder: Chemin du fichier sslcert msg: Le chemin vers le fichier sslcert ne peut pas être vide ssl_key: placeholder: Chemin du fichier sslkey msg: Le chemin vers le fichier sslkey ne peut pas être vide config_yaml: title: Créer config.yaml label: Le fichier config.yaml a été créé. desc: >- Vous pouvez créer manuellement le fichier <1>config.yaml dans le répertoire <1>/var/wwww/xxx/ et y coller le texte suivant. info: Après avoir fini, cliquez sur le bouton "Suivant". site_information: Informations du site admin_account: Compte Admin site_name: label: Nom du site msg: Le nom ne peut pas être vide. msg_max_length: Le nom affiché doit avoir une longueur de 4 à 30 caractères. site_url: label: URL du site text: L'adresse de ce site. msg: empty: L'URL ne peut pas être vide. incorrect: Le format de l'URL est incorrect. max_length: L'URL du site doit avoir une longueur maximale de 512 caractères. contact_email: label: Email de contact text: L'adresse email du responsable du site. msg: empty: L'email de contact ne peut pas être vide. incorrect: Le format de l'email du contact est incorrect. login_required: label: Privé switch: Connexion requise text: Seuls les utilisateurs connectés peuvent accéder à cette communauté. admin_name: label: Nom msg: Le nom ne peut pas être vide. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: La longueur du nom doit être comprise entre 2 et 30 caractères. admin_password: label: Mot de passe text: >- Vous aurez besoin de ce mot de passe pour vous connecter . Sauvegarder le de façon sécurisée. msg: Le mot de passe ne peut pas être vide. msg_min_length: Le mot de passe doit comporter au moins 8 caractères. msg_max_length: Le mot de passe doit comporter au maximum 32 caractères. admin_confirm_password: label: "Répétez le mot de passe" text: "Veuillez saisir à nouveau votre mot de passe pour confirmer." msg: "Les mots de passe ne correspondent pas." admin_email: label: Email text: Vous aurez besoin de cet email pour vous connecter. msg: empty: L'email ne peut pas être vide. incorrect: Le format de l'email est incorrect. ready_title: Votre site est prêt ready_desc: >- Si vous avez envie de changer plus de paramètres, visitez la <1>section admin; retrouvez la dans le menu du site. good_luck: "Amusez-vous et bonne chance !" warn_title: Attention warn_desc: >- Le fichier <1>config.yaml existe déjà. Si vous avez besoin de réinitialiser l'un des éléments de configuration de ce fichier, veuillez le supprimer d'abord. install_now: Vous pouvez essayer de <1>l'installer maintenant. installed: Déjà installé installed_desc: >- Il semble que se soit déjà installer. Pour tout réinstaller, veuillez d'abord nettoyer votre ancienne base de données. db_failed: La connexion à la base de données a échoué db_failed_desc: >- Cela signifie que les informations de la base de données dans votre fichier <1>config.yaml est incorrect ou le contact avec le serveur de base de données n'a pas pu être établi. Cela pourrait signifier que le serveur de base de données de votre hôte est hors service. counts: views: vues votes: votes answers: réponses accepted: Accepté page_error: http_error: Erreur HTTP {{ code }} desc_403: Vous n'avez pas l'autorisation d'accéder à cette page. desc_404: Malheureusement, cette page n'existe pas. desc_50X: Le serveur a rencontré une erreur et n'a pas pu répondre à votre requête. back_home: Retour à la page d'accueil page_maintenance: desc: "Nous sommes en maintenance, nous serons bientôt de retour." nav_menus: dashboard: Tableau de bord contents: Contenus questions: Questions answers: Réponses users: Utilisateurs badges: Badges flags: Signalements settings: Paramètres general: Général interface: Interface smtp: SMTP branding: Marque legal: Légal write: Écrire terms: Terms tos: Conditions d'utilisation privacy: Confidentialité seo: SEO customize: Personnaliser themes: Thèmes login: Se connecter privileges: Privilèges plugins: Extensions installed_plugins: Extensions installées apperance: Apparence community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Bienvenue sur {{site_name}} user_center: login: Connexion qrcode_login_tip: Veuillez utiliser {{ agentName }} pour scanner le code QR et vous connecter. login_failed_email_tip: La connexion a échoué, veuillez autoriser cette application à accéder à vos informations de messagerie avant de réessayer. badges: modal: title: Félicitations content: Vous avez gagné un nouveau badge. close: Fermer confirm: Voir les badges title: Badges awarded: Octroyé earned_×: Gagné ×{{ number }} ×_awarded: "{{ number }} octroyés" can_earn_multiple: Vous pouvez gagner cela plusieurs fois. earned: Gagné admin: admin_header: title: Admin dashboard: title: Tableau de bord welcome: Bienvenue dans l'admin ! site_statistics: Statistiques du site questions: "Questions :" resolved: "Résolu :" unanswered: "Sans réponse :" answers: "Réponses :" comments: "Commentaires:" votes: "Votes :" users: "Utilisateurs :" flags: "Signalements:" reviews: "Revoir :" site_health: Etat du site version: "Version :" https: "HTTPS :" upload_folder: "Dossier de téléversement :" run_mode: "Mode de fonctionnement :" private: Privé public: Public smtp: "SMTP :" timezone: "Fuseau horaire :" system_info: Informations système go_version: "Version de Go :" database: "Base de donnée :" database_size: "Taille de la base de données :" storage_used: "Stockage utilisé :" uptime: "Uptime :" links: Liens plugins: Extensions github: GitHub blog: Blog contact: Contact forum: Forum documents: Documents feedback: Commentaires support: Support review: Vérifier config: Configuration update_to: Mise à jour vers latest: Récents check_failed: Vérification échouée "yes": "Oui" "no": "Non" not_allowed: Non autorisé allowed: Autorisé enabled: Activé disabled: Désactivé writable: Écriture autorisée not_writable: Écriture refusée flags: title: Signalements pending: En attente completed: Complété flagged: Signalé flagged_type: Signalé {{ type }} created: Créé action: Action review: Vérification user_role_modal: title: Changer le rôle d'un utilisateur en... btn_cancel: Annuler btn_submit: Valider new_password_modal: title: Définir un nouveau mot de passe form: fields: password: label: Mot de passe text: L'utilisateur sera déconnecté et devra se connecter à nouveau. msg: Le mot de passe doit contenir entre 8 et 32 caractères. btn_cancel: Annuler btn_submit: Envoyer edit_profile_modal: title: Éditer le profil form: fields: display_name: label: Nom affiché msg_range: Le nom d'affichage doit contenir entre 2 et 30 caractères. username: label: Nom d'utilisateur msg_range: Le nom d'utilisateur doit contenir entre 2 et 30 caractères. email: label: Email msg_invalid: Adresse e-mail non valide. edit_success: Modifié avec succès btn_cancel: Annuler btn_submit: Soumettre user_modal: title: Ajouter un nouvel utilisateur form: fields: users: label: Ajouter des utilisateurs en masse placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Séparez « nom, email, mot de passe » par des virgules. Un utilisateur par ligne. msg: "Veuillez entrer l'email de l'utilisateur, un par ligne." display_name: label: Nom affiché msg: Le nom affiché doit avoir une longueur de 2 à 30 caractères. email: label: Email msg: L'email n'est pas valide. password: label: Mot de passe msg: Le mot de passe doit comporter entre 8 et 32 caractères. btn_cancel: Annuler btn_submit: Valider users: title: Utilisateurs name: Nom email: E-mail reputation: Réputation created_at: Date de création delete_at: Date de suppression suspend_at: Date de suspension suspend_until: Suspend until status: Statut role: Rôle action: Action change: Modifier all: Tous staff: Staff more: Plus inactive: Inactif suspended: Suspendu deleted: Supprimé normal: Normal Moderator: Modérateur Admin: Administrateur User: Utilisateur filter: placeholder: "Filtrer par nom, utilisateur:id" set_new_password: Définir un nouveau mot de passe edit_profile: Éditer le profil change_status: Modifier le statut change_role: Modifier le rôle show_logs: Voir les logs add_user: Ajouter un utilisateur deactivate_user: title: Désactiver l'utilisateur content: Un utilisateur inactif doit revalider son email. delete_user: title: Supprimer cet utilisateur content: Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est définitive ! remove: Supprimer leur contenu label: Supprimer toutes les questions, réponses, commentaires, etc. text: Ne cochez pas cette case si vous souhaitez seulement supprimer le compte de l'utilisateur. suspend_user: title: Suspendre cet utilisateur content: Un utilisateur suspendu ne peut pas se connecter. label: How long will the user be suspended for? forever: Forever questions: page_title: Questions unlisted: Non listé post: Publication votes: Votes answers: Réponses created: Créé status: Statut action: Action change: Modifier pending: En attente de traitement filter: placeholder: "Filtrer par titre, question:id" answers: page_title: Réponses post: Publication votes: Votes created: Créé status: Statut action: Action change: Modifier filter: placeholder: "Filtrer par titre, question:id" general: page_title: Général name: label: Nom du site msg: Le nom ne peut pas être vide. text: "Le nom de ce site, tel qu'il est utilisé dans la balise titre." site_url: label: URL du site msg: L'URL ne peut pas être vide. validate: Indiquez une URL valide. text: L'adresse de ce site. short_desc: label: Courte description du site msg: La description courte ne peut pas être vide. text: "La description courte, telle qu'elle est utilisée dans le tag titre de la page d'accueil." desc: label: Description du site msg: La description du site ne peut pas être vide. text: "Décrivez ce site en une phrase, telle qu'elle est utilisée dans la balise meta description." contact_email: label: Email du contact msg: L'email de contact ne peut pas être vide. validate: L'email de contact n'est pas valide. text: L'adresse email du responsable du site. check_update: label: Mises à jour logicielles text: Rechercher automatiquement les mises à jour interface: page_title: Interface language: label: Langue de l'interface msg: La langue de l'interface ne peut pas être vide. text: Langue de l'interface de l'utilisateur. Cela changera lorsque vous rafraîchissez la page. time_zone: label: Fuseau Horaire msg: Le fuseau horaire ne peut pas être vide. text: Choisissez une ville dans le même fuseau horaire que vous. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: E-mail de l'expéditeur msg: L'email expéditeur ne peut pas être vide. text: L'adresse email à partir de laquelle les emails sont envoyés. from_name: label: Nom de l'expéditeur msg: Le nom expéditeur ne peut pas être vide. text: Le nom d'expéditeur à partir duquel les emails sont envoyés. smtp_host: label: Serveur SMTP msg: Le'hôte SMTP ne peut pas être vide. text: Votre serveur de mail. encryption: label: Chiffrement msg: Le chiffrement ne peut pas être vide. text: Pour la plupart des serveurs, l'option SSL est recommandée. ssl: SSL tls: TLS none: Aucun smtp_port: label: Port SMTP msg: Le port SMTP doit être compris entre 1 et 65535. text: Le port vers votre serveur d'email. smtp_username: label: Utilisateur SMTP msg: Le nom d'utilisateur SMTP ne peut pas être vide. smtp_password: label: Mot de passe SMTP msg: Le mot de passe SMTP ne peut être vide. test_email_recipient: label: Destinataires des e-mails de test text: Indiquez l'adresse email qui recevra l'email de test. msg: Le destinataire de l'email de test est invalide smtp_authentication: label: Activer l'authentification title: Authentification SMTP msg: L'authentification SMTP ne peut pas être vide. "yes": "Oui" "no": "Non" branding: page_title: Marque logo: label: Logo msg: Le logo ne peut pas être vide. text: L'image du logo en haut à gauche de votre site. Utilisez une grande image rectangulaire avec une hauteur de 56 et un ratio d'aspect supérieur à 3:1. Si laissé vide, le titre du site sera affiché. mobile_logo: label: Logo pour la version mobile text: Le logo utilisé sur la version mobile de votre site. Utilisez une image rectangulaire large avec une hauteur de 56. Si laissé vide, l'image du paramètre « logo » sera utilisée. square_icon: label: Icône carrée msg: L'icône carrée ne peut pas être vide. text: Image utilisée comme base pour les icônes de métadonnées. Idéalement supérieure à 512x512. favicon: label: Favicon text: Une favicon pour votre site. Pour fonctionner correctement sur un CDN, il doit s'agir d'un png. Sera redimensionné en 32x32. Si laissé vide, « icône carrée » sera utilisé. legal: page_title: Légal terms_of_service: label: Conditions d’utilisation text: "Vous pouvez ajouter le contenu des conditions de service ici. Si vous avez déjà un document hébergé ailleurs, veuillez fournir l'URL complète ici." privacy_policy: label: Protection des données text: "Vous pouvez ajouter le contenu des conditions de service ici. Si vous avez déjà un document hébergé ailleurs, veuillez fournir l'URL complète ici." external_content_display: label: Contenu externe text: "Le contenu comprend des images, des vidéos et des médias intégrés à partir de sites web externes." always_display: Toujours afficher le contenu externe ask_before_display: Demander avant d'afficher le contenu externe write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Écriture de la réponse label: Chaque utilisateur ne peut écrire qu'une seule réponse pour chaque question text: "Désactivez pour permettre aux utilisateurs d'écrire plusieurs réponses à la même question, ce qui peut causer une perte de concentration des réponses." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Tags recommandés text: "Les balises recommandées apparaîtront par défaut dans la liste déroulante." msg: contain_reserved: "les tags recommandés ne peuvent pas contenir de tags réservés" required_tag: title: Définir les tags nécessaires label: Définir les balises « Recommander» comme balises requises text: "Chaque nouvelle question doit avoir au moins un tag recommandé." reserved_tags: label: Tags réservés text: "Les tags réservés ne peuvent être ajoutés à un message que par un modérateur." image_size: label: Taille maximale de l'image (MB) text: "La taille maximale de téléchargement d'image." attachment_size: label: Taille maximale des pièces jointes (MB) text: "La taille maximale de téléchargement des fichiers joints." image_megapixels: label: Max mégapixels image text: "Nombre maximum de mégapixels autorisés pour une image." image_extensions: label: Extensions de pièces jointes autorisées text: "Une liste d'extensions de fichier autorisées pour l'affichage d'image, séparées par des virgules." attachment_extensions: label: Extensions de pièces jointes autorisées text: "Une liste d'extensions de fichier autorisées pour le téléchargement, séparées par des virgules. ATTENTION : Autoriser les envois peut causer des problèmes de sécurité." seo: page_title: Référencement permalink: label: Lien permanent text: Des structures d'URL personnalisées peuvent améliorer la facilité d'utilisation et la compatibilité de vos liens. robots: label: robots.txt text: Ceci remplacera définitivement tous les paramètres liés au site. themes: page_title: Thèmes themes: label: Thèmes text: Sélectionne un thème existant. color_scheme: label: Jeu de couleurs navbar_style: label: Style d'arrière-plan de la barre de navigation primary_color: label: Couleur primaire text: Modifier les couleurs utilisées par vos thèmes layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS et HTML custom_css: label: CSS personnalisé text: > head: label: Head text: > header: label: En-tête text: > footer: label: Pied de page text: Ceci va être inséré avant </html>. sidebar: label: Panneau latéral text: Cela va être inséré dans la barre latérale. login: page_title: Se connecter membership: title: Adhésion label: Autoriser les inscriptions text: Désactivez pour empêcher quiconque de créer un nouveau compte. email_registration: title: Inscription par e-mail label: Autoriser l'inscription par e-mail text: Désactiver pour empêcher toute personne de créer un nouveau compte par e-mail. allowed_email_domains: title: Domaines d'email autorisés text: Domaines de messagerie avec lesquels les utilisateurs peuvent créer des comptes. Un domaine par ligne. Ignoré si vide. private: title: Privé label: Connexion requise text: Seuls les utilisateurs connectés peuvent accéder à cette communauté. password_login: title: Connexion par mot de passe label: Autoriser la connexion par e-mail et mot de passe text: "AVERTISSEMENT : Si cette option est désactivée, vous ne pourrez peut-être pas vous connecter si vous n'avez pas configuré une autre méthode de connexion." installed_plugins: title: Extensions installées plugin_link: Les plugins étendent les fonctionnalités d'Answer. Vous pouvez trouver des plugins dans le dépôt <1>Answer Plugin Repositor. filter: all: Tous active: Actif inactive: Inactif outdated: Est obsolète plugins: label: Extensions text: Sélectionnez une extension existante. name: Nom version: Versión status: Statut action: Action deactivate: Désactiver activate: Activer settings: Paramètres settings_users: title: Utilisateurs avatar: label: Photo de profil par défaut text: Pour les utilisateurs sans avatar personnalisé. gravatar_base_url: label: Gravatar Base URL text: URL de la base de l'API du fournisseur Gravatar. Ignorée lorsqu'elle est vide. profile_editable: title: Profil modifiable allow_update_display_name: label: Permettre aux utilisateurs de changer leur nom d'affichage allow_update_username: label: Permettre aux clients de changer leurs noms d'utilisateur allow_update_avatar: label: Permettre aux utilisateurs de changer leur image de profil allow_update_bio: label: Permettre aux utilisateurs de changer leur biographie allow_update_website: label: Permettre aux utilisateurs de modifier leur site web allow_update_location: label: Permettre aux utilisateurs de modifier leur position privilege: title: Privilèges level: label: Niveau de réputation requis text: Choisissez la réputation requise pour les privilèges msg: should_be_number: l'entrée doit être un nombre number_larger_1: le nombre doit être égal ou supérieur à 1 badges: action: Action active: Actif activate: Activer all: Tous awards: Récompenses deactivate: Désactiver filter: placeholder: Filtrer par nom, badge:id group: Groupe inactive: Inactif name: Nom show_logs: Voir les logs status: Statut title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optionnel) empty: ne peut pas être vide invalid: est invalide btn_submit: Sauvegarder not_found_props: "La propriété requise {{ key }} est introuvable." select: Sélectionner page_review: review: Vérifier proposed: proposé question_edit: Modifier la question answer_edit: Modifier la réponse tag_edit: Modifier le tag edit_summary: Modifier le résumé edit_question: Modifier la question edit_answer: Modifier la réponse edit_tag: Modifier l’étiquette empty: Aucune révision restante. approve_revision_tip: Acceptez-vous cette révision? approve_flag_tip: Acceptez-vous ce rapport ? approve_post_tip: Acceptez-vous ce post? approve_user_tip: Acceptez-vous cet utilisateur ? suggest_edits: Modifications suggérées flag_post: Signaler ce message flag_user: Signaler un utilisateur queued_post: Message en file d'attente queued_user: Utilisateur en file d'attente filter_label: Type reputation: réputation flag_post_type: A signalé ce message comme {{ type }}. flag_user_type: A signalé cet utilisateur comme {{ type }}. edit_post: Éditer le post list_post: Lister le post unlist_post: Masquer la post de la liste timeline: undeleted: restauré deleted: supprimé downvote: vote négatif upvote: voter pour accept: accepté cancelled: annulé commented: commenté rollback: Retour arrière (Rollback) edited: modifié answered: répondu asked: demandé closed: fermé reopened: réouvert created: créé pin: épinglé unpin: non épinglé show: listé hide: non listé title: "Historique de" tag_title: "Chronologie de" show_votes: "Afficher les votes" n_or_a: N/A title_for_question: "Chronologie de" title_for_answer: "Chronologie de la réponse à {{ title }} par {{ author }}" title_for_tag: "Chronologie pour le tag" datetime: Date et heure type: Type by: Par comment: Commentaire no_data: "Nous n'avons rien pu trouver." users: title: Utilisateurs users_with_the_most_reputation: Utilisateurs ayant le score de réputation le plus élevé cette semaine users_with_the_most_vote: Utilisateurs qui ont le plus voté cette semaine staffs: Staff de la communauté reputation: réputation votes: votes prompt: leave_page: Voulez-vous vraiment quitter la page ? changes_not_save: Impossible d'enregistrer vos modifications. draft: discard_confirm: Êtes-vous sûr de vouloir abandonner ce brouillon ? messages: post_deleted: Ce message a été supprimé. post_cancel_deleted: Ce post a été restauré. post_pin: Ce message a été épinglé. post_unpin: Ce message a été déépinglé. post_hide_list: Ce message a été masqué de la liste. post_show_list: Ce message a été affiché dans la liste. post_reopen: Ce message a été rouvert. post_list: Ce post a été ajouté à la liste. post_unlist: Ce post a été retiré de la liste. post_pending: Votre message est en attente de révision. C'est un aperçu, il sera visible une fois qu'il aura été approuvé. post_closed: Ce post a été fermé. answer_deleted: Cette réponse a été supprimée. answer_cancel_deleted: Cette réponse a été restaurée. change_user_role: Le rôle de cet utilisateur a été modifié. user_inactive: Cet utilisateur est déjà inactif. user_normal: Cet utilisateur est déjà normal. user_suspended: Cet utilisateur a été suspendu. user_deleted: Cet utilisateur a été supprimé. user_added: User has been added successfully. badge_activated: Ce badge a été activé. badge_inactivated: Ce badge a été désactivé. users_deleted: Ces utilisateurs ont été supprimés. posts_deleted: Ces questions ont été supprimées. answers_deleted: Ces réponses ont été supprimées. copy: Copier dans le presse-papier copied: Copié external_content_warning: Les images/médias externes ne sont pas affichés. ================================================ FILE: i18n/he_IL.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. email: other: Email password: other: Password email_or_password_wrong_error: other: Email and password do not match. error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: not_found: other: Tag not found. recommend_tag_not_found: other: Recommend Tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: other: The From Name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to Revision. user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. report: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude: name: other: rude or abusive desc: other: A reasonable person would find this content inappropriate for respectful discourse. duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. not_answer: name: other: not an answer desc: other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. not_need: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. other: name: other: something else desc: other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comments tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to {{site_name}} btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to {{site_name}} success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/hi_IN.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. forbidden_error: other: Forbidden. duplicate_request_error: other: Duplicate submission. action: report: other: Flag edit: other: Edit delete: other: Delete close: other: Close reopen: other: Reopen forbidden_error: other: Forbidden. pin: other: Pin hide: other: Unlist unpin: other: Unpin show: other: List invite_someone_to_answer: other: Edit undelete: other: Undelete merge: other: Merge role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. privilege: level_1: description: other: Level 1 (less reputation required for private team, group) level_2: description: other: Level 2 (low reputation required for startup community) level_3: description: other: Level 3 (high reputation required for mature community) level_custom: description: other: Custom Level rank_question_add_label: other: Ask question rank_answer_add_label: other: Write answer rank_comment_add_label: other: Write comment rank_report_add_label: other: Flag rank_comment_vote_up_label: other: Upvote comment rank_link_url_limit_label: other: Post more than 2 links at a time rank_question_vote_up_label: other: Upvote question rank_answer_vote_up_label: other: Upvote answer rank_question_vote_down_label: other: Downvote question rank_answer_vote_down_label: other: Downvote answer rank_invite_someone_to_answer_label: other: Invite someone to answer rank_tag_add_label: other: Create new tag rank_tag_edit_label: other: Edit tag description (need to review) rank_question_edit_label: other: Edit other's question (need to review) rank_answer_edit_label: other: Edit other's answer (need to review) rank_question_edit_without_review_label: other: Edit other's question without review rank_answer_edit_without_review_label: other: Edit other's answer without review rank_question_audit_label: other: Review question edits rank_answer_audit_label: other: Review answer edits rank_tag_audit_label: other: Review tag edits rank_tag_edit_without_review_label: other: Edit tag description without review rank_tag_synonym_label: other: Manage tag synonyms email: other: Email e_mail: other: Email password: other: Password pass: other: Password old_pass: other: Current password original_text: other: This post email_or_password_wrong_error: other: Email and password do not match. error: common: invalid_url: other: Invalid URL. status_invalid: other: Invalid status. password: space_invalid: other: Password cannot contain spaces. admin: cannot_update_their_password: other: You cannot modify your password. cannot_edit_their_profile: other: You cannot modify your profile. cannot_modify_self_status: other: You cannot modify your status. email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. question_closed_cannot_add: other: Questions are closed and cannot be added. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. illegal_email_domain_error: other: Email is not allowed from that email domain. Please use another one. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. already_deleted: other: This post has been deleted. meta: object_not_found: other: Meta object not found question: already_deleted: other: This post has been deleted. under_review: other: Your post is awaiting review. It will be visible after it has been approved. not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. vote_fail_to_meet_the_condition: other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. no_enough_rank_to_operate: other: You need at least {{.Rank}} reputation to do this. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: already_exist: other: Tag already exists. not_found: other: Tag not found. recommend_tag_not_found: other: Recommend tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. is_used_cannot_delete: other: You cannot delete a tag that is in use. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: The from name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to revise. user: external_login_missing_user_id: other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. external_login_unbinding_forbidden: other: Please set a login password for your account before you remove this login. email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration. not_allowed_login_via_password: other: Currently the site is not allowed to login via password. access_denied: other: Access denied page_access_denied: other: You do not have access to this page. add_bulk_users_format_error: other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. site_info: config_not_found: other: Site config not found. badge: object_not_found: other: Badge object not found reason: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude_or_abusive: name: other: rude or abusive desc: other: "A reasonable person would find this content inappropriate for respectful discourse." a_duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. placeholder: other: Enter the existing question link not_a_answer: name: other: not an answer desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." no_longer_needed: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. something: name: other: something else desc: other: This post requires staff attention for another reason not listed above. placeholder: other: Let us know specifically what you are concerned about community_specific: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. not_clarity: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. looks_ok: name: other: looks OK desc: other: This post is good as-is and not low quality. needs_edit: name: other: needs edit, and I did it desc: other: Improve and correct problems with this post yourself. needs_close: name: other: needs close desc: other: A closed question can't answer, but still can edit, vote and comment. needs_delete: name: other: needs delete desc: other: This post will be deleted. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified deleted_title: other: Deleted question questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted up_voted_question: other: upvoted question down_voted_question: other: downvoted question up_voted_answer: other: upvoted answer down_voted_answer: other: downvoted answer up_voted_comment: other: upvoted comment invited_you_to_answer: other: invited you to answer earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "[{{.SiteName}}] Confirm your new email address" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Password reset" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Confirm your new account" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Test Email" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: upvote upvoted: other: upvoted downvote: other: downvote downvoted: other: downvoted accept: other: accept accepted: other: accepted edit: other: edit review: queued_post: other: Queued post flagged_post: other: Flagged post suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: Editor desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki create_tag: Create Tag edit_tag: Edit Tag ask_a_question: Create Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users oauth_callback: Processing http_404: HTTP Error 404 http_50X: HTTP Error 500 http_403: HTTP Error 403 logout: Log Out posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notifications inbox: Inbox achievement: Achievements new_alerts: New alerts all_read: Mark all as read show_more: Show more someone: Someone inbox_type: all: All posts: Posts invites: Invites votes: Votes answer: Answer question: Question badge_award: Badge suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. contact_us: Contact us editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image file btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed {{size}} MB. desc: label: Description tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered list unordered_list: text: Bulleted list table: text: Table heading: Heading cell: Cell file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: Create new tag form: fields: display_name: label: Display name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description revision: label: Revision edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_cancel: Cancel btn_submit: Submit btn_post: Post new tag tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Are you sure you wish to delete? close: Close merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Edit Tag default_reason: Edit tag default_first_reason: Add tag btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day hours: hours days: days month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: "{{count}} more comments" tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. tip_vote: It adds something useful to the post edit_answer: title: Edit Answer default_reason: Edit answer default_first_reason: Add answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: Newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More wiki: Wiki ask: title: Create Question edit_title: Edit Question default_reason: Edit question default_first_reason: Create question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: What's your topic? Be specific. msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users badges: Badges profile: Profile setting: Settings logout: Log out admin: Admin review: Review bookmark: Bookmarks moderation: Moderation search: placeholder: Search footer: build_on: Powered by <1> Apache Answer upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. resend_email: url_label: Are you sure you want to resend the activation email? url_text: You can also give the activation link above to the user. login: login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New email msg: empty: Email cannot be empty. oauth: connect: Connect with {{ auth_name }} remove: Remove {{ auth_name }} oauth_bind_email: subtitle: Add a recovery email to your account. btn_update: Update email address email: label: Email msg: empty: Email cannot be empty. modal_title: Email already existes. modal_content: This email address already registered. Are you sure you want to connect to the existing account? modal_cancel: Change email modal_confirm: Connect to the existing account password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm new password settings: page_title: Settings goto_modify: Go to modify nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile image gravatar: Gravatar gravatar_text: You can change image on custom: Custom custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About me website: label: Website placeholder: "https://example.com" msg: Website incorrect format location: label: Location placeholder: "City, Country" notification: heading: Email Notifications turn_on: Turn on inbox: label: Inbox notifications description: Answers to your questions, comments, invites, and more. all_new_question: label: All new questions description: Get notified of all new questions. Up to 50 questions per week. all_new_question_for_following_tags: label: All new questions for following tags description: Get notified of new questions for following tags. account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. pass: label: Current password msg: Password cannot be empty. password_title: Password current_pass: label: Current password msg: empty: Current password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New password pass_confirm: label: Confirm new password interface: heading: Interface lang: label: Interface language text: User interface language. It will change when you refresh the page. my_logins: title: My logins label: Log in or sign up on this site using these accounts. modal_title: Remove login modal_content: Are you sure you want to remove this login from your account? modal_confirm_btn: Remove remove_success: Removed successfully toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. sent_success: Sent successfully related_question: title: Related answers: answers linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: People Asked desc: Select people who you think might know the answer. invite: Invite to answer add: Add people search: Search people question_detail: action: Action created: Created Asked: Asked asked: asked update: Modified Edited: Edited edit: edited commented: commented Views: Viewed Follow: Follow Following: Following follow_tip: Follow this question to receive notifications answered: answered closed_in: Closed in show_exist: Show existing question. useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: title: Answers score: Score newest: Newest oldest: Oldest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer edit_answer: Edit my existing answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. tips: header_1: Thanks for your answer li1_1: Please be sure to answer the question. Provide details and share your research. li1_2: Back up any statements you make with references or personal experience. header_2: But avoid ... li2_1: Asking for help, seeking clarification, or responding to other answers. reopen: confirm_btn: Reopen title: Reopen this post content: Are you sure you want to reopen? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. confirm_btn: Pin delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_answer_deleted: This answer has been deleted undelete_title: Undelete this post undelete_desc: Are you sure you wish to undelete? btns: confirm: Confirm cancel: Cancel edit: Edit save: Save delete: Delete undelete: Undelete list: List unlist: Unlist unlisted: Unlisted login: Log in signup: Sign up logout: Log out verify: Verify create: Create approve: Approve reject: Reject skip: Skip discard_draft: Discard draft pinned: Pinned all: All question: Question answer: Answer comment: Comment refresh: Refresh resend: Resend deactivate: Deactivate active: Active suspend: Suspend unsuspend: Unsuspend close: Close reopen: Reopen ok: OK light: Light dark: Dark system_setting: System setting default: Default reset: Reset tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" counts_loading: "... Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Error... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage oops: Oops! invalid: The link you used no longer works. confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" x_posts: "{{ count }} Posts" questions: Questions answers: Answers newest: Newest active: Active hot: Hot frequent: Frequent recommend: Recommend score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes badges: Badges newest: Newest score: Score edit_profile: Edit profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined comma: "," last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? content_empty: No posts found. accepted: Accepted answered: answered asked: asked downvoted: downvoted mod_short: MOD mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions recent_badges: Recent Badges install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please choose a language db_type: label: Database engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database host placeholder: "db:3306" msg: Database host cannot be empty. db_name: label: Database name placeholder: answer msg: Database name cannot be empty. db_file: label: Database file placeholder: /data/answer.db msg: Database file cannot be empty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site name msg: Site name cannot be empty. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. max_length: Site URL must be at maximum 512 characters in length. contact_email: label: Contact email text: Email address of key contact responsible for this site. msg: empty: Contact email cannot be empty. incorrect: Contact email incorrect format. login_required: label: Private switch: Login required text: Only logged in users can access this community. admin_name: label: Name msg: Name cannot be empty. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_error: http_error: HTTP Error {{ code }} desc_403: You don't have permission to access this page. desc_404: Unfortunately, this page doesn't exist. desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users badges: Badges flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write terms: Terms tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes login: Login privileges: Privileges plugins: Plugins installed_plugins: Installed Plugins apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Welcome to {{site_name}} user_center: login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site statistics questions: "Questions:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Answers:" comments: "Comments:" votes: "Votes:" users: "Users:" flags: "Flags:" reviews: "Reviews:" site_health: Site health version: "Version:" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Private public: Public smtp: "SMTP:" timezone: "Timezone:" system_info: System info go_version: "Go version:" database: "Database:" database_size: "Database size:" storage_used: "Storage used:" uptime: "Uptime:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Contact forum: Forum documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled writable: Writable not_writable: Not writable flags: title: Flags pending: Pending completed: Completed flagged: Flagged flagged_type: Flagged {{ type }} created: Created action: Action review: Review user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: users: label: Bulk add user placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separate “name, email, password” with commas. One user per line. msg: "Please enter the user's email, one per line." display_name: label: Display name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Status role: Role action: Action change: Change all: All staff: Staff more: More inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password edit_profile: Edit profile change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user deactivate_user: title: Deactivate user content: An inactive user must re-validate their email. delete_user: title: Delete this user content: Are you sure you want to delete this user? This is permanent! remove: Remove their content label: Remove all questions, answers, comments, etc. text: Don’t check this if you wish to only delete the user’s account. suspend_user: title: Suspend this user content: A suspended user can't log in. label: How long will the user be suspended for? forever: Forever questions: page_title: Questions unlisted: Unlisted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change pending: Pending filter: placeholder: "Filter by title, question:id" answers: page_title: Answers post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short site description msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site description msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. check_update: label: Software updates text: Automatically check for updates interface: page_title: Interface language: label: Interface language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: From email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL tls: TLS none: None smtp_port: label: SMTP port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP username msg: SMTP username cannot be empty. smtp_password: label: SMTP password msg: SMTP password cannot be empty. test_email_recipient: label: Test email recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile logo text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square icon msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for each question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. color_scheme: label: Color scheme navbar_style: label: Navbar background style primary_color: label: Primary color text: Modify the colors used by your themes layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: > head: label: Head text: > header: label: Header text: > footer: label: Footer text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: Allowed email domains text: Email domains that users must register accounts with. One domain per line. Ignored when empty. private: title: Private label: Login required text: Only logged in users can access this community. password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: All active: Active inactive: Inactive outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Name version: Version status: Status action: Action deactivate: Deactivate activate: Activate settings: Settings settings_users: title: Users avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Allow users to change their display name allow_update_username: label: Allow users to change their username allow_update_avatar: label: Allow users to change their profile image allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optional) empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." select: Select page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created pin: pinned unpin: unpinned show: listed hide: unlisted title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: Our community staff reputation: reputation votes: votes prompt: leave_page: Are you sure you want to leave the page? changes_not_save: Your changes may not be saved. draft: discard_confirm: Are you sure you want to discard your draft? messages: post_deleted: This post has been deleted. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/hu_HU.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. email: other: Email password: other: Password email_or_password_wrong_error: other: Email and password do not match. error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: not_found: other: Tag not found. recommend_tag_not_found: other: Recommend Tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: other: The From Name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to Revision. user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. report: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude: name: other: rude or abusive desc: other: A reasonable person would find this content inappropriate for respectful discourse. duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. not_answer: name: other: not an answer desc: other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. not_need: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. other: name: other: something else desc: other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comments tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to {{site_name}} btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to {{site_name}} success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/hy_AM.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: "Success." unknown: other: "Unknown error." request_format_error: other: "Request format is not valid." unauthorized_error: other: "Unauthorized." database_error: other: "Data server error." role: name: user: other: "User" admin: other: "Admin" moderator: other: "Moderator" description: user: other: "Default with no special access." admin: other: "Have the full power to access the site." moderator: other: "Has access to all posts except admin settings." email: other: "Email" password: other: "Password" email_or_password_wrong_error: other: "Email and password do not match." error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: "Answer do not found." cannot_deleted: other: "No permission to delete." cannot_update: other: "No permission to update." comment: edit_without_permission: other: "Comment are not allowed to edit." not_found: other: "Comment not found." email: duplicate: other: "Email already exists." need_to_be_verified: other: "Email should be verified." verify_url_expired: other: "Email verified URL has expired, please resend the email." lang: not_found: other: "Language file not found." object: captcha_verification_failed: other: "Captcha wrong." disallow_follow: other: "You are not allowed to follow." disallow_vote: other: "You are not allowed to vote." disallow_vote_your_self: other: "You can't vote for your own post." not_found: other: "Object not found." verification_failed: other: "Verification failed." email_or_password_incorrect: other: "Email and password do not match." old_password_verification_failed: other: "The old password verification failed" new_password_same_as_previous_setting: other: "The new password is the same as the previous one." question: not_found: other: "Question not found." cannot_deleted: other: "No permission to delete." cannot_close: other: "No permission to close." cannot_update: other: "No permission to update." rank: fail_to_meet_the_condition: other: "Rank fail to meet the condition." report: handle_failed: other: "Report handle failed." not_found: other: "Report not found." tag: not_found: other: "Tag not found." recommend_tag_not_found: other: "Recommend Tag is not exist." recommend_tag_enter: other: "Please enter at least one required tag." not_contain_synonym_tags: other: "Should not contain synonym tags." cannot_update: other: "No permission to update." cannot_set_synonym_as_itself: other: "You cannot set the synonym of the current tag as itself." smtp: config_from_name_cannot_be_email: other: "The From Name cannot be a email address." theme: not_found: other: "Theme not found." revision: review_underway: other: "Can't edit currently, there is a version in the review queue." no_permission: other: "No permission to Revision." user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: "User not found." suspended: other: "User has been suspended." username_invalid: other: "Username is invalid." username_duplicate: other: "Username is already in use." set_avatar: other: "Avatar set failed." cannot_update_your_role: other: "You cannot modify your role." not_allowed_registration: other: "Currently the site is not open for registration" config: read_config_failed: other: "Read config failed" database: connection_failed: other: "Database connection failed" create_table_failed: other: "Create table failed" install: create_config_failed: other: "Can't create the config.yaml file." report: spam: name: other: "spam" desc: other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." rude: name: other: "rude or abusive" desc: other: "A reasonable person would find this content inappropriate for respectful discourse." duplicate: name: other: "a duplicate" desc: other: "This question has been asked before and already has an answer." not_answer: name: other: "not an answer" desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." not_need: name: other: "no longer needed" desc: other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" desc: other: "This post requires staff attention for another reason not listed above." question: close: duplicate: name: other: "spam" desc: other: "This question has been asked before and already has an answer." guideline: name: other: "a community-specific reason" desc: other: "This question doesn't meet a community guideline." multiple: name: other: "needs details or clarity" desc: other: "This question currently includes multiple questions in one. It should focus on one problem only." other: name: other: "something else" desc: other: "This post requires another reason not listed above." operation_type: asked: other: "asked" answered: other: "answered" modified: other: "modified" notification: action: update_question: other: "updated question" answer_the_question: other: "answered question" update_answer: other: "updated answer" accept_answer: other: "accepted answer" comment_question: other: "commented question" comment_answer: other: "commented answer" reply_to_you: other: "replied to you" mention_you: other: "mentioned you" your_question_is_closed: other: "Your question has been closed" your_question_was_deleted: other: "Your question has been deleted" your_answer_was_deleted: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comment tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to Answer btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name up to 30 characters username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username up to 30 characters character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to Answer success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: "After you've done that, click “Next” button." site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: display_name must be at 2 - 30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/i18n.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package i18n import "embed" //go:embed *.yaml var I18n embed.FS ================================================ FILE: i18n/i18n.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # all support language language_options: - label: "English" value: "en_US" progress: 100 - label: "Español" value: "es_ES" progress: 96 - label: "Português(BR)" value: "pt_BR" progress: 96 - label: "Português" value: "pt_PT" progress: 96 - label: "Deutsch" value: "de_DE" progress: 96 - label: "Français" value: "fr_FR" progress: 96 - label: "日本語" value: "ja_JP" progress: 96 - label: "Italiano" value: "it_IT" progress: 96 - label: "Русский" value: "ru_RU" progress: 80 - label: "简体中文" value: "zh_CN" progress: 100 - label: "繁體中文" value: "zh_TW" progress: 47 - label: "한국어" value: "ko_KR" progress: 73 - label: "Tiếng Việt" value: "vi_VN" progress: 96 - label: "Slovak" value: "sk_SK" progress: 45 - label: "فارسی" value: "fa_IR" progress: 69 ================================================ FILE: i18n/id_ID.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Sukses. unknown: other: Kesalahan tidak diketahui. request_format_error: other: Permintaan tidak sah. unauthorized_error: other: Tidak diizinkan. database_error: other: Kesalahan data server. forbidden_error: other: Forbidden. duplicate_request_error: other: Duplicate submission. action: report: other: Flag edit: other: Ubah delete: other: Hapus close: other: Tutup reopen: other: Buka kembali forbidden_error: other: Forbidden. pin: other: Pin hide: other: Unlist unpin: other: Unpin show: other: Daftar invite_someone_to_answer: other: Undang seseorang untuk menjawab undelete: other: Undelete merge: other: Merge role: name: user: other: Pengguna admin: other: Administrator moderator: other: Moderator description: user: other: Default tanpa akses khusus. admin: other: Memiliki kontrol penuh atas situs. moderator: other: Memiliki kendali atas semua kiriman kecuali pengaturan administrator. privilege: level_1: description: other: Level 1 (less reputation required for private team, group) level_2: description: other: Level 2 (low reputation required for startup community) level_3: description: other: Level 3 (high reputation required for mature community) level_custom: description: other: Custom Level rank_question_add_label: other: Tanyakan sesuatu rank_answer_add_label: other: Tulis jawaban rank_comment_add_label: other: Tulis komentar rank_report_add_label: other: Flag rank_comment_vote_up_label: other: Upvote comment rank_link_url_limit_label: other: Kirim lebih dari 2 tautan secara bersamaan rank_question_vote_up_label: other: Upvote question rank_answer_vote_up_label: other: Upvote answer rank_question_vote_down_label: other: Downvote question rank_answer_vote_down_label: other: Downvote answer rank_invite_someone_to_answer_label: other: Undang seseorang untuk menjawab rank_tag_add_label: other: Buat tag baru rank_tag_edit_label: other: Edit tag description (need to review) rank_question_edit_label: other: Edit other's question (need to review) rank_answer_edit_label: other: Edit other's answer (need to review) rank_question_edit_without_review_label: other: Edit other's question without review rank_answer_edit_without_review_label: other: Edit other's answer without review rank_question_audit_label: other: Review question edits rank_answer_audit_label: other: Review answer edits rank_tag_audit_label: other: Review tag edits rank_tag_edit_without_review_label: other: Edit tag description without review rank_tag_synonym_label: other: Manage tag synonyms email: other: Email e_mail: other: Email password: other: Kata sandi pass: other: Password old_pass: other: Current password original_text: other: This post email_or_password_wrong_error: other: '"Email" dan kata sandi tidak cocok.' error: common: invalid_url: other: URL salah. status_invalid: other: Invalid status. password: space_invalid: other: Password tidak boleh mengandung spasi. admin: cannot_update_their_password: other: You cannot modify your password. cannot_edit_their_profile: other: You cannot modify your profile. cannot_modify_self_status: other: You cannot modify your status. email_or_password_wrong: other: Email dan kata sandi tidak cocok. answer: not_found: other: Jawaban tidak ditemukan. cannot_deleted: other: Tidak memiliki izin untuk menghapus. cannot_update: other: Tidak memiliki izin untuk memperbarui. question_closed_cannot_add: other: Questions are closed and cannot be added. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Tidak diizinkan untuk mengubah komentar. not_found: other: Komentar tidak ditemukan. cannot_edit_after_deadline: other: The comment time has been too long to modify. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: Email telah terdaftar. need_to_be_verified: other: Email harus terverifikasi. verify_url_expired: other: URL verifikasi email telah kadaluwarsa, silahkan kirim ulang. illegal_email_domain_error: other: Email is not allowed from that email domain. Please use another one. lang: not_found: other: Bahasa tidak ditemukan. object: captcha_verification_failed: other: Captcha salah. disallow_follow: other: Anda tidak diizinkan untuk mengikuti. disallow_vote: other: Anda tisak diizinkan untuk melakukan vote. disallow_vote_your_self: other: Anda tidak dapat melakukan voting untuk ulasan Anda sendiri. not_found: other: Objek tidak ditemukan. verification_failed: other: Verifikasi gagal. email_or_password_incorrect: other: Email dan kata sandi tidak cocok. old_password_verification_failed: other: Verifikasi password lama, gagal new_password_same_as_previous_setting: other: Kata sandi baru sama dengan kata sandi yang sebelumnya. already_deleted: other: This post has been deleted. meta: object_not_found: other: Meta object not found question: already_deleted: other: This post has been deleted. under_review: other: Your post is awaiting review. It will be visible after it has been approved. not_found: other: Pertanyaan tidak ditemukan. cannot_deleted: other: Tidak memiliki izin untuk menghapus. cannot_close: other: Tidak diizinkan untuk menutup. cannot_update: other: Tidak diizinkan untuk memperbarui. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. vote_fail_to_meet_the_condition: other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. no_enough_rank_to_operate: other: You need at least {{.Rank}} reputation to do this. report: handle_failed: other: Laporan penanganan gagal. not_found: other: Laporan tidak ditemukan. tag: already_exist: other: Tag already exists. not_found: other: Tag tidak ditemukan. recommend_tag_not_found: other: Recommend tag is not exist. recommend_tag_enter: other: Silahkan isi setidaknya satu tag yang diperlukan. not_contain_synonym_tags: other: Tidak boleh mengandung Tag sinonim. cannot_update: other: Tidak memiliki izin untuk memperbaharui. is_used_cannot_delete: other: You cannot delete a tag that is in use. cannot_set_synonym_as_itself: other: Anda tidak bisa menetapkan sinonim dari tag saat ini dengan tag yang sama. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: The from name cannot be a email address. theme: not_found: other: Tema tidak ditemukan. revision: review_underway: other: Tidak dapat mengedit saat ini, sedang ada review versi pada antrian. no_permission: other: No permission to revise. user: external_login_missing_user_id: other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. external_login_unbinding_forbidden: other: Please set a login password for your account before you remove this login. email_or_password_wrong: other: other: Email dan kata sandi tidak cocok. not_found: other: Pengguna tidak ditemukan. suspended: other: Pengguna ini telah ditangguhkan. username_invalid: other: Nama pengguna tidak sesuai. username_duplicate: other: Nama pengguna sudah digunakan. set_avatar: other: Set avatar gagal. cannot_update_your_role: other: Anda tidak dapat mengubah peran anda sendiri. not_allowed_registration: other: Currently the site is not open for registration. not_allowed_login_via_password: other: Currently the site is not allowed to login via password. access_denied: other: Access denied page_access_denied: other: You do not have access to this page. add_bulk_users_format_error: other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Gagal membaca konfigurasi database: connection_failed: other: Koneksi ke database gagal create_table_failed: other: Gagal membuat tabel install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. site_info: config_not_found: other: Site config not found. badge: object_not_found: other: Badge object not found reason: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude_or_abusive: name: other: rude or abusive desc: other: "A reasonable person would find this content inappropriate for respectful discourse." a_duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. placeholder: other: Enter the existing question link not_a_answer: name: other: not an answer desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." no_longer_needed: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. something: name: other: something else desc: other: This post requires staff attention for another reason not listed above. placeholder: other: Let us know specifically what you are concerned about community_specific: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. not_clarity: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. looks_ok: name: other: looks OK desc: other: This post is good as-is and not low quality. needs_edit: name: other: needs edit, and I did it desc: other: Improve and correct problems with this post yourself. needs_close: name: other: needs close desc: other: A closed question can't answer, but still can edit, vote and comment. needs_delete: name: other: needs delete desc: other: This post will be deleted. question: close: duplicate: name: other: spam desc: other: Pertanyaan ini telah ditanyakan sebelumnya dan sudah ada jawabannya. guideline: name: other: a community-specific reason desc: other: Pertanyaan ini tidak sesuai dengan pedoman komunitas. multiple: name: other: membutuhkan detail atau kejelasan desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: lainnya desc: other: Posting ini membutuhkan alasan lain yang tidak tercantum di atas. operation_type: asked: other: ditanyakan answered: other: dijawab modified: other: dimodifikasi deleted_title: other: Deleted question questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: pertanyaan yang diperbaharui answer_the_question: other: pertanyaan yang dijawab update_answer: other: jawaban yang diperbaharui accept_answer: other: pertanyaan yanag diterima comment_question: other: pertanyaan yang dikomentari comment_answer: other: jawaban yang dikomentari reply_to_you: other: membalas Anda mention_you: other: menyebutmu your_question_is_closed: other: Pertanyaanmu telah ditutup your_question_was_deleted: other: Pertanyaanmu telah dihapus your_answer_was_deleted: other: Jawabanmu telah dihapus your_comment_was_deleted: other: Komentarmu telah dihapus up_voted_question: other: upvoted question down_voted_question: other: downvoted question up_voted_answer: other: upvoted answer down_voted_answer: other: downvoted answer up_voted_comment: other: upvoted comment invited_you_to_answer: other: invited you to answer earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "[{{.SiteName}}] Confirm your new email address" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Password reset" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Confirm your new account" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Test Email" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: upvote upvoted: other: upvoted downvote: other: downvote downvoted: other: downvoted accept: other: accept accepted: other: accepted edit: other: edit review: queued_post: other: Queued post flagged_post: other: Flagged post suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: Editor desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Cara memformat desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Sebelumnya next: Selanjutnya page_title: question: Pertanyaan questions: Pertanyaan tag: Tag tags: Tags tag_wiki: tag wiki create_tag: Create Tag edit_tag: Ubah Tag ask_a_question: Create Question edit_question: Sunting Pertanyaan edit_answer: Sunting jawaban search: Cari posts_containing: Postingan mengandung settings: Pengaturan notifications: Pemberitahuan login: Log In sign_up: Daftar account_recovery: Pemulihan Akun account_activation: Aktivasi Akun confirm_email: Konfirmasi email account_suspended: Akun Ditangguhkan admin: Admin change_email: Modifikasi email install: Instalasi Answer upgrade: Meng-upgrade Answer maintenance: Pemeliharaan Website users: Pengguna oauth_callback: Processing http_404: HTTP Error 404 http_50X: HTTP Error 500 http_403: HTTP Error 403 logout: Log Out posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Pemberitahuan inbox: Kotak Masuk achievement: Pencapaian new_alerts: New alerts all_read: Tandai Semua Jika Sudah Dibaca show_more: Tampilkan lebih banyak someone: Someone inbox_type: all: All posts: Posts invites: Invites votes: Votes answer: Answer question: Question badge_award: Badge suspended: title: Akun Anda telah ditangguhkan until_time: "Akun anda ditangguhkan sampai {{ time }}." forever: Pengguna ini ditangguhkan selamanya. end: Anda tidak sesuai dengan syarat pedoman komunitas. contact_us: Contact us editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Diagram alir sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Tambahkan sample code form: fields: code: label: Code msg: empty: Code tidak boleh kosong. language: label: Language placeholder: Deteksi otomatis btn_cancel: Batal btn_confirm: Tambah formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal rule image: text: Gambar add_image: Tambahkan gambar tab_image: Unggah gambar form_image: fields: file: label: Image file btn: Pilih gambar msg: empty: File tidak boleh kosong. only_image: Hanya file Gambar yang diperbolehkan. max_size: File size cannot exceed {{size}} MB. desc: label: Description tab_url: URL gambar form_url: fields: url: label: URL gambar msg: empty: URL gambar tidak boleh kosong. name: label: Description btn_cancel: Batal btn_confirm: Tambah uploading: Sedang mengunggah indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description btn_cancel: Batal btn_confirm: Tambah ordered_list: text: Numbered list unordered_list: text: Bulleted list table: text: Table heading: Heading cell: Cell file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: Postingan ini saya tutup sebagai... btn_cancel: Batal btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: Create new tag form: fields: display_name: label: Display name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description revision: label: Revision edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_cancel: Cancel btn_submit: Submit btn_post: Post new tag tag_info: created_at: Dibuat edited_at: Disunting history: Riwayat synonyms: title: Sinonim text: Tag berikut akan dipetakan ulang ke empty: Sinonim tidak ditemukan. btn_add: Tambahkan sinonim btn_edit: Sunting btn_save: Simpan synonyms_text: Tag berikut akan dipetakan ulang ke delete: title: Hapus tagar ini tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Are you sure you wish to delete? close: Tutup merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Ubah Tag default_reason: Sunting tag default_first_reason: Add tag btn_save_edits: Simpan suntingan btn_cancel: Batal dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: sekarang x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: jam day: hari hours: hours days: days month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Tambahkan Komentar reply_to: Balas ke btn_reply: Balas btn_edit: Sunting btn_delete: Hapus btn_flag: Flag btn_save_edits: Simpan suntingan btn_cancel: Batal show_more: "{{count}} more comments" tip_question: >- Gunakan komentar untuk meminta informasi lebih lanjut atau menyarankan perbaikan. Hindari menjawab pertanyaan di komentar. tip_answer: >- Gunakan komentar untuk membalas pengguna lain atau memberi tahu mereka tentang perubahan. Jika Anda menambahkan informasi baru, cukup edit posting Anda. tip_vote: It adds something useful to the post edit_answer: title: Sunting jawaban default_reason: Edit jawaban default_first_reason: Add answer form: fields: revision: label: Revisi answer: label: Jawaban feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: Newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More wiki: Wiki ask: title: Create Question edit_title: Edit Question default_reason: Edit question default_first_reason: Create question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: What's your topic? Be specific. msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Pengguna badges: Badges profile: Profil setting: Pengaturan logout: Keluar admin: Admin review: Ulasan bookmark: Bookmarks moderation: Moderation search: placeholder: Cari footer: build_on: Powered by <1> Apache Answer upload_img: name: Ubah loading: sedang memuat... pic_auth_code: title: Capthcha placeholder: Masukkan teks di atas msg: empty: Captcha tidak boleh kosong. inactive: first: >- Kamu hampir selesai! Kami telah mengirimkan email aktivasi ke {{mail}}. Silakan ikuti petunjuk dalam email untuk mengaktifkan akun Anda. info: "Jika tidak ada email masuk, mohon periksa folder spam Anda." another: >- Kami telah mengirimkan email aktivasi lain kepada Anda di {{mail}}. Mungkin butuh beberapa menit untuk tiba; pastikan untuk memeriksa folder spam Anda. btn_name: Kirim ulang email aktivasi change_btn_name: Ganti email msg: empty: Tidak bisa kosong. resend_email: url_label: Are you sure you want to resend the activation email? url_text: You can also give the activation link above to the user. login: login_to_continue: Masuk untuk melanjutkan info_sign: Belum punya akun? <1>Daftar info_login: Sudah punya akun? <1>Masuk agreements: Dengan mendaftar, Anda menyetujui <1>kebijakan privasi dan <3>persyaratan layanan. forgot_pass: Lupa password? name: label: Nama msg: empty: Nama tidak boleh kosong. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Email msg: empty: Email tidak boleh kosong. password: label: Kata sandi msg: empty: Kata sandi tidak boleh kosong. different: Kata sandi yang dimasukkan tidak sama account_forgot: page_title: Lupa kata sandi Anda btn_name: Tulis email pemulihan send_success: >- Jika akun cocok dengan {{mail}}, Anda akan segera menerima email berisi petunjuk tentang cara menyetel ulang sandi. email: label: Email msg: empty: Email tidak boleh kosong. change_email: btn_cancel: Batal btn_update: Perbarui alamat email send_success: >- Jika akun cocok dengan {{mail}}, Anda akan segera menerima email berisi petunjuk tentang cara menyetel ulang sandi. email: label: New email msg: empty: Email tidak boleh kosong. oauth: connect: Connect with {{ auth_name }} remove: Remove {{ auth_name }} oauth_bind_email: subtitle: Add a recovery email to your account. btn_update: Update email address email: label: Email msg: empty: Email cannot be empty. modal_title: Email already existes. modal_content: This email address already registered. Are you sure you want to connect to the existing account? modal_cancel: Change email modal_confirm: Connect to the existing account password_reset: page_title: Atur ulang kata sandi btn_name: Atur ulang kata sandi saya reset_success: >- Anda berhasil mengubah kata sandi Anda; Anda akan dialihkan ke halaman login. link_invalid: >- Maaf, link setel ulang sandi ini sudah tidak valid. Mungkin kata sandi Anda sudah diatur ulang? to_login: Lanjutkan ke halaman Login password: label: Kata sandi msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm new password settings: page_title: Settings goto_modify: Go to modify nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile image gravatar: Gravatar gravatar_text: You can change image on custom: Custom custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About me website: label: Website placeholder: "https://example.com" msg: Website incorrect format location: label: Location placeholder: "City, Country" notification: heading: Email Notifications turn_on: Turn on inbox: label: Inbox notifications description: Answers to your questions, comments, invites, and more. all_new_question: label: All new questions description: Get notified of all new questions. Up to 50 questions per week. all_new_question_for_following_tags: label: All new questions for following tags description: Get notified of new questions for following tags. account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. pass: label: Current password msg: Password cannot be empty. password_title: Password current_pass: label: Current password msg: empty: Current password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New password pass_confirm: label: Confirm new password interface: heading: Interface lang: label: Interface language text: Bahasa antarmuka pengguna. Itu akan berubah ketika Anda me-refresh halaman. my_logins: title: My logins label: Log in or sign up on this site using these accounts. modal_title: Remove login modal_content: Are you sure you want to remove this login from your account? modal_confirm_btn: Remove remove_success: Removed successfully toast: update: pembaruan sukses update_password: Kata sandi berhasil diganti. flag_success: Terima kasih telah menandai. forbidden_operate_self: Dilarang melakukan operasi ini pada diri sendiri review: Revisi Anda akan ditampilkan setelah ditinjau. sent_success: Sent successfully related_question: title: Related answers: jawaban linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: People Asked desc: Select people who you think might know the answer. invite: Invite to answer add: Add people search: Search people question_detail: action: Action created: Created Asked: Ditanyakan asked: ditanyakan update: Diubah Edited: Edited edit: disunting commented: commented Views: Dilihat Follow: Ikuti Following: Mengikuti follow_tip: Follow this question to receive notifications answered: dijawab closed_in: Ditutup pada show_exist: Gunakan pertanyaan yang sudah ada. useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: title: Jawaban score: Nilai newest: Terbaru oldest: Oldest btn_accept: Terima btn_accepted: Diterima write_answer: title: Jawaban Anda edit_answer: Edit my existing answer btn_name: Kirimkan jawaban Anda add_another_answer: Tambahkan jawaban lain confirm_title: Lanjutkan menjawab continue: Lanjutkan confirm_info: >-

Yakin ingin menambahkan jawaban lain?

Sebagai gantinya, Anda dapat menggunakan tautan edit untuk menyaring dan menyempurnakan jawaban anda.

empty: Jawaban tidak boleh kosong. characters: content must be at least 6 characters in length. tips: header_1: Thanks for your answer li1_1: Please be sure to answer the question. Provide details and share your research. li1_2: Back up any statements you make with references or personal experience. header_2: But avoid ... li2_1: Asking for help, seeking clarification, or responding to other answers. reopen: confirm_btn: Reopen title: Buka kembali postingan ini content: Kamu yakin ingin membuka kembali? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. confirm_btn: Pin delete: title: Hapus pos ini question: >- Kami tidak menyarankan menghapus pertanyaan dengan jawaban karena hal itu menghilangkan pengetahuan ini dari pembaca di masa mendatang.

Penghapusan berulang atas pertanyaan yang dijawab dapat mengakibatkan akun Anda diblokir untuk bertanya. Apakah Anda yakin ingin menghapus? answer_accepted: >-

Kami tidak menyarankan menghapus jawaban yang diterima karena hal itu menghilangkan pengetahuan ini dari pembaca di masa mendatang.

Penghapusan berulang dari jawaban yang diterima dapat menyebabkan akun Anda diblokir dari menjawab. Apakah Anda yakin ingin menghapus? other: Anda yakin ingin menghapusnya? tip_answer_deleted: Jawaban ini telah dihapus undelete_title: Undelete this post undelete_desc: Are you sure you wish to undelete? btns: confirm: Konfirmasi cancel: Batal edit: Edit save: Simpan delete: Hapus undelete: Undelete list: List unlist: Unlist unlisted: Unlisted login: Masuk signup: Daftar logout: Keluar verify: Verifikasi create: Create approve: Approve reject: Reject skip: Skip discard_draft: Discard draft pinned: Pinned all: All question: Question answer: Answer comment: Comment refresh: Refresh resend: Resend deactivate: Deactivate active: Active suspend: Suspend unsuspend: Unsuspend close: Close reopen: Reopen ok: OK light: Light dark: Dark system_setting: System setting default: Default reset: Reset tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" counts_loading: "... Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Error... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage oops: Oops! invalid: The link you used no longer works. confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" x_posts: "{{ count }} Posts" questions: Questions answers: Jawaban newest: Terbaru active: Aktif hot: Hot frequent: Frequent recommend: Recommend score: Nilai unanswered: Belum dijawab modified: diubah answered: dijawab asked: ditanyakan closed: ditutup follow_a_tag: Ikuti tagar more: Lebih personal: overview: Ringkasan answers: Jawaban answer: jawaban questions: Pertanyaan question: pertanyaan bookmarks: Bookmarks reputation: Reputasi comments: Komentar votes: Vote badges: Badges newest: Terbaru score: Nilai edit_profile: Edit profile visited_x_days: "Dikunjungi {{ count }} hari" viewed: Dilihat joined: Bergabung comma: "," last_login: Dilihat about_me: Tentang Saya about_me_empty: "// Hello, World !" top_answers: Jawaban terpopuler top_questions: Pertanyaan terpopuler stats: Statistik list_empty: Postingan tidak ditemukan.
Mungkin Anda ingin memilih tab lain? content_empty: No posts found. accepted: Diterima answered: dijawab asked: ditanyakan downvoted: downvoted mod_short: MOD mod_long: Moderator x_reputation: reputasi x_votes: vote diterima x_answers: jawaban x_questions: pertanyaan recent_badges: Recent Badges install: title: Installation next: Selanjutnya done: Selesai config_yaml_error: Can't create the config.yaml file. lang: label: Please choose a language db_type: label: Database engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database host placeholder: "db:3306" msg: Database host cannot be empty. db_name: label: Database name placeholder: answer msg: Database name cannot be empty. db_file: label: Database file placeholder: /data/answer.db msg: Database file cannot be empty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site name msg: Site name cannot be empty. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. max_length: Site URL must be at maximum 512 characters in length. contact_email: label: Contact email text: Email address of key contact responsible for this site. msg: empty: Contact email cannot be empty. incorrect: Contact email incorrect format. login_required: label: Private switch: Login required text: Only logged in users can access this community. admin_name: label: Name msg: Name cannot be empty. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Koneksi ke database gagal db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_error: http_error: HTTP Error {{ code }} desc_403: You don't have permission to access this page. desc_404: Unfortunately, this page doesn't exist. desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dasbor contents: Konten questions: Pertanyaan answers: Jawaban users: Pengguna badges: Badges flags: Flags settings: Pengaturan general: Umum interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write terms: Terms tos: Terms of Service privacy: Privasi seo: SEO customize: Kostumisasi themes: Tema login: Masuk privileges: Privileges plugins: Plugins installed_plugins: Installed Plugins apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Welcome to {{site_name}} user_center: login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Admin dashboard: title: Dasbor welcome: Welcome to Admin! site_statistics: Site statistics questions: "Pertanyaan:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Jawaban:" comments: "Komentar:" votes: "Vote:" users: "Users:" flags: "Flags:" reviews: "Reviews:" site_health: Site health version: "Versi:" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Private public: Public smtp: "SMTP:" timezone: "Zona Waktu:" system_info: System info go_version: "Go version:" database: "Database:" database_size: "Database size:" storage_used: "Penyimpanan yang terpakai:" uptime: "Uptime:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Contact forum: Forum documents: Dokumen feedback: Masukan support: Dukungan review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled writable: Writable not_writable: Not writable flags: title: Flags pending: Pending completed: Completed flagged: Flagged flagged_type: Flagged {{ type }} created: Created action: Action review: Review user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: users: label: Bulk add user placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separate “name, email, password” with commas. One user per line. msg: "Please enter the user's email, one per line." display_name: label: Display name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Status role: Role action: Action change: Ubah all: Semua staff: Staf more: More inactive: Tidak Aktif suspended: Ditangguhkan deleted: Dihapus normal: Normal Moderator: Moderator Admin: Admin User: Pengguna filter: placeholder: "Filter berdasarkan nama, user:id" set_new_password: Atur password baru edit_profile: Edit profile change_status: Ubah status change_role: Ubah role show_logs: Tampilkan log add_user: Tambahkan pengguna deactivate_user: title: Deactivate user content: An inactive user must re-validate their email. delete_user: title: Delete this user content: Are you sure you want to delete this user? This is permanent! remove: Remove their content label: Remove all questions, answers, comments, etc. text: Don’t check this if you wish to only delete the user’s account. suspend_user: title: Suspend this user content: A suspended user can't log in. label: How long will the user be suspended for? forever: Forever questions: page_title: Pertanyaan unlisted: Unlisted post: Post votes: Vote answers: Jawaban created: Dibuat status: Status action: Action change: Ubah pending: Pending filter: placeholder: "Filter berdasarkan judul, question:id" answers: page_title: Jawaban post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short site description msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site description msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. check_update: label: Software updates text: Automatically check for updates interface: page_title: Interface language: label: Interface language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: From email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Enkripsi msg: Enkripsi tidak boleh kosong. text: For most servers SSL is the recommended option. ssl: SSL tls: TLS none: Tidak ada smtp_port: label: SMTP port msg: SMTP port must be number 1 ~ 65535. text: Port untuk server email Anda. smtp_username: label: SMTP username msg: Nama Pengguna SMTP tidak boleh kosong. smtp_password: label: SMTP password msg: Password SMTP tidak boleh kosong. test_email_recipient: label: Test email recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile logo text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square icon msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for each question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. color_scheme: label: Color scheme navbar_style: label: Navbar background style primary_color: label: Primary color text: Modify the colors used by your themes layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: > head: label: Head text: > header: label: Header text: > footer: label: Footer text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: Allowed email domains text: Email domains that users must register accounts with. One domain per line. Ignored when empty. private: title: Private label: Login required text: Only logged in users can access this community. password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: All active: Active inactive: Inactive outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Name version: Version status: Status action: Action deactivate: Deactivate activate: Activate settings: Settings settings_users: title: Users avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar Base URL text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Allow users to change their display name allow_update_username: label: Allow users to change their username allow_update_avatar: label: Allow users to change their profile image allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optional) empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." select: Select page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created pin: pinned unpin: unpinned show: listed hide: unlisted title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: Our community staff reputation: reputation votes: votes prompt: leave_page: Are you sure you want to leave the page? changes_not_save: Your changes may not be saved. draft: discard_confirm: Are you sure you want to discard your draft? messages: post_deleted: This post has been deleted. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/it_IT.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Successo. unknown: other: Errore sconosciuto. request_format_error: other: Il formato della richiesta non è valido. unauthorized_error: other: Non autorizzato. database_error: other: Errore nel server dati. forbidden_error: other: Vietato. duplicate_request_error: other: Duplica invio. action: report: other: Segnala edit: other: Modifica delete: other: Cancella close: other: Chiudi reopen: other: Riapri forbidden_error: other: Vietato. pin: other: Fissa sul profilo hide: other: Rimuovi dall'elenco unpin: other: Stacca dal profilo show: other: Aggiungi all'elenco invite_someone_to_answer: other: Modifica undelete: other: Ripristina merge: other: Unisci role: name: user: other: Utente admin: other: Amministratore moderator: other: Moderatore description: user: other: Predefinito senza alcun accesso speciale. admin: other: Avere il pieno potere di accedere al sito. moderator: other: Ha accesso a tutti i post tranne le impostazioni di amministratore. privilege: level_1: description: other: 'Livello 1 (per team o gruppi privati: richiesta reputazione limitata)' level_2: description: other: 'Livello 2 (per startup community: richiesta reputazione scarsa)' level_3: description: other: 'Livello 3 (per community riconosciute: richiesta reputazione alta)' level_custom: description: other: Livello personalizzato rank_question_add_label: other: Fai una domanda rank_answer_add_label: other: Scrivi una risposta rank_comment_add_label: other: Scrivi un commento rank_report_add_label: other: Segnala rank_comment_vote_up_label: other: Approva commento rank_link_url_limit_label: other: Pubblica più di 2 link alla volta rank_question_vote_up_label: other: Approva domanda rank_answer_vote_up_label: other: Approva risposta rank_question_vote_down_label: other: Disapprova domanda rank_answer_vote_down_label: other: Disapprova risposta rank_invite_someone_to_answer_label: other: Invita qualcuno a rispondere rank_tag_add_label: other: Crea un nuovo tag rank_tag_edit_label: other: Modifica descrizione tag (necessità di revisione) rank_question_edit_label: other: Modifica la domanda di altri (necessità di revisione) rank_answer_edit_label: other: Modifica la risposta di altri (necessità di revisione) rank_question_edit_without_review_label: other: Modifica la domanda di altri senza bisogno di revisione rank_answer_edit_without_review_label: other: Modifica la risposta di altri senza bisogno di revisione rank_question_audit_label: other: Rivedi modifiche alla domanda rank_answer_audit_label: other: Rivedi modifiche alla risposta rank_tag_audit_label: other: Esamina le modifiche ai tag rank_tag_edit_without_review_label: other: Modifica la descrizione del tag senza necessità di revisione rank_tag_synonym_label: other: Gestisci sinonimi dei tag email: other: E-mail e_mail: other: E-mail password: other: Chiave di accesso pass: other: Password old_pass: other: Password attuale original_text: other: Questo post email_or_password_wrong_error: other: Email o password errati. error: common: invalid_url: other: URL non valido. status_invalid: other: Status non valido. password: space_invalid: other: La password non può contenere spazi. admin: cannot_update_their_password: other: Non è possibile modificare la password. cannot_edit_their_profile: other: Non è possibile modificare il profilo. cannot_modify_self_status: other: Non è possibile modificare il tuo status. email_or_password_wrong: other: Email o password errati. answer: not_found: other: Risposta non trovata. cannot_deleted: other: Permesso per cancellare mancante. cannot_update: other: Nessun permesso per l'aggiornamento. question_closed_cannot_add: other: Le domande sono chiuse e non possono essere aggiunte. content_cannot_empty: other: Il contenuto della risposta non può essere vuoto. comment: edit_without_permission: other: Non si hanno di privilegi sufficienti per modificare il commento. not_found: other: Commento non trovato. cannot_edit_after_deadline: other: Il tempo per editare è scaduto. content_cannot_empty: other: Il commento non può essere vuoto. email: duplicate: other: Email già esistente. need_to_be_verified: other: L'email deve essere verificata. verify_url_expired: other: L'url di verifica email è scaduto, si prega di reinviare l'email. illegal_email_domain_error: other: L'email non è consentita da quel dominio di posta elettronica. Si prega di usarne un altro. lang: not_found: other: File lingua non trovato. object: captcha_verification_failed: other: Captcha errato. disallow_follow: other: Non sei autorizzato a seguire disallow_vote: other: non sei autorizzato a votare disallow_vote_your_self: other: Non puoi votare un tuo post! not_found: other: oggetto non trovato verification_failed: other: verifica fallita email_or_password_incorrect: other: email o password incorretti old_password_verification_failed: other: la verifica della vecchia password è fallita new_password_same_as_previous_setting: other: La nuova password è identica alla precedente already_deleted: other: Questo post è stato eliminato. meta: object_not_found: other: Meta oggetto non trovato question: already_deleted: other: Questo post è stato eliminato. under_review: other: Il tuo post è in attesa di revisione. Sarà visibile dopo essere stato approvato. not_found: other: domanda non trovata cannot_deleted: other: Permesso per cancellare mancante. cannot_close: other: Nessun permesso per chiudere. cannot_update: other: Nessun permesso per l'aggiornamento. content_cannot_empty: other: Il contenuto non può essere vuoto. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Il rango di reputazione non soddisfa le condizioni. vote_fail_to_meet_the_condition: other: Grazie per il feedback. Hai bisogno di almeno una reputazione {{.Rank}} per votare. no_enough_rank_to_operate: other: Hai bisogno di almeno una reputazione {{.Rank}} per fare questo. report: handle_failed: other: Gestione del report fallita not_found: other: Report non trovato tag: already_exist: other: Tag già esistente. not_found: other: Etichetta non trovata recommend_tag_not_found: other: Il tag consigliato non esiste. recommend_tag_enter: other: Inserisci almeno un tag. not_contain_synonym_tags: other: Non deve contenere tag sinonimi. cannot_update: other: Nessun permesso per l'aggiornamento. is_used_cannot_delete: other: Non è possibile eliminare un tag in uso. cannot_set_synonym_as_itself: other: Non puoi impostare il sinonimo del tag corrente come se stesso. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: Il mittente non può essere un indirizzo email. theme: not_found: other: tema non trovato revision: review_underway: other: Non è possibile modificare al momento, c'è una versione nella coda di revisione. no_permission: other: Non è permessa la revisione. user: external_login_missing_user_id: other: La piattaforma di terze parti non fornisce un utente ID unico, quindi non è possibile effettuare il login. Si prega di contattare l'amministratore del sito web. external_login_unbinding_forbidden: other: Per favore imposta una password di login per il tuo account prima di rimuovere questo accesso. email_or_password_wrong: other: other: Email o password errati not_found: other: utente non trovato suspended: other: utente sospeso username_invalid: other: utente non valido username_duplicate: other: Nome utente già in uso set_avatar: other: Inserimento dell'Avatar non riuscito. cannot_update_your_role: other: Non puoi modificare il tuo ruolo. not_allowed_registration: other: Attualmente il sito non è aperto per la registrazione. not_allowed_login_via_password: other: Attualmente non è consentito accedere al sito tramite password. access_denied: other: Accesso negato page_access_denied: other: Non hai accesso a questa pagina. add_bulk_users_format_error: other: "Errore {{.Field}} formato vicino a '{{.Content}}' alla riga {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Il numero di utenti che aggiungi contemporaneamente dovrebbe essere compreso tra 1 e {{.MaxAmount}}." status_suspended_forever: other: "Questo utente è stato sospeso per sempre. Questo utente non soddisfa le linee guida della comunità." status_suspended_until: other: "Questo utente è stato sospeso fino a {{.SuspendedUntil}}. Questo utente non soddisfa le linee guida della comunità." status_deleted: other: "Utente eliminato." status_inactive: other: "L'utente è inattivo." config: read_config_failed: other: Configurazione lettura fallita database: connection_failed: other: Connessione al database fallita create_table_failed: other: Creazione tabella non riuscita install: create_config_failed: other: Impossibile creare il file config.yaml. upload: unsupported_file_format: other: Formato file non supportato. site_info: config_not_found: other: Configurazione del sito non trovata. badge: object_not_found: other: Oggetto badge non trovato reason: spam: name: other: Spam desc: other: "Questo post è una pubblicità o spam. Non è utile o rilevante per l'argomento attuale.\n" rude_or_abusive: name: other: scortese o offensivo desc: other: "Una persona ragionevole troverebbe questo contenuto inappropriato per un discorso rispettoso." a_duplicate: name: other: duplicato desc: other: Questa domanda è già stata posta e ha già una risposta. placeholder: other: Inserisci il link alla domanda esistente not_a_answer: name: other: Non è una risposta desc: other: "Questo è stato pubblicato come una risposta, ma non tenta di rispondere alla domanda. Dovrebbe forse essere una modifica, un commento, un'altra domanda,o cancellato del tutto." no_longer_needed: name: other: Non più necessario desc: other: Questo commento è obsoleto, informale o non rilevante per questo post. something: name: other: Qualcos'altro desc: other: Questo post richiede l'attenzione dello staff per un altro motivo non elencato sopra. placeholder: other: Facci sapere nello specifico cosa ti preoccupa community_specific: name: other: Un motivo legato alla community desc: other: Questa domanda non soddisfa le linee guida della community. not_clarity: name: other: Richiede maggiori dettagli o chiarezza desc: other: Questa domanda include più domande in una. Dovrebbe concentrarsi su un unico problema. looks_ok: name: other: sembra OK desc: other: Questo post è corretto così com'è e non è di bassa qualità. needs_edit: name: other: Necessita di modifiche che ho effettuato desc: other: Migliora e correggi tu stesso i problemi di questo post. needs_close: name: other: necessita di chiusura desc: other: A una domanda chiusa non è possibile rispondere, ma è comunque possibile modificare, votare e commentare. needs_delete: name: other: Necessario eliminare desc: other: Questo post verrà eliminato. question: close: duplicate: name: other: posta indesiderata desc: other: Questa domanda è già stata posta e ha già una risposta. guideline: name: other: motivo legato alla community desc: other: Questa domanda non soddisfa le linee guida della comunità. multiple: name: other: richiede maggiori dettagli o chiarezza desc: other: Questa domanda attualmente include più domande in uno. Dovrebbe concentrarsi su un solo problema. other: name: other: altro desc: other: Questo articolo richiede un'altro motivo non listato sopra. operation_type: asked: other: chiesto answered: other: Risposto modified: other: Modificato deleted_title: other: "\nDomanda cancellata" questions_title: other: Domande tag: tags_title: other: Tags no_description: other: Il tag non ha descrizioni. notification: action: update_question: other: domanda aggiornata answer_the_question: other: domanda risposta update_answer: other: risposta aggiornata accept_answer: other: risposta accettata comment_question: other: domanda commentata comment_answer: other: risposta commentata reply_to_you: other: hai ricevuto risposta mention_you: other: sei stato menzionato your_question_is_closed: other: la tua domanda è stata chiusa your_question_was_deleted: other: la tua domanda è stata rimossa your_answer_was_deleted: other: la tua risposta è stata rimossa your_comment_was_deleted: other: il tuo commento è stato rimosso up_voted_question: other: domanda approvata down_voted_question: other: domanda scartata up_voted_answer: other: risposta approvata down_voted_answer: other: risposta sfavorevole up_voted_comment: other: commento approvato invited_you_to_answer: other: sei invitato a rispondere earned_badge: other: Hai ottenuto il badge "{{.BadgeName}}" email_tpl: change_email: title: other: "[{{.SiteName}}] Conferma il tuo nuovo indirizzo email" body: other: "Conferma il tuo nuovo indirizzo email per {{.SiteName}} cliccando sul seguente link:
\n{{.ChangeEmailUrl}}

\n\nSe non hai richiesto questa modifica, ignora questa email.

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} ha risposto alla tua domanda" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nVisualizza su {{.SiteName}}

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.

\n\nCancellazione" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} ti ha invitato a rispondere" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Penso che tu possa sapere la risposta.

\nVisualizza su {{.SiteName}}

\n\n--
\nNota: Questa è un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.

\n\nCancellazione" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} ha commentato il tuo post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nVisualizza su {{.SiteName}}

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.

\n\nCancellazione" new_question: title: other: "[{{.SiteName}}] Nuova domanda: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non sarà visualizzata.

\n\nCancellati" pass_reset: title: other: "[{{.SiteName }}] Reimpostazione della password" body: other: "Qualcuno ha chiesto di reimpostare la tua password su {{.SiteName}}.

\n\nSe non sei tu, puoi tranquillamente ignorare questa email.

\n\nClicca sul seguente link per scegliere una nuova password:
\n{{.PassResetUrl}}\n

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata." register: title: other: "[{{.SiteName}}] Conferma il tuo nuovo account" body: other: "Benvenuto in {{.SiteName}}!

\n\nClicca il seguente link per confermare e attivare il tuo nuovo account:
\n{{.RegisterUrl}}

\n\nSe il link di cui sopra non è cliccabile, prova a copiarlo e incollarlo nella barra degli indirizzi del tuo browser web.\n

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata." test: title: other: "[{{.SiteName}}] Email di prova" body: other: "Questa è una email di prova.\n

\n\n--
\nNota: Questa è un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata." action_activity_type: upvote: other: voto a favore upvoted: other: voto a favore downvote: other: voto negativo downvoted: other: votato negativamente accept: other: Accetta accepted: other: Accettato edit: other: modifica review: queued_post: other: Post in coda flagged_post: other: Post contrassegnato suggested_post_edit: other: Modifiche suggerite reaction: tooltip: other: "{{ .Names }} e {{ .Count }} più..." badge: default_badges: autobiographer: name: other: Autobiografo desc: other: Informazioni sul profilo completate. certified: name: other: Certificato desc: other: "\nCompletato il nostro nuovo tutorial per l'utente." editor: name: other: Editor desc: other: Prima modifica al post. first_flag: name: other: Primo Contrassegno desc: other: Primo contrassegno di un post. first_upvote: name: other: Primo Mi Piace desc: other: Primo Mi Piace a un post first_link: name: other: Primo Link desc: other: Per prima cosa è stato aggiunto un link ad un altro post. first_reaction: name: other: Prima Reazione desc: other: Prima reazione al post. first_share: name: other: Prima Condivisione desc: other: Prima condivisione a un post. scholar: name: other: Studioso desc: other: Ha posto una domanda e ha accettato una risposta commentator: name: other: Commentatore desc: other: Lascia 5 commenti. new_user_of_the_month: name: other: Nuovo Utente del Mese desc: other: Contributi straordinari nel primo mese. read_guidelines: name: other: Leggi le Linee Guida desc: other: Leggi le [linee guida della community]. reader: name: other: Lettore desc: other: Leggi ogni risposta in un argomento con più di 10 risposte. welcome: name: other: Benvenuto desc: other: Ricevuto un voto positivo. nice_share: name: other: Condivisione positiva. desc: other: Ha condiviso un post con 25 visitatori unici. good_share: name: other: Condivisione positiva. desc: other: Condiviso un post con 300 visitatori unici. great_share: name: other: Grande Condivisione desc: other: Condiviso un post con 1000 visitatori unici. out_of_love: name: other: Fuori Amore desc: other: Usato 50 voti in su in un giorno. higher_love: name: other: Amore Superiore desc: other: Usato 50 voti in su in un giorno 5 volte. crazy_in_love: name: other: Pazzo innamorato desc: other: Utilizzato 50 voti in su in un giorno 20 volte. promoter: name: other: Promotore desc: other: Invitato un utente. campaigner: name: other: Campagnia desc: other: Invitato a 3 utenti di base. champion: name: other: Campione desc: other: Invitati 5 membri. thank_you: name: other: Grazie desc: other: Il post ha ricevuto 20 voti positivi e 10 voti positivi. gives_back: name: other: Feedback desc: other: Post con 100 voti positivi e 100 voti positivi espressi. empathetic: name: other: Empatico desc: other: Ha 500 posti votati e ha rinunciato a 1000 voti. enthusiast: name: other: Entusiasta desc: other: Visitato 10 giorni consecutivi. aficionado: name: other: Aficionado desc: other: Visitato 100 giorni consecutivi. devotee: name: other: Devotee desc: other: Visitato 365 giorni consecutivi. anniversary: name: other: Anniversario desc: other: Membro attivo per un anno, pubblicato almeno una volta. appreciated: name: other: Apprezzato desc: other: Ricevuto 1 voto su 20 posti. respected: name: other: Rispettati desc: other: Ricevuto 2 voto su 100 posti. admired: name: other: Ammirato desc: other: Ricevuto 5 voto su 300 posti. solved: name: other: Risolto desc: other: Avere una risposta accettata. guidance_counsellor: name: other: Consulente Di Orientamento desc: other: Si accettano 10 risposte. know_it_all: name: other: Sa tutto desc: other: Si accettano 50 risposte. solution_institution: name: other: Istituzione Di Soluzione desc: other: Si accettano 150 risposte. nice_answer: name: other: Bella risposta desc: other: Punteggio domande pari o superiore a 10. good_answer: name: other: Buona risposta desc: other: Punteggio domande pari o superiore a 25. great_answer: name: other: Risposta molto buona desc: other: Punteggio domande pari o superiore a 50. nice_question: name: other: Bella domanda desc: other: Punteggio domande pari o superiore a 10. good_question: name: other: Buona domanda desc: other: Punteggio della domanda di 25 o più great_question: name: other: Ottima domanda desc: other: Punteggio domande pari o superiore a 50. popular_question: name: other: Domanda popolare desc: other: "Domanda con 500 visualizzazioni\n" notable_question: name: other: Domanda notevole desc: other: Domanda con 1.000 visualizzazioni. famous_question: name: other: Domanda celebre desc: other: "Domanda con 5.000 visualizzazioni.\n." popular_link: name: other: Link Popolare desc: other: Pubblicato un link esterno con 50 clic. hot_link: name: other: Link popolare desc: other: Pubblicato un link esterno con 300 clic. famous_link: name: other: Link celebre desc: other: Pubblicato un link esterno con 100 clic. default_badge_groups: getting_started: name: other: Primi passi community: name: other: Community posting: name: other: Pubblicazione in corso # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Come formattare desc: >-
  • menzionare un post: #post_id

  • per creare collegamenti

    <https://url.com>

    [Titolo](https://url.com)
  • mettere ritorni a capo tra i paragrafi

  • corsivo o **grassetto**

  • indentare il codice con 4 spazi

  • citare iniziando la riga con >

  • utilizzare il backtick per scapeggiare il codice `come _questo_`

  • creare recinti di codice con backticks `

    ```
    codice qui
    ```
pagination: prev: Prec next: Successivo page_title: question: Domanda questions: Domande tag: Tag tags: Tags tag_wiki: tag wiki create_tag: Crea tag edit_tag: Modifica Tag ask_a_question: Crea domanda edit_question: Modifica Domanda edit_answer: Modifica risposta search: Cerca posts_containing: Post contenenti settings: Impostazioni notifications: Notifiche login: Accedi sign_up: Registrati account_recovery: Recupero dell'account account_activation: Attivazione account confirm_email: Conferma Email account_suspended: Account sospeso admin: Amministratore change_email: Modifica la tua email install: Installazione di Answer upgrade: Upgrade di Answer maintenance: Manutenzione del sito users: Utenti oauth_callback: Elaborazione in corso http_404: Errore HTTP 404 http_50X: Errore HTTP 500 http_403: Errore HTTP 403 logout: Disconnetti posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notifiche inbox: Posta in arrivo achievement: Risultati new_alerts: Nuovi avvisi all_read: Segna tutto come letto show_more: Mostra di più someone: Qualcuno inbox_type: all: Tutti posts: Messaggi invites: Inviti votes: Voti answer: Risposta question: Domanda badge_award: Distintivo suspended: title: Il tuo account è stato sospeso until_time: "Il tuo account è stato sospeso fino a {{ time }}." forever: Questo utente è stato sospeso per sempre. end: Non soddisfi le linee guida della community. contact_us: Contattaci editor: blockquote: text: Citazione bold: text: Forte chart: text: Grafico flow_chart: Diagramma di flusso sequence_diagram: Diagramma di sequenza class_diagram: Diagramma di classe state_diagram: Diagramma di stato entity_relationship_diagram: Diagramma Entità-Relazione user_defined_diagram: "\nDiagramma definito dall'utente" gantt_chart: Diagramma di Gantt pie_chart: Grafico a torta code: text: Esempio di codice add_code: Aggiungi un esempio di codice form: fields: code: label: Codice msg: empty: Il codice non può essere vuoto language: label: Lingua placeholder: Rilevamento automatico btn_cancel: Cancella btn_confirm: Aggiungi formula: text: Formula options: inline: Formula nella linea block: "\nFormula di blocco" heading: text: Intestazione options: h1: Intestazione 1 h2: Intestazione 2 h3: Intestazione 3 h4: Intestazione 4 h5: Intestazione 5 h6: Intestazione 6 help: text: Aiuto hr: text: Riga orizzontale image: text: Immagine add_image: Aggiungi immagine tab_image: Carica immagine form_image: fields: file: label: File d'immagine btn: Scegli immagine msg: empty: Il file non può essere vuoto. only_image: Sono ammesse solo le immagini max_size: La dimensione del file non può superare {{size}} MB. desc: label: Descrizione tab_url: Url dell'Immagine form_url: fields: url: label: URL dell'immagine msg: empty: L'URL dell'immagine non può essere vuoto. name: label: Descrizione btn_cancel: Cancella btn_confirm: Aggiungi uploading: Caricamento in corso... indent: text: Indenta outdent: text: Deindenta italic: text: Corsivo link: text: Collegamento ipertestuale add_link: Aggiungi collegamento form: fields: url: label: URL msg: empty: L'URL non può essere vuoto name: label: Descrizione btn_cancel: Cancella btn_confirm: Aggiungi ordered_list: text: Elenco numerato unordered_list: text: Elenco puntato table: text: Tabella heading: Intestazione cell: Cella file: text: Allega file not_supported: "Non supportare quel tipo di file. Riprovare con {{file_type}}." max_size: "Allega la dimensione dei file non può superare {{size}} MB." close_modal: title: Sto chiudendo questo post come... btn_cancel: Cancella btn_submit: Invia remark: empty: Non può essere vuoto. msg: empty: Per favore seleziona un motivo report_modal: flag_title: Sto segnalando questo post per riportarlo come... close_title: Sto chiudendo questo post come... review_question_title: Rivedi la domanda review_answer_title: Rivedi la risposta review_comment_title: Rivedi il commento btn_cancel: Cancella btn_submit: Invia remark: empty: Non può essere vuoto. msg: empty: Per favore seleziona un motivo not_a_url: Formato URL errato. url_not_match: L'origine dell'URL non corrisponde al sito web corrente. tag_modal: title: Crea un nuovo tag form: fields: display_name: label: Nome da visualizzare msg: empty: Il nome utente non può essere vuoto. range: Nome utente fino a 35 caratteri. slug_name: label: Slug dell'URL desc: URL slug fino a 35 caratteri. msg: empty: Lo slug dell'URL non può essere vuoto. range: Slug dell'URL fino a 35 caratteri. character: Lo slug dell'URL contiene un set di caratteri non consentiti. desc: label: Descrizione revision: label: Revisione edit_summary: label: Modifica il riepilogo placeholder: >- Spiega brevemente le tue modifiche (ortografia rifinita, grammatica corretta, formattazione migliorata) btn_cancel: Cancella btn_submit: Invia btn_post: Pubblica un nuovo tag tag_info: created_at: Creato edited_at: Modificato history: Cronologia synonyms: title: Sinonimi text: I seguenti tag verranno rimappati a empty: Nessun sinonimo trovato. btn_add: Aggiungi un sinonimo btn_edit: Modifica btn_save: Salva synonyms_text: I seguenti tag verranno rimappati a delete: title: Elimina questo tag tip_with_posts: >-

Non è consentita l'eliminazione di tag con post.

Rimuovi prima questo tag dai post.

tip_with_synonyms: >-

Non è consentita l'eliminazione di tag con sinonimi.

Per favore, rimuovi prima i sinonimi da questo tag.

tip: Sei sicuro di voler cancellare? close: Chiudi merge: title: Unisci tag source_tag_title: Cerca tag source_tag_description: Il tag sorgente e i dati associati verranno rimappati al tag di destinazione. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Modifica Tag default_reason: Modifica tag default_first_reason: Aggiungi tag btn_save_edits: Salva modifiche btn_cancel: Cancella dates: long_date: Mese, giorno long_date_with_year: "Giorno, Mese, Anno" long_date_with_time: "MMM D, AAAA [at] HH:mm" now: ora x_seconds_ago: "{{count}}s fa" x_minutes_ago: "{{count}}m fa" x_hours_ago: "{{count}}h fa" hour: ora day: giorno hours: ore days: giorni month: month months: months year: year reaction: heart: cuore smile: sorriso frown: disapprovare btn_label: aggiungere o rimuovere le reazioni undo_emoji: annulla reazione {{ emoji }} react_emoji: reagire con {{ emoji }} unreact_emoji: non reagire con {{ emoji }} comment: btn_add_comment: Aggiungi un commento reply_to: Rispondi a btn_reply: Rispondi btn_edit: Modifica btn_delete: Cancella btn_flag: Segnala btn_save_edits: Salva modifiche btn_cancel: Cancella show_more: "{{count}} altri commenti" tip_question: >- Usa i commenti per chiedere maggiori informazioni o per suggerire miglioramenti. Evita di rispondere alle domande nei commenti. tip_answer: >- Utilizza i commenti per rispondere ad altri utenti o avvisarli delle modifiche. Se stai aggiungendo nuove informazioni, modifica il tuo post invece di commentare. tip_vote: Aggiunge qualcosa di utile al post edit_answer: title: Modifica risposta default_reason: Modifica risposta default_first_reason: Aggiungi risposta form: fields: revision: label: Revisione answer: label: Risposta feedback: characters: Il testo deve contenere almeno 6 caratteri. edit_summary: label: Modifica il riepilogo placeholder: >- Spiega brevemente le tue modifiche (ortografia rifinita, grammatica corretta, formattazione migliorata) btn_save_edits: Salva modifiche btn_cancel: Annulla tags: title: Tag sort_buttons: popular: Popolari name: Nome newest: Più recente button_follow: Segui button_following: Segui già tag_label: domande search_placeholder: Filtra per nome del tag no_desc: Il tag non ha descrizioni. more: Altro wiki: Wiki ask: title: Create Question edit_title: Modifica Domanda default_reason: Modifica domanda default_first_reason: Create question similar_questions: Domande simili form: fields: revision: label: Revisione title: label: Titolo placeholder: What's your topic? Be specific. msg: empty: Il titolo non può essere vuoto. range: Il titolo non può superare i 150 caratteri body: label: Contenuto msg: empty: Il corpo del testo non può essere vuoto. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Tags msg: empty: I tag non possono essere vuoti. answer: label: Risposta msg: empty: La risposta non può essere vuota. edit_summary: label: Modifica riepilogo placeholder: >- Spiega brevemente le tue modifiche (ortografia corretta, grammatica corretta, formattazione migliorata) btn_post_question: Posta la tua domanda btn_save_edits: Salva modifiche answer_question: Rispondi alla tua domanda post_question&answer: Posta la tua domanda e risposta tag_selector: add_btn: Aggiungi tag create_btn: Crea un nuovo tag search_tag: Cerca tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Nessun tag corrispondente tag_required_text: Tag richiesto (almeno uno) header: nav: question: Domande tag: Tags user: Utenti badges: Badges profile: Profilo setting: Impostazioni logout: Disconnetti admin: Amministratore review: Revisione bookmark: Segnalibri moderation: Moderazione search: placeholder: Cerca footer: build_on: Powered by <1> Apache Answer upload_img: name: Modifica loading: caricamento in corso... pic_auth_code: title: Captcha placeholder: Digita il testo sopra msg: empty: Il Captcha non può essere vuoto. inactive: first: >- Hai quasi finito! Abbiamo inviato un'e-mail di attivazione a {{mail}}. Segui le istruzioni contenute nella mail per attivare il tuo account. info: "Se non arriva, controlla la cartella spam." another: >- Ti abbiamo inviato un'altra email di attivazione all'indirizzo {{mail}}. Potrebbero volerci alcuni minuti prima che arrivi; assicurati di controllare la cartella spam. btn_name: Reinvia l'e-mail di attivazione change_btn_name: Modifica e-mail msg: empty: Non può essere vuoto. resend_email: url_label: Sei sicuro di voler inviare nuovamente l'e-mail di attivazione? url_text: Puoi anche fornire all'utente il link di attivazione riportato sopra. login: login_to_continue: Accedi per continuare info_sign: Non hai un account? <1>Iscriviti info_login: Hai già un account? <1>Accedi agreements: Registrandoti, accetti l'<1>informativa sulla privacy e i <3>termini del servizio. forgot_pass: Password dimenticata? name: label: Nome msg: empty: Il nome non può essere vuoto. range: Il nome deve essere di lunghezza compresa tra 2 e 30 caratteri. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: E-mail msg: empty: L'email non può essere vuota. password: label: Password msg: empty: La password non può essere vuota. different: Le password inserite su entrambi i lati non corrispondono account_forgot: page_title: Password dimenticata? btn_name: Inviami email di recupero send_success: >- Se un account corrisponde a {{mail}}, a breve dovresti ricevere un'e-mail con le istruzioni su come reimpostare la password. email: label: E-mail msg: empty: Il campo email non può essere vuoto. change_email: btn_cancel: Cancella btn_update: Aggiorna indirizzo email send_success: >- Se un account corrisponde a {{mail}}, a breve dovresti ricevere un'email con le istruzioni su come reimpostare la password. email: label: Nuova email msg: empty: L'email non può essere vuota. oauth: connect: Connettiti con {{ auth_name }} remove: Rimuovi {{ auth_name }} oauth_bind_email: subtitle: Aggiungi un'email di recupero al tuo account. btn_update: Aggiorna l'indirizzo email email: label: E-mail msg: empty: L'email non può essere vuota. modal_title: Email già esistente modal_content: Questo indirizzo email è già registrato. Sei sicuro che vuoi connetterti all'account esistente? modal_cancel: Cambia email modal_confirm: Connettiti all'account esistente password_reset: page_title: Reimposta la password btn_name: Reimposta la mia password reset_success: >- Hai cambiato con successo la tua password; sarai reindirizzato alla pagina di accesso. link_invalid: >- Siamo spiacenti, questo link di reset della password non è più valido. Forse la password è già stata reimpostata? to_login: Continua per effettuare il login password: label: Password msg: empty: La password non può essere vuota length: La lunghezza deve essere compresa tra 8 e 32 different: Le password inserite non corrispondono password_confirm: label: Conferma la nuova password settings: page_title: Impostazioni goto_modify: Vai alle modifiche nav: profile: Profilo notification: Notifiche account: Profilo interface: Interfaccia profile: heading: Profilo btn_name: Salva display_name: label: Visualizza nome msg: Il nome utente non può essere vuoto. msg_range: Display name must be 2-30 characters in length. username: label: Nome utente caption: Gli altri utenti possono menzionarti con @{{username}}. msg: Il nome utente non può essere vuoto. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Immagine del profilo gravatar: Gravatar gravatar_text: Puoi cambiare l'immagine custom: Personalizzato custom_text: Puoi caricare la tua immagine. default: Sistema msg: Per favore carica un avatar bio: label: Chi sono website: label: Sito web placeholder: "https://esempio.com" msg: Formato non corretto del sito web location: label: Luogo placeholder: "Città, Paese" notification: heading: Notifiche email turn_on: Accendi inbox: label: Notifiche in arrivo description: Risposte alle tue domande, commenti, inviti e altro ancora. all_new_question: label: Tutte le nuove domande description: Ricevi una notifica per tutte le nuove domande. Fino a 50 domande a settimana. all_new_question_for_following_tags: label: Tutte le nuove domande per i seguenti tag description: Ricevi una notifica delle nuove domande per i seguenti tag. account: heading: Profilo change_email_btn: Modifica e-mail change_pass_btn: Modifica password change_email_info: >- Abbiamo inviato una mail a quell'indirizzo. Si prega di seguire le istruzioni di conferma. email: label: E-mail new_email: label: Nuova mail msg: La nuova email non può essere vuota. pass: label: Password attuale msg: La password non può essere vuota password_title: Password current_pass: label: Password attuale msg: empty: La password attuale non può essere vuota. length: La lunghezza deve essere compresa tra 8 e 32. different: Le due password inserite non corrispondono. new_pass: label: Nuova password pass_confirm: label: Conferma la nuova password interface: heading: Interfaccia lang: label: Lingua dell'interfaccia text: La lingua dell'interfaccia utente cambierà quando aggiorni la pagina. my_logins: title: I miei login label: Accedi o registrati su questo sito utilizzando questi account. modal_title: Rimuovi login modal_content: Sei sicuro di voler rimuovere questo login dal tuo account? modal_confirm_btn: Rimuovi remove_success: Rimosso con successo toast: update: Aggiornamento riuscito update_password: Password modificata con successo. flag_success: Grazie per la segnalazione. forbidden_operate_self: Vietato operare su se stessi. review: Le tue modifiche verranno visualizzata dopo la revisione. sent_success: Inviato correttamente related_question: title: Related answers: risposte linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Persone Interpellate desc: Seleziona le persone che pensi potrebbero conoscere la risposta. invite: Invita a rispondere add: Aggiungi contatti search: Cerca persone question_detail: action: Azione created: Created Asked: Chiesto asked: chiesto update: Modificato Edited: Edited edit: modificato commented: commentato Views: Visualizzati Follow: Segui Following: Segui già follow_tip: Segui questa domanda per ricevere notifiche answered: Risposto closed_in: Chiuso in show_exist: Mostra domanda esistente. useful: Utile question_useful: È utile e chiaro question_un_useful: Non è chiaro né utile question_bookmark: Aggiungi questa domanda ai segnalibri answer_useful: È utile answer_un_useful: Non è utile answers: title: Risposte score: Punteggio newest: Più recenti oldest: Meno recente btn_accept: Accetta btn_accepted: Accettato write_answer: title: La tua risposta edit_answer: Modifica la mia risposta attuale btn_name: Pubblica la tua risposta add_another_answer: Aggiungi un'altra risposta confirm_title: Continua a rispondere continue: Continua confirm_info: >-

Sei sicuro di voler aggiungere un'altra risposta?

In alternativa, puoi usare il link di modifica per perfezionare e migliorare la tua risposta esistente.

empty: La risposta non può essere vuota. characters: Il contenuto deve avere una lunghezza di almeno 6 caratteri. tips: header_1: Grazie per la risposta li1_1: Assicurati di rispondere alla domanda. Fornisci dettagli e condividi la tua ricerca. li1_2: Effettua il backup di qualsiasi dichiarazione fatta con riferimenti o esperienze personali. header_2: Ma evita... li2_1: Chiedere aiuto, cercare chiarimenti o rispondere ad altre risposte. reopen: confirm_btn: Riapri title: Riapri questo post content: Sei sicuro di voler riaprire? list: confirm_btn: Listare title: Lista questo post content: Sei sicuro di volerlo listare? unlist: confirm_btn: Rimuovi dall'elenco title: Rimuovi questo post content: Sei sicuro di voler rimuovere dall'elenco? pin: title: Fissa questo post in cima al profilo content: Sei sicuro di voler fissare in blocco? Questo post apparirà in cima a tutte le liste. confirm_btn: Fissa sul profilo delete: title: Cancella questo post question: >- Non consigliamo di eliminaredomande con risposte perché ciò priva i futuri lettori di questa conoscenza.

L'eliminazione ripetuta di domande con risposte può comportare il blocco delle domande nel tuo account. Sei sicuro di voler eliminare? answer_accepted: >-

Non consigliamo di eliminare la risposta accettata perché così facendo si priva i futuri lettori di questa conoscenza.

La cancellazione ripetuta delle risposte accettate può causare il blocco del tuo account dalla risposta. Sei sicuro di voler eliminare? other: Sei sicuro di voler eliminare? tip_answer_deleted: Questa risposta è stata cancellata undelete_title: Ripristina questo post undelete_desc: Sei sicuro di voler ripristinare? btns: confirm: Conferma cancel: Cancella edit: Modifica save: Salva delete: Elimina undelete: Ripristina list: Aggiungi all'elenco unlist: Rimuovi dall'elenco unlisted: Rimosso dall'elenco login: Accedi signup: Registrati logout: Disconnetti verify: Verifica create: Create approve: Approva reject: Rifiuta skip: Salta discard_draft: Elimina bozza pinned: Fissato in cima all: Tutti question: Domanda answer: Risposta comment: Commento refresh: Aggiorna resend: Rinvia deactivate: Disattivare active: Attivo suspend: Sospendi unsuspend: Riabilita close: Chiudi reopen: Riapri ok: OK light: Illuminazione dark: Scuro system_setting: Configurazione di sistema default: Predefinito reset: Resetta tag: Tag post_lowercase: post filter: 'Filtra' ignore: Ignora submit: Invia normal: Normale closed: Chiuso deleted: Eliminato deleted_permanently: Deleted permanently pending: In attesa more: Altro view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Risultati della ricerca keywords: Parole chiave options: Opzioni follow: Segui following: Segui già counts: "{{count}} Risultati" counts_loading: "... Results" more: Altro sort_btns: relevance: Rilevanza newest: Più recenti active: Attivo score: Punteggio more: Altro tips: title: Suggerimenti per ricerca avanzata tag: "<1>[tag] cerca dentro un tag" user: "<1>user:username ricerca per autore" answer: "<1>risposte:0 domande senza risposta" score: "<1>punteggio:3 messaggi con un punteggio di 3+" question: "<1>is:question cerca domande" is_answer: "<1>is:answer cerca risposte" empty: Non siamo riusciti a trovare nulla.
Prova parole chiave diverse o meno specifiche. share: name: Condividi copy: Copia il link via: Condividi il post via... copied: Copiato facebook: Condividi su Facebook twitter: Share to X cannot_vote_for_self: Non puoi votare un tuo post! modal_confirm: title: Errore... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Il tuo nuovo account è confermato; sarai reindirizzato alla home page. link: Continua alla Homepage oops: Oops! invalid: Il link che hai usato non è più attivo. confirm_new_email: La tua email è stata aggiornata. confirm_new_email_invalid: >- Siamo spiacenti, questo link di conferma non è più valido. Forse la tua email è già stata modificata? unsubscribe: page_title: Annulla l'iscrizione success_title: Cancellazione effettuata con successo success_desc: Sei stato rimosso con successo da questa lista e non riceverai ulteriori email link: Cambia impostazioni question: following_tags: Tag seguenti edit: Modifica save: Salva follow_tag_tip: Segui i tag per curare la tua lista di domande. hot_questions: Domande scottanti all_questions: Tutte le domande x_questions: "{{ count }} Domande" x_answers: "{{count}} risposte" x_posts: "{{ count }} Posts" questions: Domande answers: Risposte newest: Più recenti active: Attivo hot: Caldo frequent: Frequenti recommend: Raccomandato score: Punteggio unanswered: Senza risposta modified: Modificato answered: Risposte asked: chiesto closed: Chiuso follow_a_tag: Segui un tag more: Altro personal: overview: Informazioni Generali answers: Risposte answer: Risposta questions: Domande question: Domanda bookmarks: Segnalibri reputation: Reputazione comments: Commenti votes: Voti badges: Badges newest: Più recenti score: Punteggio edit_profile: Modifica profilo visited_x_days: "{{ count }} giorni visitati" viewed: Visualizzati joined: Iscritto comma: "," last_login: Visto about_me: Chi sono about_me_empty: "// Ciao, mondo !" top_answers: Migliori risposte top_questions: Domande principali stats: Statistiche list_empty: Nessun post trovato.
Forse desideri selezionare una scheda diversa? content_empty: Nessun post trovato. accepted: Accettato answered: risposto asked: chiesto downvoted: votato negativamente mod_short: Moderatore mod_long: Moderatori x_reputation: reputazione x_votes: voti ricevuti x_answers: risposte x_questions: domande recent_badges: Badges Recenti install: title: Installazione next: Avanti done: Fatto config_yaml_error: Impossibile creare il file config.yaml. lang: label: Scegli una lingua db_type: label: Motore database db_username: label: Nome utente placeholder: root msg: Il nome utente non può essere vuoto. db_password: label: Password placeholder: root msg: La password non può essere vuota. db_host: label: Host del database placeholder: "db:3306" msg: L'host del database non può essere vuoto. db_name: label: Nome database placeholder: risposta msg: Il nome del database non può essere vuoto. db_file: label: File del database placeholder: /data/answer.db msg: Il file del database non può essere vuoto. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Crea config.yaml label: File config.yaml creato. desc: >- Puoi creare manualmente il file <1>config.yaml nella directory <1>/var/wwww/xxx/ e incollarvi il seguente testo. info: Una volta fatto, fai clic sul pulsante "Avanti". site_information: Informazioni sul sito admin_account: Account Amministratore site_name: label: Nome del sito msg: Il nome del sito non può essere vuoto. msg_max_length: Il nome del sito deve contenere un massimo di 30 caratteri. site_url: label: URL del sito text: L'indirizzo del tuo sito. msg: empty: L'URL del sito non può essere vuoto. incorrect: Formato errato dell'URL del sito. max_length: L'URL del sito deve contenere un massimo di 512 caratteri. contact_email: label: Email di contatto text: Indirizzo e-mail del contatto chiave responsabile di questo sito. msg: empty: L'email del contatto non può essere vuota. incorrect: Formato errato dell'e-mail di contatto. login_required: label: Privato switch: Login obbligatorio text: Solo gli utenti registrati possono accedere a questa community. admin_name: label: Nome msg: Il nome non può essere vuoto. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Password text: >- Avrai bisogno di questa password per accedere. Conservala in un luogo sicuro. msg: La password non può essere vuota msg_min_length: La password deve contenere almeno 8 caratteri. msg_max_length: La password deve contenere un massimo di 32 caratteri. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: E-mail text: Avrai bisogno di questa email per accedere. msg: empty: Il campo email non può essere vuoto. incorrect: Formato dell'email errato. ready_title: Il tuo sito è pronto ready_desc: >- Se vuoi cambiare più impostazioni, visita la sezione <1>admin che si trova nel menu del sito. good_luck: "Divertiti e buona fortuna!" warn_title: Pericolo warn_desc: >- Il file <1>config.yaml esiste già. Se vuoi reimpostare uno qualsiasi degli elementi di configurazione in questo file, eliminalo prima. install_now: Puoi provare a <1>installare ora. installed: Già installato installed_desc: >- Sembra che tu abbia già installato. Per reinstallare, cancella prima le vecchie tabelle del database. db_failed: Connessione al database fallita db_failed_desc: >- Questo significa che le informazioni sul database nel file <1>config.yaml non sono corrette o che non è stato possibile stabilire il contatto con il server del database. Ciò potrebbe significare che il server del database del tuo host è inattivo. counts: views: visualizzazioni votes: Voti answers: risposte accepted: Accettato page_error: http_error: Errore HTTP {{ code }} desc_403: Non hai i permessi per accedere a questa pagina. desc_404: Sfortunatamente, questa pagina non esiste. desc_50X: Il server ha riscontrato un errore e non è stato possibile completare la richiesta. back_home: Torna alla home page page_maintenance: desc: "Siamo in manutenzione, torneremo presto." nav_menus: dashboard: Pannello di controllo contents: Contenuti questions: Domande answers: Risposte users: Utenti badges: Badges flags: Contrassegni settings: Impostazioni general: Generale interface: Interfaccia smtp: Protocollo di Trasferimento Posta Semplice branding: Marchio legal: Legale write: Scrivi terms: Terms tos: Termini del servizio privacy: Privacy seo: SEO customize: Personalizza themes: Temi login: Accedi privileges: Privilegi plugins: Plugin installed_plugins: Plugin installati apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Benvenuto/a su {{site_name}}! user_center: login: Accedi qrcode_login_tip: Si prega di utilizzare {{ agentName }} per scansionare il codice QR e accedere. login_failed_email_tip: Accesso non riuscito. Consenti a questa app di accedere alle tue informazioni email prima di riprovare. badges: modal: title: Congratulazioni content: Hai guadagnato un nuovo distintivo. close: Chiudi confirm: Visualizza badge title: Badges awarded: Premiati earned_×: Ottenuto ×{{ number }} ×_awarded: "{{ number }} premiato" can_earn_multiple: Puoi guadagnare questo più volte. earned: Ottenuti admin: admin_header: title: Amministratore dashboard: title: Pannello di controllo welcome: Benvenuto ad Admin! site_statistics: Statistiche del sito questions: "Domande:" resolved: "Risolto:" unanswered: "Senza risposta:" answers: "Risposte:" comments: "Commenti:" votes: "Voti:" users: "Utenti:" flags: "Flags" reviews: "Revisioni" site_health: Stato del sito version: "Versione:" https: "HTTPS:" upload_folder: "Carica Cartella" run_mode: "Modalità di esecuzione:" private: Privato public: Pubblico smtp: "Protocollo di Trasferimento Posta Semplice" timezone: "Fuso orario:" system_info: Info sistema go_version: "Versione Go:" database: "Banca dati:" database_size: "Dimensioni del database" storage_used: "Spazio di archiviazione utilizzato:" uptime: "Tempo di attività:" links: Collegamenti plugins: Plugin github: GitHub blog: Blog contact: Contatti forum: Forum documents: Documenti feedback: Feedback support: Assistenza review: Revisione config: Configurazione update_to: Aggiornato a latest: Recenti check_failed: Controllo fallito "yes": "Sì" "no": "No" not_allowed: Non autorizzato allowed: Consentito enabled: Abilitato disabled: Disabilitato writable: Editabile not_writable: Non editabile flags: title: Contrassegni pending: In attesa completed: Completato flagged: Contrassegnato flagged_type: Contrassegnato {{ type }} created: Creato action: Azione review: Revisione user_role_modal: title: Cambia ruolo utente in... btn_cancel: Cancella btn_submit: Invia new_password_modal: title: Imposta una nuova password form: fields: password: label: Password text: L'utente sarà disconnesso e dovrà effettuare nuovamente il login. msg: La password deve contenere da 8 a 32 caratteri. btn_cancel: Cancella btn_submit: Invia edit_profile_modal: title: Modifica profilo form: fields: display_name: label: Visualizza nome msg_range: Il nome visualizzato deve essere di 2-30 caratteri di lunghezza. username: label: Nome utente msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Indirizzo e-mail non valido. edit_success: Modificato con successo btn_cancel: Annulla btn_submit: Invia user_modal: title: Aggiungi un nuovo utente form: fields: users: label: Aggiungi utenti in blocco placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separare “nome, email, password” con delle virgole. Un utente per riga. msg: "Inserisci l'email dell'utente, una per riga." display_name: label: Nome da visualizzare msg: Il nome visualizzato deve essere di 2-30 caratteri di lunghezza. email: label: E-mail msg: L'email non è valida. password: label: Password msg: La password deve contenere da 8 a 32 caratteri. btn_cancel: Cancella btn_submit: Invia users: title: Utenti name: Nome email: E-mail reputation: Reputazione created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Sospendi fino al status: Stato role: Ruolo action: Azione change: Modifica all: Tutti staff: Personale more: Altro inactive: Inattivo suspended: Sospeso deleted: Eliminato normal: Normale Moderator: Moderatore Admin: Amministratore User: Utente filter: placeholder: "Filtra per nome, utente:id" set_new_password: Imposta una nuova password edit_profile: Modifica profilo change_status: Modifica lo stato change_role: Cambia il ruolo show_logs: Visualizza i log add_user: Aggiungi utente deactivate_user: title: Disattiva utente content: Un utente inattivo deve riconvalidare la propria email. delete_user: title: Rimuovi questo utente content: Sei sicuro di voler eliminare questo utente? L'operazione è permanente. remove: Rimuovi il loro contenuto label: Rimuovi tutte le domande, risposte, commenti, ecc. text: Non selezionare questa opzione se desideri eliminare solo l'account dell'utente. suspend_user: title: Sospendi questo utente content: Un utente sospeso non può accedere. label: Per quanto tempo vuoi sospendere l'utente? forever: Per sempre questions: page_title: Domande unlisted: Rimosso dall'elenco post: Posta votes: Voti answers: Risposte created: Creato status: Stato action: Azione change: Modifica pending: In attesa filter: placeholder: "Filtra per titolo, domanda:id" answers: page_title: Risposte post: Post votes: Voti created: Creato status: Stato action: Azione change: Cambio filter: placeholder: "Filtra per titolo, domanda:id" general: page_title: Generale name: label: Nome del sito msg: Il nome del sito non può essere vuoto. text: "Il nome di questo sito, come usato nel tag del titolo." site_url: label: URL del sito msg: L'url del sito non può essere vuoto. validate: Inserisci un URL valido. text: L'indirizzo del tuo sito. short_desc: label: Descrizione breve del sito msg: La descrizione breve del sito non può essere vuota. text: "Breve descrizione, come utilizzata nel tag del titolo sulla home page." desc: label: Descrizione del sito msg: La descrizione del sito non può essere vuota. text: "Descrivi questo sito in una frase, come utilizzato nel tag meta description." contact_email: label: Email di contatto msg: L'email del contatto non può essere vuota. validate: Email di contatto non valida. text: Indirizzo e-mail del contatto chiave responsabile di questo sito. check_update: label: Aggiornamenti Software text: Controlla automaticamente gli aggiornamenti interface: page_title: Interfaccia language: label: Lingua dell'interfaccia msg: La lingua dell'interfaccia non può essere vuota. text: La lingua dell'interfaccia utente cambierà quando aggiorni la pagina. time_zone: label: Fuso orario msg: Il fuso orario non può essere vuoto. text: Scegli una città con il tuo stesso fuso orario. avatar: label: Avatar Predefinito text: Per gli utenti senza un proprio avatar personalizzato. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: Protocollo di Trasferimento Posta Semplice from_email: label: Dall'email msg: L'email del contatto non può essere vuota. text: L'indirizzo email da cui vengono inviate le email. from_name: label: Dal nome msg: "Il nome del mittente non può essere vuoto.\n" text: Il nome da cui vengono inviate le email. smtp_host: label: Host SMTP msg: L'host SMTP non può essere vuoto. text: Il tuo server di posta. encryption: label: Crittografia msg: La crittografia non può essere vuota. text: Per la maggior parte dei server SSL è l'opzione consigliata. ssl: SSL tls: TLS none: Nessuna smtp_port: label: Porta SMTP msg: La porta SMTP deve essere numero 1 ~ 65535. text: La porta del tuo server di posta. smtp_username: label: Nome utente SMTP msg: Il nome utente SMTP non può essere vuoto. smtp_password: label: Password SMTP msg: La password SMTP non può essere vuota. test_email_recipient: label: Verifica destinatari email text: Fornisci l'indirizzo email che riceverà i test inviati. msg: Destinatari email di prova non validi smtp_authentication: label: "\nAbilita l'autenticazione" title: Autenticazione SMTP msg: L'autenticazione SMTP non può essere vuota. "yes": "Sì" "no": "No" branding: page_title: Marchio logo: label: Logo msg: Il logo non può essere vuoto. text: L'immagine del logo in alto a sinistra del tuo sito. Utilizza un'immagine rettangolare ampia con un'altezza di 56 e proporzioni superiori a 3:1. Se lasciato vuoto, il testo del titolo del sito verrà mostrato. mobile_logo: label: Logo per versione mobile text: "\nIl logo utilizzato nella versione mobile del tuo sito. Utilizza un'immagine rettangolare ampia con un'altezza di 56. Se lasciata vuota, verrà utilizzata l'immagine dell'impostazione \"logo\"." square_icon: label: Icona quadrata msg: L'icona quadrata non può essere vuota. text: "Immagine utilizzata come base per le icone dei metadata. Idealmente dovrebbe essere più grande di 512x512.\n" favicon: label: Favicon text: Una icona favorita per il tuo sito. Per funzionare correttamente su un CDN deve essere un png. Verrà ridimensionato a 32x32. Se lasciato vuoto, verrà utilizzata l'"icona quadrata". legal: page_title: Legale terms_of_service: label: Termini del servizio text: "Puoi aggiungere qui i termini del contenuto del servizio. Se hai già un documento ospitato altrove, fornisci qui l'URL completo." privacy_policy: label: Informativa sulla privacy text: "Puoi aggiungere il contenuto della politica sulla privacy qui. Se hai già un documento ospitato altrove, fornisci l'URL completo qui." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Risposta a scrivere label: Ogni utente può scrivere una sola risposta per ogni domanda text: "Disattiva per consentire agli utenti di scrivere risposte multiple alla stessa domanda, il che potrebbe causare una risposta sfocata." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Raccomanda tag text: "I tag consigliati verranno mostrati nell'elenco a discesa per impostazione predefinita." msg: contain_reserved: "i tag consigliati non possono contenere tag riservati" required_tag: title: Imposta tag necessari label: Imposta “Raccomanda tag” come tag richiesti text: "Ogni nuova domanda deve avere almeno un tag raccomandato." reserved_tags: label: Tag riservati text: "I tag riservati possono essere utilizzati solo dal moderatore." image_size: label: Dimensione massima dell'immagine (MB) text: "La dimensione massima del caricamento dell'immagine." attachment_size: label: Dimensione massima degli allegati (MB) text: "Dimensione massima del caricamento dei file allegati." image_megapixels: label: Massima immagine megapixel text: "Numero massimo di megapixel consentiti per un'immagine." image_extensions: label: Estensioni di immagini autorizzate text: "Un elenco di estensioni di file consentite per la visualizzazione dell'immagine, separate da virgole." attachment_extensions: label: Estensioni di allegati autorizzate text: "Una lista di estensioni di file consentite per il caricamento, separate con virgole. ATTENZIONE: Consentire i caricamenti potrebbe causare problemi di sicurezza." seo: page_title: SEO permalink: label: Permalink text: Le strutture URL personalizzate possono migliorare l'usabilità e la compatibilità futura dei tuoi link. robots: label: robots.txt text: Questo sovrascriverà definitivamente tutte le impostazioni relative al sito. themes: page_title: Temi themes: label: Temi text: Seleziona un tema esistente. color_scheme: label: Schema colore navbar_style: label: Navbar background style primary_color: label: Colore primario text: Modifica i colori utilizzati dai tuoi temi layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS e HTML custom_css: label: CSS personalizzato text: > head: label: Testata text: > header: label: Intestazione text: > footer: label: Piè di pagina text: Questo verrà inserito prima di </body>. sidebar: label: Barra laterale text: Questo verrà inserito nella barra laterale. login: page_title: Accedi membership: title: Adesione label: Consenti nuove registrazioni text: Disattiva per impedire a chiunque di creare un nuovo account. email_registration: title: Registrazione email label: Consenti registrazione email text: Disattiva per impedire a chiunque di creare un nuovo account tramite email. allowed_email_domains: title: Domini email consentiti text: "Domini email con cui gli utenti devono registrare gli account. Un dominio per riga. Verrà ignorato quando vuoto.\n" private: title: Privato label: Login obbligatorio text: Solo gli utenti registrati possono accedere a questa community. password_login: title: Password di accesso label: Consenti il login di email e password text: "ATTENZIONE: Se disattivi, potresti non essere in grado di accedere se non hai precedentemente configurato un altro metodo di login." installed_plugins: title: Plugin installati plugin_link: I plugin estendono ed espandono le funzionalità. È possibile trovare plugin nel repository <1>Plugin. filter: all: Tutto active: Attivo inactive: Inattivo outdated: Obsoleto plugins: label: Plugin text: Seleziona un plugin esistente. name: Nome version: Versione status: Stato action: Azione deactivate: Disattivare activate: Attivare settings: Impostazioni settings_users: title: Utenti avatar: label: Avatar Predefinito text: Per gli utenti senza un proprio avatar personalizzato. gravatar_base_url: label: Gravatar Base URL text: "\nURL della base API del provider Gravatar. Ignorato quando vuoto." profile_editable: title: Profilo modificabile allow_update_display_name: label: Consenti di cambiare il nome utente allow_update_username: label: Consenti di modificare il proprio nome utente allow_update_avatar: label: Consenti agli utenti di modificare l'immagine del profilo allow_update_bio: label: Consenti agli utenti di modificare la sezione "su di me" allow_update_website: label: Consenti agli utenti di modificare il proprio sito web allow_update_location: label: Consenti agli utenti di modificare la propria posizione privilege: title: Privilegi level: label: Livello di reputazione richiesto text: Scegli la reputazione richiesta per i privilegi msg: should_be_number: l'input dovrebbe essere un numero number_larger_1: il numero dovrebbe essere uguale o superiore a 1 badges: action: Azione active: Attivo activate: Attivare all: Tutto awards: Ricompense deactivate: Disattivare filter: placeholder: Filtra per nome, badge:id group: Gruppo inactive: Inattivo name: Nome show_logs: Visualizza i log status: Stato title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (opzionale) empty: non può essere vuoto invalid: non è valido btn_submit: Salva not_found_props: "Proprietà richiesta {{ key }} non trovata." select: Seleziona page_review: review: Revisione proposed: Proposto question_edit: Edita domanda answer_edit: Edita risposta tag_edit: Edita tag edit_summary: Edita il riepilogo edit_question: Edita la domanda edit_answer: Edita la risposta edit_tag: Edita tag empty: Nessuna attività di revisione rimasta. approve_revision_tip: Approvi questa revisione? approve_flag_tip: Approvi questo contrassegno? approve_post_tip: Approvi questo post? approve_user_tip: Approvate questo utente? suggest_edits: Modifiche suggerite flag_post: Post contrassegnato flag_user: Utente contrassegno queued_post: "Posta in coda\n" queued_user: Utente in coda filter_label: Digita reputation: reputazione flag_post_type: Post contrassegnato come {{ type }}. flag_user_type: Utente contrassegnato come {{ type }}. edit_post: Modifica il post list_post: Lista il post unlist_post: Rimuovi post timeline: undeleted: Non cancellato deleted: eliminato downvote: voto negativo upvote: voto a favore accept: accetta cancelled: Cancellato commented: commentato rollback: ripristino edited: modificato answered: risposto asked: chiesto closed: chiuso reopened: riaperto created: creato pin: Fissa in cima unpin: Rimosso dalla cima show: elencato hide: non elencato title: "Cronologia per" tag_title: "Timeline per" show_votes: "Mostra voti" n_or_a: N/D title_for_question: "Timeline per" title_for_answer: "Timeline per rispondere a {{ title }} di {{ author }}" title_for_tag: "Timeline per tag" datetime: Data e ora type: Tipo by: Di comment: Commento no_data: "Non abbiamo trovato nulla" users: title: Utenti users_with_the_most_reputation: Utenti con i punteggi di reputazione più alti questa settimana users_with_the_most_vote: Utenti che hanno votato di più questa settimana staffs: Lo staff della community reputation: reputazione votes: voti prompt: leave_page: Sei sicuro di voler lasciare questa pagina? changes_not_save: Le modifiche potrebbero non essere salvate. draft: discard_confirm: Sei sicuro di voler eliminare la bozza? messages: post_deleted: Questo post è stato eliminato. post_cancel_deleted: Questo post è stato ripristinato. post_pin: Questo post è stato selezionato. post_unpin: Questo post è stato sbloccato. post_hide_list: Questo post è stato nascosto dall'elenco. post_show_list: Questo post è stato mostrato in elenco. post_reopen: Questo post è stato riaperto. post_list: Questo post è stato inserito. post_unlist: Questo post è stato rimosso. post_pending: Il tuo post è in attesa di revisione. Sarà visibile dopo essere stato approvato. post_closed: Questo post è stato chiuso. answer_deleted: Questa risposta è stata eliminata. answer_cancel_deleted: Questa risposta è stata ripristinata. change_user_role: Il ruolo di questo utente è stato cambiato. user_inactive: Questo utente è già inattivo. user_normal: Questo utente è già normale. user_suspended: Questo utente è stato sospeso. user_deleted: Questo utente è stato eliminato. user_added: User has been added successfully. badge_activated: Questo badge è stato attivato. badge_inactivated: Questo badge è stato disattivato. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/ja_JP.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: 成功 unknown: other: 不明なエラー request_format_error: other: リクエスト形式が無効です。 unauthorized_error: other: 権限がありません。 database_error: other: データサーバーエラー forbidden_error: other: アクセス権限がありません。 duplicate_request_error: other: 重複しています action: report: other: 通報 edit: other: 編集 delete: other: 削除 close: other: 解決済み reopen: other: 再オープン forbidden_error: other: アクセス権限がありません。 pin: other: ピン留めする hide: other: 限定公開にする unpin: other: ピン留め解除 show: other: 限定公開を解除する invite_someone_to_answer: other: 編集 undelete: other: 復元する merge: other: マージ role: name: user: other: ユーザー admin: other: 管理者 moderator: other: モデレーター description: user: other: 一般的なアクセスしか持ちません。 admin: other: すべてにアクセスできる大いなる力を持っています。 moderator: other: 管理者以外のすべての投稿へのアクセス権を持っています。 privilege: level_1: description: other: レベル1 必要最低の評判レベルで利用可(クローズサイト・グループ・特定の人数下での利用) level_2: description: other: レベル2 少しだけ評判レベルが必要(スタートアップコミュニティ・不特定多数の人数での利用) level_3: description: other: レベル3 高い評判レベルが必要(成熟したコミュニティ) level_custom: description: other: カスタムレベル rank_question_add_label: other: 質問する rank_answer_add_label: other: 回答を書く rank_comment_add_label: other: コメントを書く rank_report_add_label: other: 通報 rank_comment_vote_up_label: other: コメントを高評価 rank_link_url_limit_label: other: 一度に2つ以上のリンクを投稿する rank_question_vote_up_label: other: 質問を高評価 rank_answer_vote_up_label: other: 回答を高評価 rank_question_vote_down_label: other: 質問を低評価 rank_answer_vote_down_label: other: 回答を低評価 rank_invite_someone_to_answer_label: other: 誰かを回答に招待する rank_tag_add_label: other: 新しいタグを作成 rank_tag_edit_label: other: タグの説明を編集(レビューが必要) rank_question_edit_label: other: 他の質問を編集(レビューが必要) rank_answer_edit_label: other: 他の回答を編集(レビューが必要) rank_question_edit_without_review_label: other: レビューなしで他の質問を編集する rank_answer_edit_without_review_label: other: レビューなしで他の回答を編集する rank_question_audit_label: other: 質問の編集をレビュー rank_answer_audit_label: other: 回答の編集をレビュー rank_tag_audit_label: other: タグの編集をレビュー rank_tag_edit_without_review_label: other: レビューなしでタグの説明を編集 rank_tag_synonym_label: other: タグの同義語を管理する email: other: メールアドレス e_mail: other: メールアドレス password: other: パスワード pass: other: パスワード old_pass: other: 現在のパスワード original_text: other: 投稿 email_or_password_wrong_error: other: メールアドレスとパスワードが一致しません。 error: common: invalid_url: other: 無効なURL status_invalid: other: 無効なステータス password: space_invalid: other: パスワードにスペースを含めることはできません。 admin: cannot_update_their_password: other: パスワードは変更できません。 cannot_edit_their_profile: other: プロフィールを変更できません。 cannot_modify_self_status: other: ステータスを変更できません。 email_or_password_wrong: other: メールアドレスとパスワードが一致しません。 answer: not_found: other: 回答が見つかりません。 cannot_deleted: other: 削除する権限がありません。 cannot_update: other: 更新する権限がありません。 question_closed_cannot_add: other: 質問はクローズされて、追加できません。 content_cannot_empty: other: 回答を入力してください。 comment: edit_without_permission: other: コメントを編集することはできません。 not_found: other: コメントが見つかりません。 cannot_edit_after_deadline: other: コメント時間が長すぎて変更できません。 content_cannot_empty: other: コメントを入力してください。 email: duplicate: other: メールアドレスは既に存在しています。 need_to_be_verified: other: 電子メールを確認する必要があります。 verify_url_expired: other: メール認証済みURLの有効期限が切れています。メールを再送信してください。 illegal_email_domain_error: other: そのメールドメインからのメールは許可されていません。別のメールアドレスを使用してください。 lang: not_found: other: 言語ファイルが見つかりません。 object: captcha_verification_failed: other: Captchaが間違っています。 disallow_follow: other: フォローが許可されていません。 disallow_vote: other: 投票が許可されていません。 disallow_vote_your_self: other: 自分の投稿には投票できません。 not_found: other: オブジェクトが見つかりません。 verification_failed: other: 認証に失敗しました。 email_or_password_incorrect: other: メールアドレスとパスワードが一致しません。 old_password_verification_failed: other: 古いパスワードの確認に失敗しました。 new_password_same_as_previous_setting: other: 新しいパスワードは前のパスワードと同じです。 already_deleted: other: この投稿は削除されました。 meta: object_not_found: other: メタオブジェクトが見つかりません question: already_deleted: other: この投稿は削除されました。 under_review: other: あなたの投稿はレビュー待ちです。承認されると表示されます。 not_found: other: 質問が見つかりません。 cannot_deleted: other: 削除する権限がありません。 cannot_close: other: クローズする権限がありません。 cannot_update: other: 更新する権限がありません。 content_cannot_empty: other: 内容を入力してください。 content_less_than_minimum: other: 入力された内容の文字数が足りません。 rank: fail_to_meet_the_condition: other: 評判ランクが条件を満たしていません vote_fail_to_meet_the_condition: other: フィードバックをありがとうございます。投票には少なくとも {{.Rank}} の評判が必要です。 no_enough_rank_to_operate: other: 少なくとも {{.Rank}} の評判が必要です。 report: handle_failed: other: レポートの処理に失敗しました。 not_found: other: レポートが見つかりません。 tag: already_exist: other: タグは既に存在します。 not_found: other: タグが見つかりません。 recommend_tag_not_found: other: おすすめタグは存在しません。 recommend_tag_enter: other: 少なくとも 1 つの必須タグを入力してください。 not_contain_synonym_tags: other: 同義語のタグを含めないでください。 cannot_update: other: 更新する権限がありません。 is_used_cannot_delete: other: 使用中のタグは削除できません。 cannot_set_synonym_as_itself: other: 現在のタグの同義語をそのものとして設定することはできません。 minimum_count: other: タグが不足しています。 smtp: config_from_name_cannot_be_email: other: Fromの名前はメールアドレスにできません。 theme: not_found: other: テーマが見つかりません。 revision: review_underway: other: 現在編集できません。レビューキューにバージョンがあります。 no_permission: other: 編集する権限がありません。 user: external_login_missing_user_id: other: サードパーティのプラットフォームは一意のユーザーIDを提供していないため、ログインできません。ウェブサイト管理者にお問い合わせください。 external_login_unbinding_forbidden: other: ログインを削除する前に、アカウントのログインパスワードを設定してください。 email_or_password_wrong: other: other: メールアドレスとパスワードが一致しません。 not_found: other: ユーザーが見つかりません。 suspended: other: このユーザーは凍結されています username_invalid: other: 無効なユーザー名です! username_duplicate: other: ユーザー名は既に使用されています! set_avatar: other: アバターを設定できませんでした cannot_update_your_role: other: ロールを変更できません not_allowed_registration: other: 現在、このサイトは新規登録を受け付けておりません not_allowed_login_via_password: other: 現在、このサイトはパスワードでログインできません access_denied: other: アクセスが拒否されました page_access_denied: other: このページへのアクセス権がありません add_bulk_users_format_error: other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "一度に追加するユーザーの数は、1 -{{.MaxAmount}} の範囲にする必要があります。" status_suspended_forever: other: "このユーザーは永久に停止されました。 このユーザーはコミュニティガイドラインに準拠していません。" status_suspended_until: other: "このユーザーは {{.SuspendedUntil}} まで利用停止となりました。 このユーザーはコミュニティ ガイドラインに準拠していません。" status_deleted: other: "このユーザーは削除されました。" status_inactive: other: "このユーザーは非アクティブです。" config: read_config_failed: other: configの読み込みに失敗しました database: connection_failed: other: データベースの接続が失敗しました create_table_failed: other: テーブルの作成に失敗しました install: create_config_failed: other: config.yaml を作成できません。 upload: unsupported_file_format: other: サポートされていないファイル形式です。 site_info: config_not_found: other: configが見つかりません。 badge: object_not_found: other: バッジオブジェクトが見つかりません reason: spam: name: other: スパム desc: other: この投稿は広告です。現在のトピックには有用ではありません。 rude_or_abusive: name: other: 誹謗中傷 desc: other: "合理的な人は、このコンテンツを尊重する言説には不適切と判断するでしょう。" a_duplicate: name: other: 重複 desc: other: この質問は以前に質問されており、すでに回答があります。 placeholder: other: 既存の質問リンクを入力してください not_a_answer: name: other: 回答では無い desc: other: "これは答えとして投稿されましたが、質問に答えようとしません。 それはおそらく編集、コメント、別の質問、または完全に削除されるべきです。" no_longer_needed: name: other: 必要では無い desc: other: このコメントは古く、この投稿とは関係がありません。 something: name: other: その他 desc: other: 上記以外の理由でスタッフの注意が必要です。 placeholder: other: あなたが懸念していることを私たちに教えてください community_specific: name: other: コミュニティ固有の理由です desc: other: この質問はコミュニティガイドラインを満たしていません not_clarity: name: other: 詳細や明快さが必要です desc: other: この質問には現在複数の質問が含まれています。1つの問題にのみ焦点を当てる必要があります。 looks_ok: name: other: LGTM desc: other: この投稿はそのままで良く、改善する必要はありません! needs_edit: name: other: 編集する必要があったため変更しました。 desc: other: 自分自身でこの投稿の問題を改善し修正します。 needs_close: name: other: クローズする必要がある desc: other: クローズされた質問は回答できませんが、編集、投票、コメントはできます。 needs_delete: name: other: 削除が必要です desc: other: この投稿は削除されました question: close: duplicate: name: other: スパム desc: other: この質問は以前に質問されており、すでに回答があります。 guideline: name: other: コミュニティ固有の理由です desc: other: この質問はコミュニティガイドラインを満たしていません multiple: name: other: 詳細や明快さが必要です desc: other: この質問には現在複数の質問が含まれています。1つの問題にのみ焦点を当てる必要があります。 other: name: other: その他 desc: other: 上記以外の理由でスタッフの注意が必要です。 operation_type: asked: other: 質問済み answered: other: 回答済み modified: other: 修正済み deleted_title: other: 質問を削除 questions_title: other: 質問 tag: tags_title: other: タグ no_description: other: タグには説明がありません。 notification: action: update_question: other: 質問を更新 answer_the_question: other: 回答済みの質問 update_answer: other: 回答を更新 accept_answer: other: 承認された回答 comment_question: other: コメントされた質問 comment_answer: other: コメントされた回答 reply_to_you: other: あなたへの返信 mention_you: other: メンションされました your_question_is_closed: other: あなたの質問はクローズされました your_question_was_deleted: other: あなたの質問は削除されました your_answer_was_deleted: other: あなたの質問は削除されました your_comment_was_deleted: other: あなたのコメントは削除されました up_voted_question: other: 質問を高評価 down_voted_question: other: 質問を低評価 up_voted_answer: other: 回答を高評価 down_voted_answer: other: 回答を低評価 up_voted_comment: other: コメントを高評価 invited_you_to_answer: other: あなたを回答に招待しました earned_badge: other: '"{{.BadgeName}}"バッジを獲得しました' email_tpl: change_email: title: other: "[{{.SiteName}}] 新しいメールアドレスを確認してください" body: other: "{{.SiteName}}の新しいメールアドレスを確認しリンクをクリックしてください。
\n{{.ChangeEmailUrl}}

\n\n身に覚えがない場合はこのメールを無視してください。\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。" new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} があなたの質問に回答しました" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n{{.SiteName}}で確認

\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} があなたを回答に招待しました" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
あなたなら答えを知っているかもしれません。

\n{{.SiteName}}で確認

\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

\n\n配信停止\n" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} があなたの投稿にコメントしました" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nで確認{{.SiteName}}

\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

\n\n配信停止" new_question: title: other: "[{{.SiteName}}] 新しい質問: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\n注: これはシステムから送信される自動メールです。ご返信いただいても返信は表示されませんので、ご返信はご遠慮ください。

\n\n購読解除" pass_reset: title: other: "[{{.SiteName }}] パスワードリセット" body: other: "{{.SiteName}}であなたのパスワードをリセットしようとしました。

\n\nもしお心当たりがない場合は、このメールを無視してください。

\n\n新しいパスワードを設定するには、以下のリンクをクリックしてください。
\n{{.PassResetUrl}}\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

\n" register: title: other: "[{{.SiteName}}] 新しいアカウントを確認" body: other: "{{.SiteName}}へようこそ!

\n\n以下のリンクをクリックして、新しいアカウントを確認・有効化してください。
\n{{.RegisterUrl}}

\n\n上記のリンクがクリックできない場合は、リンクをコピーしてブラウザのアドレスバーに貼り付けてみてください。\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

" test: title: other: "[{{.SiteName}}] テストメール" body: other: "これはテストメールです。\n

\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

" action_activity_type: upvote: other: 高評価 upvoted: other: 高評価しました downvote: other: 低評価 downvoted: other: 低評価しました accept: other: 承認 accepted: other: 承認済み edit: other: 編集 review: queued_post: other: キューに入れられた投稿 flagged_post: other: 投稿を通報 suggested_post_edit: other: 提案された編集 reaction: tooltip: other: "{{ .Names }} と {{ .Count }} もっと..." badge: default_badges: autobiographer: name: other: 自伝作家 desc: other: プロファイル 情報を入力しました。 certified: name: other: 認定済み desc: other: 新しいユーザーがチュートリアルを完了しました。 editor: name: other: 編集者 desc: other: 最初の投稿の編集 first_flag: name: other: 初めての報告 desc: other: 初めての報告 first_upvote: name: other: はじめての高評価 desc: other: はじめて投稿に高評価した first_link: name: other: はじめてのリンク desc: other: 初めて別の投稿へのリンクを追加した。 first_reaction: name: other: 初めてのリアクション desc: other: はじめて投稿にリアクションした first_share: name: other: はじめての共有 desc: other: はじめて投稿を共有した scholar: name: other: 研究生 desc: other: 質問をして回答が承認された commentator: name: other: コメントマン desc: other: 5つのコメントをした new_user_of_the_month: name: other: 今月の新しいユーザー desc: other: 最初の1ヶ月で優れた貢献をした read_guidelines: name: other: ガイドラインを読んだ desc: other: '「コミュニティガイドライン」を読んだ' reader: name: other: リーダー desc: other: 10以上の回答を持つトピックのすべての回答を読んだ welcome: name: other: ようこそ! desc: other: 高評価をされた nice_share: name: other: Nice Share desc: other: 25人の訪問者と投稿を共有した good_share: name: other: Good Share desc: other: 300の訪問者と投稿を共有した great_share: name: other: Great Share desc: other: 1000人の訪問者と投稿を共有した out_of_love: name: other: お隣さん desc: other: 1日に50票いれた higher_love: name: other: お友達 desc: other: 5日目に50回投票した crazy_in_love: name: other: 崇拝 desc: other: 一日に50回投票を20回した promoter: name: other: プロモーター desc: other: ユーザーを招待した campaigner: name: other: キャンペーン desc: other: 3人のベーシックユーザーを招待しました。 champion: name: other: チャンピオン desc: other: 5人のメンバーを招待しました。 thank_you: name: other: Thank you desc: other: 投稿が20件!投票数が10件! gives_back: name: other: 返品 desc: other: 投稿が100件 !?!? 投票数が100件 !?!? empathetic: name: other: 共感性 desc: other: 500の投稿を投票し、1000の投票を与えた。 enthusiast: name: other: 楽天家 desc: other: 10日間連続ログイン aficionado: name: other: Aficionado desc: other: 100日連続ログイン!!! devotee: name: other: 献身者 desc: other: 365日連続訪問!!!!!!!! anniversary: name: other: 周年記念 desc: other: 年に一回は... appreciated: name: other: ありがとう! desc: other: 20件の投稿に1件の投票を受け取った respected: name: other: 尊敬される desc: other: 100件の投稿で2件の投票を受け取った admired: name: other: 崇拝された desc: other: 300の投稿に5票を獲得した solved: name: other: 解決 desc: other: 答えを受け入れられた guidance_counsellor: name: other: アドバイザー desc: other: 10個の回答が承認された know_it_all: name: other: 物知り博士 desc: other: 50個の回答が承認された solution_institution: name: other: 解決機関 desc: other: 150個の回答が承認された nice_answer: name: other: 素敵な回答 desc: other: 回答スコアは10以上!! good_answer: name: other: 良い回答 desc: other: 回答スコアは25以上!?! great_answer: name: other: 素晴らしい回答 desc: other: 回答スコアは50以上!!!! nice_question: name: other: ナイスな質問 desc: other: 質問スコアは10以上!! good_question: name: other: よい質問 desc: other: 質問スコアは25以上!?! great_question: name: other: 素晴らしい質問 desc: other: 50人の閲覧者!! popular_question: name: other: 人気のある質問 desc: other: 500人の閲覧者!!! notable_question: name: other: 注目すべき質問 desc: other: 1,000人の閲覧者!!!! famous_question: name: other: 偉大な質問 desc: other: 5,000人の閲覧者!!!!! popular_link: name: other: 人気のリンク desc: other: 外部リンクを50回クリック hot_link: name: other: 激アツリンク desc: other: 外部リンクを300回クリック famous_link: name: other: 有名なリンク desc: other: 外部リンクを100回クリック default_badge_groups: getting_started: name: other: はじめに community: name: other: コミュニティ posting: name: other: 投稿中 # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: 書式設定 desc: \n pagination: prev: 前へ next: 次へ page_title: question: 質問 questions: 質問 tag: タグ tags: タグ tag_wiki: タグ wiki create_tag: タグを作成 edit_tag: タグを編集 ask_a_question: 質問を作成 edit_question: 質問を編集 edit_answer: 回答を編集 search: 検索 posts_containing: 記事を含む投稿 settings: 設定 notifications: お知らせ login: ログイン sign_up: 新規登録 account_recovery: アカウントの復旧 account_activation: アカウント有効化 confirm_email: メールアドレスを確認 account_suspended: アカウントは凍結されています admin: 管理者 change_email: メールアドレスを変更 install: 回答に応答する upgrade: 回答を改善する maintenance: ウェブサイトのメンテナンス users: ユーザー oauth_callback: 処理中 http_404: HTTP エラー 404 http_50X: HTTP エラー 500 http_403: HTTP エラー 403 logout: ログアウト posts: 投稿 ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: 通知 inbox: 受信トレイ achievement: 実績 new_alerts: 新しい通知 all_read: すべて既読にする show_more: もっと見る someone: 誰か inbox_type: all: すべて posts: 投稿 invites: 招待 votes: 投票 answer: 回答 question: 質問 badge_award: バッジ suspended: title: あなたのアカウントは停止されています。 until_time: "あなたのアカウントは {{ time }} まで停止されました。" forever: このユーザーは永久に停止されました。 end: コミュニティガイドラインを満たしていません。 contact_us: お問い合わせ editor: blockquote: text: 引用 bold: text: 強い chart: text: チャート flow_chart: フローチャート sequence_diagram: シーケンス図 class_diagram: クラス図 state_diagram: 状態図 entity_relationship_diagram: ER図 user_defined_diagram: ユーザー定義図 gantt_chart: ガントチャート pie_chart: 円グラフ code: text: コードサンプル add_code: コードサンプルを追加 form: fields: code: label: コード msg: empty: Code を空にすることはできません。 language: label: 言語 placeholder: 自動検出 btn_cancel: キャンセル btn_confirm: 追加 formula: text: 数式 options: inline: インライン数式 block: ブロック数式 heading: text: 見出し options: h1: 見出し1 h2: 見出し2 h3: 見出し3 h4: 見出し4 h5: 見出し5 h6: 見出し6 help: text: ヘルプ hr: text: 水平方向の罫線 image: text: 画像 add_image: 画像を追加する tab_image: 画像をアップロードする form_image: fields: file: label: 画像ファイル btn: 画像を選択する msg: empty: ファイルは空にできません。 only_image: 画像ファイルのみが許可されています。 max_size: ファイルサイズは {{size}} MBを超えることはできません。 desc: label: 説明 tab_url: 画像URL form_url: fields: url: label: 画像URL msg: empty: 画像のURLは空にできません。 name: label: 説明 btn_cancel: キャンセル btn_confirm: 追加 uploading: アップロード中 indent: text: インデント outdent: text: アウトデント italic: text: 斜体 link: text: ハイパーリンク add_link: ハイパーリンクを追加 form: fields: url: label: URL msg: empty: URLを入力してください。 name: label: 説明 btn_cancel: キャンセル btn_confirm: 追加 ordered_list: text: 順序付きリスト unordered_list: text: 箇条書きリスト table: text: ' テーブル' heading: 見出し cell: セル file: text: ファイルを添付 not_supported: "そのファイルタイプをサポートしていません。 {{file_type}} でもう一度お試しください。" max_size: "添付ファイルサイズは {{size}} MB を超えることはできません。" close_modal: title: この投稿を次のように閉じます... btn_cancel: キャンセル btn_submit: 送信 remark: empty: 入力してください。 msg: empty: 理由を選んでください。 report_modal: flag_title: この投稿を報告するフラグを立てています... close_title: この投稿を次のように閉じます... review_question_title: 質問の編集をレビュー review_answer_title: 答えをレビューする review_comment_title: レビューコメント btn_cancel: キャンセル btn_submit: 送信 remark: empty: 入力してください。 msg: empty: 理由を選んでください。 not_a_url: URL形式が正しくありません。 url_not_match: URL の原点が現在のウェブサイトと一致しません。 tag_modal: title: 新しいタグを作成 form: fields: display_name: label: 表示名 msg: empty: 表示名を入力してください。 range: 表示名は最大 35 文字までです。 slug_name: label: URLスラッグ desc: '文字セット「a-z」、「0-9」、「+ # -」を使用する必要があります。' msg: empty: URL スラッグを空にすることはできません。 range: タイトルは最大35文字までです. character: URL スラグに許可されていない文字セットが含まれています。 desc: label: 説明 revision: label: 修正 edit_summary: label: 概要を編集 placeholder: >- 簡単にあなたの変更を説明します(修正スペル、固定文法、改善されたフォーマット) btn_cancel: キャンセル btn_submit: 送信 btn_post: 新しいタグを投稿 tag_info: created_at: 作成 edited_at: 編集済 history: 履歴 synonyms: title: 類義語 text: 次のタグが再マップされます empty: 同義語は見つかりません。 btn_add: 同義語を追加 btn_edit: 編集 btn_save: 保存 synonyms_text: 次のタグが再マップされます delete: title: このタグを削除 tip_with_posts: >-

同義語でタグを削除することはできません。

最初にこのタグから同義語を削除してください。

tip_with_synonyms: >-

同義語でタグを削除することはできません。

最初にこのタグから同義語を削除してください。

tip: 本当に削除してもよろしいですか? close: クローズ merge: title: タグをマージ source_tag_title: ソース タグ source_tag_description: ソースタグとそれに関連付けられたデータは、ターゲットタグに再マップされます。 target_tag_title: ターゲットタグ target_tag_description: マージ後、これら2つのタグの同義語が作成されます。 no_results: 一致するタグはありません btn_submit: 送信 btn_close: 閉じる edit_tag: title: タグを編集 default_reason: タグを編集 default_first_reason: タグを追加 btn_save_edits: 編集を保存 btn_cancel: キャンセル dates: long_date: MMM D long_date_with_year: "YYYY年MM月D日" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: 今 x_seconds_ago: "{{count}}秒前" x_minutes_ago: "{{count}}分前" x_hours_ago: "{{count}}時間前" hour: 時 day: 日 hours: 時 days: 日 month: 月 months: ヶ月 year: 年 reaction: heart: ハート smile: 笑顔 frown: 眉をひそめる btn_label: リアクションの追加または削除 undo_emoji: '{{ emoji }} のリアクションを元に戻す' react_emoji: '{{ emoji }} に反応する' unreact_emoji: '{{ emoji }} に反応しない' comment: btn_add_comment: コメントを追加 reply_to: 返信: btn_reply: 返信 btn_edit: 編集 btn_delete: 削除 btn_flag: フラグ btn_save_edits: 編集内容を保存 btn_cancel: キャンセル show_more: "{{count}} 件のその他のコメント" tip_question: >- コメントを使用して、より多くの情報を求めたり、改善を提案したりします。コメントで質問に答えることは避けてください。 tip_answer: >- コメントを使用して他のユーザーに返信したり、変更を通知します。新しい情報を追加する場合は、コメントの代わりに投稿を編集します。 tip_vote: 投稿に役に立つものを追加します edit_answer: title: 回答を編集 default_reason: 回答を編集 default_first_reason: 回答を追加 form: fields: revision: label: 修正 answer: label: 回答 feedback: characters: コンテンツは6文字以上でなければなりません。 edit_summary: label: 概要を編集 placeholder: >- 簡単にあなたの変更を説明します(修正スペル、固定文法、改善されたフォーマット) btn_save_edits: 編集を保存 btn_cancel: キャンセル tags: title: タグ sort_buttons: popular: 人気 name: 名前 newest: 最新 button_follow: フォロー button_following: フォロー中 tag_label: 質問 search_placeholder: タグ名でフィルタ no_desc: タグには説明がありません。 more: もっと見る wiki: Wiki ask: title: 質問を作成 edit_title: 質問を編集 default_reason: 質問を編集 default_first_reason: 質問を作成 similar_questions: 類似の質問 form: fields: revision: label: 修正 title: label: タイトル placeholder: どのようなトピックですか?具体的に教えてください。 msg: empty: タイトルを空にすることはできません。 range: タイトルは最大150文字までです body: label: 本文 msg: empty: 本文を空にすることはできません。 hint: optional_body: 質問を記載してください。 minimum_characters: "質問を記載してください。{{min_content_length}} 文字以上の記載が必要です。" tags: label: タグ msg: empty: 少なくとも一つ以上のタグが必要です。 answer: label: 回答 msg: empty: 回答は空欄にできません edit_summary: label: 概要を編集 placeholder: >- 簡単にあなたの変更を説明します(修正スペル、固定文法、改善されたフォーマット) btn_post_question: 質問を投稿する btn_save_edits: 編集内容を保存 answer_question: ご自身の質問に答えてください post_question&answer: 質問と回答を投稿する tag_selector: add_btn: タグを追加 create_btn: 新しタグを作成 search_tag: タグを検索 hint: 内容を記載してください。1つ以上のタグが必要です。 hint_zero_tags: 内容を記載してください。 hint_more_than_one_tag: "内容を記載してください。{{min_content_length}} 文字以上の記載が必要です。" no_result: 一致するタグはありません tag_required_text: 必須タグ (少なくとも 1 つ) header: nav: question: 質問 tag: タグ user: ユーザー badges: バッジ profile: プロフィール setting: 設定 logout: ログアウト admin: 管理者 review: レビュー bookmark: ブックマーク moderation: モデレーション search: placeholder: 検索 footer: build_on: Powered by <1> Apache Answer upload_img: name: 変更 loading: 読み込み中… pic_auth_code: title: CAPTCHA placeholder: 上記のテキストを入力してください msg: empty: Code を空にすることはできません。 inactive: first: >- {{mail}}にアクティベーションメールを送信しました。メールの指示に従ってアカウントをアクティベーションしてください。 info: "届かない場合は、迷惑メールフォルダを確認してください。" another: >- {{mail}}に別のアクティベーションメールを送信しました。 到着には数分かかる場合があります。スパムフォルダを確認してください。 btn_name: 認証メールを再送信 change_btn_name: メールアドレスを変更 msg: empty: 空にすることはできません resend_email: url_label: 本当に認証メールを再送信してもよろしいですか? url_text: ユーザーに上記のアクティベーションリンクを渡すこともできます。 login: login_to_continue: ログインして続行 info_sign: アカウントをお持ちではありませんか?<1>サインアップ info_login: すでにアカウントをお持ちですか?<1>ログイン agreements: 登録することにより、<1>プライバシーポリシーおよび<3>サービス規約に同意するものとします。 forgot_pass: パスワードをお忘れですか? name: label: 名前 msg: empty: 名前を空にすることはできません。 range: 2~30文字の名前を設定してください。 character: '使用可能な文字は、英小字「a-z」、数字「0-9」、記号「- . _ 」のみです' email: label: メールアドレス msg: empty: メールアドレスを入力してください。 password: label: パスワード msg: empty: パスワードを入力してください。 different: 入力されたパスワードが一致しません account_forgot: page_title: パスワードを忘れた方はこちら btn_name: 回復用のメールを送る send_success: >- アカウントが {{mail}}と一致する場合は、パスワードをすぐにリセットする方法に関するメールが送信されます。 email: label: メールアドレス msg: empty: メールアドレスを入力してください。 change_email: btn_cancel: キャンセル btn_update: メールアドレスを更新 send_success: >- アカウントが {{mail}}と一致する場合は、パスワードをすぐにリセットする方法に関するメールが送信されます。 email: label: 新しいメールアドレス msg: empty: メールアドレス欄を空白にしておくことはできません oauth: connect: '{{ auth_name }} と接続' remove: '{{ auth_name }} を削除' oauth_bind_email: subtitle: アカウントに回復用のメールアドレスを追加します。 btn_update: メールアドレスを更新 email: label: メールアドレス msg: empty: メールアドレスは空にできません。 modal_title: このメールアドレスはすでに存在しています。 modal_content: このメールアドレスは既に登録されています。既存のアカウントに接続してもよろしいですか? modal_cancel: メールアドレスを変更する modal_confirm: 既存のアカウントに接続 password_reset: page_title: パスワード再設定 btn_name: パスワードをリセット reset_success: >- パスワードを変更しました。ログインページにリダイレクトされます。 link_invalid: >- 申し訳ありませんが、このパスワードリセットのリンクは無効になりました。パスワードが既にリセットされている可能性がありますか? to_login: ログインページへ password: label: パスワード msg: empty: パスワードを入力してください。 length: 長さは 8 から 32 の間である必要があります different: 両側に入力されたパスワードが一致しません password_confirm: label: 新しいパスワードの確認 settings: page_title: 設定 goto_modify: 変更を開く nav: profile: プロフィール notification: お知らせ account: アカウント interface: 外観 profile: heading: プロフィール btn_name: 保存 display_name: label: 表示名 msg: 表示名は必須です。 msg_range: 表示名は 2 ~ 30 文字で入力してください。 username: label: ユーザー名 caption: ユーザーは "@username" としてあなたをメンションできます。 msg: ユーザー名は空にできません。 msg_range: ユーザー名は2 ~ 30文字で入力してください。 character: '使用可能な文字は、英小字「a-z」、数字「0-9」、記号「- . _ 」のみです' avatar: label: プロフィール画像 gravatar: Gravatar gravatar_text: 画像を変更できます: custom: カスタム custom_text: 画像をアップロードできます。 default: システム msg: アバターをアップロードしてください bio: label: 自己紹介 website: label: ウェブサイト placeholder: "http://example.com" msg: ウェブサイトの形式が正しくありません location: label: ロケーション placeholder: "都道府県,国" notification: heading: メール通知 turn_on: ONにする inbox: label: 受信トレイの通知 description: 質問、コメント、招待状などへの回答。 all_new_question: label: すべての新規質問 description: すべての新しい質問の通知を受け取ります。週に最大50問まで。 all_new_question_for_following_tags: label: 以下のタグに対するすべての新しい質問 description: タグをフォローするための新しい質問の通知を受け取る。 account: heading: アカウント change_email_btn: メールアドレスを変更する change_pass_btn: パスワードを変更する change_email_info: >- このアドレスにメールを送信しました。メールの指示に従って確認処理を行ってください。 email: label: メールアドレス new_email: label: 新しいメールアドレス msg: 新しいメールアドレスは空にできません。 pass: label: 現在のパスワード msg: パスワードは空白にできません password_title: パスワード current_pass: label: 現在のパスワード msg: empty: 現在のパスワードが空欄です length: 長さは 8 から 32 の間である必要があります. different: パスワードが一致しません。 new_pass: label: 新しいパスワード pass_confirm: label: 新しいパスワードの確認 interface: heading: 外観 lang: label: インタフェース言語 text: ユーザーインターフェイスの言語。ページを更新すると変更されます。 my_logins: title: ログイン情報 label: これらのアカウントを使用してログインまたはこのサイトでサインアップします。 modal_title: ログイン情報を削除 modal_content: このログイン情報をアカウントから削除してもよろしいですか? modal_confirm_btn: 削除 remove_success: 削除に成功しました toast: update: 更新に成功しました update_password: パスワードの変更に成功しました。 flag_success: フラグを付けてくれてありがとう forbidden_operate_self: 自分自身で操作することはできません review: レビュー後にあなたのリビジョンが表示されます。 sent_success: 正常に送信されました。 related_question: title: 関連 answers: 回答 linked_question: title: リンク済 description: リンクされた投稿 no_linked_question: このコンテンツからリンクされたコンテンツはありません。 invite_to_answer: title: 回答をリクエスト desc: 答えられそうな人を招待します。 invite: 回答に招待する add: ユーザーを追加 search: ユーザーを検索 question_detail: action: 動作 created: 作成済 Asked: 質問済み asked: 質問済み update: 修正済み Edited: 編集済 edit: 編集済み commented: コメントしました Views: 閲覧回数 Follow: フォロー Following: フォロー中 follow_tip: この質問をフォローして通知を受け取る answered: 回答済み closed_in: クローズまで show_exist: 既存の質問を表示します useful: 役に立った question_useful: それは有用で明確です。 question_un_useful: 不明確または有用ではない question_bookmark: この質問をブックマークする answer_useful: 役に立った answer_un_useful: 役に立たない answers: title: 回答 score: スコア newest: 最新 oldest: 古い順 btn_accept: 承認 btn_accepted: 承認済み write_answer: title: あなたの回答 edit_answer: 既存の回答を編集する btn_name: 回答を投稿する add_another_answer: 別の回答を追加 confirm_title: 回答を続ける continue: 続行 confirm_info: >-

本当に別の答えを追加したいのですか?

代わりに、編集リンクを使って既存の答えを洗練させ、改善することができます。

empty: 回答は空欄にできません characters: コンテンツは6文字以上でなければなりません。 tips: header_1: ご回答ありがとうございます。 li1_1: " 必ず質問に答えてください。詳細を述べ、あなたの研究を共有してください。\n" li1_2: 参考文献や個人的な経験による裏付けを取ること。. header_2: しかし、 を避けてください... li2_1: 助けを求める、説明を求める、または他の答えに応答する。 reopen: confirm_btn: 再オープン title: この投稿を再度開く content: 再オープンしてもよろしいですか? list: confirm_btn: 一覧 title: この投稿の一覧 content: 一覧表示してもよろしいですか? unlist: confirm_btn: 限定公開にする title: この投稿を元に戻す content: 本当に元に戻しますか? pin: title: この投稿をピン留めする content: "グローバルに固定してもよろしいですか?\nこの投稿はすべての投稿リストの上部に表示されます。" confirm_btn: ピン留めする delete: title: この投稿を削除 question: >-

承認された回答を削除することはお勧めしません。削除すると、今後の読者がこの知識を得られなくなってしまうからです。

承認された回答を繰り返し削除すると、回答機能が制限され、アカウントがブロックされる場合があります。本当に削除しますか? answer_accepted: >-

承認された回答を削除することはお勧めしません。削除すると、今後の読者がこの知識を得られなくなってしまうからです。

承認された回答を繰り返し削除すると、回答機能が制限され、アカウントがブロックされる場合があります。本当に削除しますか? other: 本当に削除してもよろしいですか? tip_answer_deleted: この回答は削除されました undelete_title: この投稿を元に戻す undelete_desc: 本当に元に戻しますか? btns: confirm: 確認 cancel: キャンセル edit: 編集 save: 保存 delete: 削除 undelete: 元に戻す list: 限定公開を解除する unlist: 限定公開にする unlisted: 限定公開済み login: ログイン signup: 新規登録 logout: ログアウト verify: 認証 create: 作成 approve: 承認 reject: 却下 skip: スキップする discard_draft: 下書きを破棄 pinned: ピン留めしました all: すべて question: 質問 answer: 回答 comment: コメント refresh: 更新 resend: 再送 deactivate: 無効化する active: 有効 suspend: 凍結 unsuspend: 凍結解除 close: クローズ reopen: 再オープン ok: OK light: ライト dark: ダーク system_setting: システム設定 default: 既定 reset: リセット tag: タグ post_lowercase: 投稿 filter: フィルター ignore: 除外 submit: 送信 normal: 通常 closed: クローズ済み deleted: 削除済み deleted_permanently: 完全に削除する pending: 処理待ち more: もっと見る view: 表示方法 card: カード compact: コンパクト display_below: 以下に表示 always_display: 常に表示 or: または back_sites: サイトに戻る search: title: 検索結果 keywords: キーワード options: オプション follow: フォロー following: フォロー中 counts: "結果:{{count}}" counts_loading: "... 結果" more: もっと見る sort_btns: relevance: 関連性 newest: 最新 active: アクティブ score: スコア more: もっと見る tips: title: 詳細検索のヒント tag: "<1>[tag] タグで検索" user: "<1>ユーザー:ユーザー名作成者による検索" answer: "<1>回答:0未回答の質問" score: "<1>スコア:33以上のスコアを持つ投稿" question: "<1>質問質問を検索" is_answer: "<1>は答え答えを検索" empty: 何も見つかりませんでした。
別のキーワードまたはそれ以下の特定のキーワードをお試しください。 share: name: シェア copy: リンクをコピー via: 投稿を共有... copied: コピーしました facebook: Facebookで共有 twitter: Xでシェア cannot_vote_for_self: 自分の投稿には投票できません。 modal_confirm: title: エラー... delete_permanently: title: 完全に削除する content: 完全に削除しても良いですか? account_result: success: 新しいアカウントが確認されました。ホームページにリダイレクトされます。 link: ホームページへ oops: おっと! invalid: 使用したリンクは機能しません。 confirm_new_email: メールアドレスが更新されました。 confirm_new_email_invalid: >- 申し訳ありませんが、この確認リンクは無効です。メールアドレスが既に変更されている可能性があります。 unsubscribe: page_title: 購読解除 success_title: 購読解除成功 success_desc: 配信リストから削除され、その他のメールの送信が停止されました。 link: 設定の変更 question: following_tags: フォロー中のタグ edit: 編集 save: 保存 follow_tag_tip: タグに従って質問のリストをキュレートします。 hot_questions: ホットな質問 all_questions: すべての質問 x_questions: "{{ count }} の質問" x_answers: "{{ count }} の回答" x_posts: "{{ count }} の回答" questions: 質問 answers: 回答 newest: 最新 active: 有効 hot: 人気 frequent: 関心順 recommend: おすすめ score: スコア unanswered: 未回答 modified: 修正済み answered: 回答済み asked: 質問済み closed: 解決済み follow_a_tag: タグをフォロー more: その他 personal: overview: 概要 answers: 回答 answer: 回答 questions: 質問 question: 質問 bookmarks: ブックマーク reputation: 評判 comments: コメント votes: 投票 badges: バッジ newest: 最新 score: スコア edit_profile: プロファイルを編集 visited_x_days: "{{ count }}人の閲覧者" viewed: 閲覧回数 joined: 参加しました comma: "," last_login: 閲覧数 about_me: 自己紹介 about_me_empty: "// Hello, World !" top_answers: よくある回答 top_questions: よくある質問 stats: 統計情報 list_empty: 投稿が見つかりませんでした。
他のタブを選択しますか? content_empty: 投稿が見つかりませんでした。 accepted: 承認済み answered: 回答済み asked: 質問済み downvoted: 低評価しました mod_short: MOD mod_long: モデレーター x_reputation: 評価 x_votes: 投票を受け取りました x_answers: 回答 x_questions: 質問 recent_badges: 最近のバッジ install: title: Installation next: 次へ done: 完了 config_yaml_error: config.yaml を作成できません。 lang: label: 言語を選択してください db_type: label: データベースエンジン db_username: label: ユーザー名 placeholder: root msg: ユーザー名は空にできません。 db_password: label: パスワード placeholder: root msg: パスワードを入力してください。 db_host: label: データベースのホスト。 placeholder: "db:3306" msg: データベースホストは空にできません。 db_name: label: データベース名 placeholder: 回答 msg: データベース名を空にすることはできません。 db_file: label: データベースファイル placeholder: /data/answer.db msg: データベースファイルは空にできません。 ssl_enabled: label: SSLを有効化 ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL モード ssl_root_cert: placeholder: sslrootcert ファイルパス msg: sslrootcert ファイルパスは空にできません ssl_cert: placeholder: sslcert ファイルパス msg: sslcert ファイルパスは空にできません ssl_key: placeholder: sslkey ファイルパス msg: sslkey ファイルパスは空にできません config_yaml: title: config.yamlを作成 label: config.yaml ファイルが作成されました。 desc: >- <1>/var/www/xxx/ディレクトリに<1>config.yamlファイルを手動で作成し、その中に次のテキストを貼り付けます。 info: 完了したら、「次へ」ボタンをクリックします。 site_information: サイト情報 admin_account: 管理者アカウント site_name: label: サイト名: msg: サイト名は空にできません. msg_max_length: サイト名は最大30文字でなければなりません。 site_url: label: サイトURL text: あなたのサイトのアドレス msg: empty: サイト URL は空にできません. incorrect: サイトURLの形式が正しくありません。 max_length: サイトのURLは最大512文字でなければなりません contact_email: label: 連絡先メール アドレス text: このサイトを担当するキーコンタクトのメールアドレスです。 msg: empty: 連絡先メールアドレスを空にすることはできません。 incorrect: 連絡先メールアドレスの形式が正しくありません。 login_required: label: 非公開 switch: ログインが必要です text: ログインしているユーザーのみがこのコミュニティにアクセスできます。 admin_name: label: 名前 msg: 名前を空にすることはできません。 character: '使用可能な文字は、英小字「a-z」、数字「0-9」、記号「- . _ 」のみです' msg_max_length: 名前は2 ~ 30文字で入力してください。 admin_password: label: パスワード text: >- ログインするにはこのパスワードが必要です。安全な場所に保存してください。 msg: パスワードは空白にできません msg_min_length: パスワードは8文字以上でなければなりません。 msg_max_length: パスワードは最大 32 文字でなければなりません。 admin_confirm_password: label: "パスワードの確認" text: "確認のため、パスワードを再入力してください。" msg: "確認用パスワードが一致しません" admin_email: label: メールアドレス text: ログインするにはこのメールアドレスが必要です。 msg: empty: メールアドレスは空にできません。 incorrect: メールアドレスの形式が正しくありません. ready_title: サイトの準備ができました ready_desc: >- もっと設定を変更したいと思ったことがある場合は、<1>管理者セクションをご覧ください。サイトメニューで見つけてください。 good_luck: "楽しんで、幸運を!" warn_title: 警告 warn_desc: >- ファイル<1>config.yamlは既に存在します。このファイルのいずれかの設定アイテムをリセットする必要がある場合は、最初に削除してください。 install_now: <1>今すぐインストールを試してみてください。 installed: 既にインストール済みです installed_desc: >- 既にインストールされているようです。再インストールするには、最初に古いデータベーステーブルをクリアしてください。 db_failed: データベースの接続が失敗しました db_failed_desc: >- これは、<1>設定内のデータベース情報を意味します。 amlファイルが正しくないか、データベースサーバーとの連絡先が確立できませんでした。ホストのデータベースサーバーがダウンしている可能性があります。 counts: views: ビュー votes: 投票数 answers: 回答 accepted: 承認済み page_error: http_error: HTTP Error {{ code }} desc_403: このページにアクセスする権限がありません。 desc_404: 残念ながら、このページは存在しません。 desc_50X: サーバーにエラーが発生し、リクエストを完了できませんでした。 back_home: ホームページに戻ります page_maintenance: desc: "メンテナンス中です。まもなく戻ります。" nav_menus: dashboard: ダッシュボード contents: コンテンツ questions: 質問 answers: 回答 users: ユーザー badges: バッジ flags: フラグ settings: 設定 general: 一般 interface: 外観 smtp: SMTP branding: ブランディング legal: 法的事項 write: 編集 terms: 規約 tos: 利用規約 privacy: プライバシー seo: SEO customize: カスタマイズ themes: テーマ login: ログイン privileges: 特典 plugins: プラグイン installed_plugins: 使用中のプラグイン apperance: 外観 community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: '{{site_name}} へようこそ' user_center: login: ログイン qrcode_login_tip: QRコードをスキャンしてログインするには {{ agentName }} を使用してください。 login_failed_email_tip: ログインに失敗しました。もう一度やり直す前に、このアプリがあなたのメール情報にアクセスすることを許可してください。 badges: modal: title: お疲れ様でした! content: 新しいバッジを獲得しました。 close: クローズ confirm: バッジを表示 title: バッジ awarded: 受賞済み earned_×: 獲得×{{ number }} ×_awarded: "{{ number }} 受賞" can_earn_multiple: これを複数回獲得できます。 earned: 獲得済み admin: admin_header: title: 管理者 dashboard: title: ダッシュボード welcome: Adminへようこそ! site_statistics: サイト統計 questions: "質問:" resolved: "解決済み:" unanswered: "未回答:" answers: "回答:" comments: "評論:" votes: "投票:" users: "ユーザー数:" flags: "フラグ:" reviews: "レビュー:" site_health: サイトの状態 version: "バージョン:" https: "HTTPS:" upload_folder: "フォルダを上げる" run_mode: "実行中モード:" private: 非公開 public: 公開 smtp: "SMTP:" timezone: "Timezone:" system_info: システム情報 go_version: "バージョン:" database: "データベース:" database_size: "データベースのサイズ:" storage_used: "使用されているストレージ" uptime: "稼働時間:" links: リンク plugins: プラグイン github: GitHub blog: ブログ contact: 連絡先 forum: Forum documents: ドキュメント feedback: フィードバック support: サポート review: レビュー config: 設定 update_to: 更新日時 latest: 最新 check_failed: チェックに失敗しました "yes": "はい" "no": "いいえ" not_allowed: 許可されていません allowed: 許可 enabled: 有効 disabled: 無効 writable: 書き込み可 not_writable: 書き込み不可 flags: title: フラグ pending: 処理待ち completed: 完了済 flagged: フラグ付き flagged_type: フラグを立てた {{ type }} created: 作成 action: 動作 review: レビュー user_role_modal: title: ユーザーロールを変更... btn_cancel: キャンセル btn_submit: 送信 new_password_modal: title: 新しいパスワードを設定 form: fields: password: label: パスワード text: ユーザーはログアウトされ、再度ログインする必要があります。 msg: パスワードの長さは 8 ~ 32 文字である必要があります。 btn_cancel: キャンセル btn_submit: 送信 edit_profile_modal: title: プロファイルを編集 form: fields: display_name: label: 表示名 msg_range: 表示名は 2 ~ 30 文字で入力してください。 username: label: ユーザー名 msg_range: ユーザー名は 2 ~ 30 文字で入力してください。 email: label: メールアドレス msg_invalid: 無効なメールアドレス edit_success: 更新が成功しました btn_cancel: キャンセル btn_submit: 送信 user_modal: title: 新しいユーザーを追加 form: fields: users: label: ユーザーを一括追加 placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: '「名前、メールアドレス、パスワード」をカンマで区切ってください。' msg: "ユーザーのメールアドレスを1行に1つ入力してください。" display_name: label: 表示名 msg: 表示名の長さは 2 ~ 30 文字にする必要があります。 email: label: メールアドレス msg: メールアドレスが無効です。 password: label: パスワード msg: パスワードの長さは 8 ~ 32 文字である必要があります。 btn_cancel: キャンセル btn_submit: 送信 users: title: ユーザー name: 名前 email: メールアドレス reputation: 評価 created_at: 作成日時 delete_at: 削除日時 suspend_at: 凍結時間 suspend_until: まで凍結 status: ステータス role: ロール action: 操作 change: 変更 all: すべて staff: スタッフ more: もっと見る inactive: 非アクティブ suspended: 凍結済み deleted: 削除済み normal: 通常 Moderator: モデレーター Admin: 管理者 User: ユーザー filter: placeholder: "ユーザー名でフィルタ" set_new_password: 新しいパスワードを設定します。 edit_profile: プロファイルを編集 change_status: ステータスを変更 change_role: ロールを変更 show_logs: ログを表示 add_user: ユーザを追加 deactivate_user: title: ユーザーを非アクティブにする content: アクティブでないユーザーはメールアドレスを再認証する必要があります。 delete_user: title: このユーザの削除 content: このユーザーを削除してもよろしいですか?これは永久的です! remove: このコンテンツを削除 label: すべての質問、回答、コメントなどを削除 text: ユーザーのアカウントのみ削除したい場合は、これを確認しないでください。 suspend_user: title: ユーザーをサスペンドにする content: 一時停止中のユーザーはログインできません。 label: いつまで凍結しますか? forever: 無期限 questions: page_title: 質問 unlisted: 限定公開済み post: 投稿 votes: 投票 answers: 回答 created: 作成 status: ステータス action: 動作 change: 変更 pending: 処理待ち filter: placeholder: "タイトル、質問:id でフィルター" answers: page_title: 回答 post: 投稿 votes: 投票 created: 作成 status: ステータス action: 操作 change: 変更 filter: placeholder: "タイトル、質問:id でフィルター" general: page_title: 一般 name: label: サイト名 msg: サイト名は空にできません. text: "タイトルタグで使用されるこのサイトの名前。" site_url: label: サイトURL msg: サイト URL は空にできません. validate: 正しいURLを入力してください。 text: あなたのサイトのアドレス short_desc: label: 短いサイトの説明 msg: 短いサイト説明は空にできません. text: "ホームページのタイトルタグで使用されている簡単な説明。" desc: label: サイトの説明 msg: サイト説明を空にすることはできません。 text: "メタ説明タグで使用されるように、このサイトを1つの文で説明します。" contact_email: label: 連絡先メール アドレス msg: 連絡先メールアドレスを空にすることはできません。 validate: 連絡先のメールアドレスが無効です。 text: このサイトを担当するキーコンタクトのメールアドレスです。 check_update: label: ソフトウェアアップデート text: 自動的に更新を確認 interface: page_title: 外観 language: label: インタフェース言語 msg: インターフェース言語は空にできません。 text: ユーザーインターフェイスの言語。ページを更新すると変更されます。 time_zone: label: タイムゾーン msg: タイムゾーンを空にすることはできません。 text: あなたのタイムゾーンを選択してください。 avatar: label: デフォルトのアバター text: 独自のカスタムアバターを持たないユーザー向け。 gravatar_base_url: label: GravatarのベースURL text: GravatarプロバイダーのAPIベースのURL。空の場合は無視されます。 smtp: page_title: SMTP from_email: label: 差出人 msg: 差出人メールアドレスは空にできません。 text: 送信元のメールアドレス from_name: label: 差出人名 msg: 差出人名は空にできません text: メールの送信元の名前 smtp_host: label: SMTP ホスト msg: SMTPホストは空にできません。 text: メールサーバー encryption: label: 暗号化 msg: 暗号化は空にできません。 text: ほとんどのサーバではSSLが推奨されます。 ssl: SSL tls: TLS none: なし smtp_port: label: SMTPポート msg: SMTPポートは1〜65535でなければなりません。 text: メールサーバーへのポート番号 smtp_username: label: SMTPユーザ名 msg: SMTP ユーザー名を空にすることはできません。 smtp_password: label: SMTPパスワード msg: SMTP パスワードを入力してください。 test_email_recipient: label: テストメールの受信者 text: テスト送信を受信するメールアドレスを入力してください。 msg: テストメールの受信者が無効です smtp_authentication: label: 認証を有効にする title: SMTP認証 msg: SMTP認証は空にできません。 "yes": "はい" "no": "いいえ" branding: page_title: ブランディング logo: label: ロゴ msg: ロゴは空にできません。 text: あなたのサイトの左上にあるロゴ画像。 高さが56、アスペクト比が3:1を超える広い矩形画像を使用します。 空白の場合、サイトタイトルテキストが表示されます。 mobile_logo: label: モバイルロゴ text: サイトのモバイル版で使用されるロゴです。高さが56の横長の長方形の画像を使用してください。空白のままにすると、"ロゴ"設定の画像が使用されます。 square_icon: label: アイコン画像 msg: アイコン画像は空にできません。 text: メタデータアイコンのベースとして使用される画像。理想的には512x512より大きくなければなりません。 favicon: label: Favicon text: あなたのサイトのファビコン。CDN上で正しく動作するにはpngである必要があります。 32x32にリサイズされます。空白の場合は、"正方形のアイコン"が使用されます。 legal: page_title: 法的情報 terms_of_service: label: 利用規約 text: "ここで利用規約のサービスコンテンツを追加できます。すでに他の場所でホストされているドキュメントがある場合は、こちらにフルURLを入力してください。" privacy_policy: label: プライバシーポリシー text: "ここにプライバシーポリシーの内容を追加できます。すでに他の場所でホストされているドキュメントを持っている場合は、こちらにフルURLを入力してください。" external_content_display: label: 外部コンテンツ text: "コンテンツには、外部ウェブサイトから埋め込まれた画像、ビデオ、およびメディアが含まれます" always_display: 常に外部コンテンツを表示する ask_before_display: 外部コンテンツを表示する前に確認する write: page_title: Files min_content: label: 質問に必要な文字数 text: 質問の投稿に必要な本文の文字数です。 restrict_answer: title: 回答を書く label: 各ユーザーは同じ質問に対して1つの回答しか書けません text: "ユーザが同じ質問に複数の回答を書き込めるようにするにはオフにします。これにより回答がフォーカスされていない可能性があります。" min_tags: label: "質問に必要なタグ数" text: "質問の投稿に必要なタグの数です。" recommend_tags: label: おすすめタグ text: "デフォルトでドロップダウンリストに推奨タグが表示されます。" msg: contain_reserved: "推奨されるタグは予約済みタグを含めることはできません" required_tag: title: 必須タグを設定 label: 必須タグに「推奨タグ」を設定 text: "新しい質問には少なくとも1つの推奨タグが必要です。" reserved_tags: label: 予約済みタグ text: "予約済みのタグはモデレータのみ使用できます。" image_size: label: 画像ファイルの最大サイズ(MB) text: "画像ファイルの最大アップロードサイズ。" attachment_size: label: 添付ファイルの最大サイズ (MB) text: "添付ファイルの最大アップロードサイズ。" image_megapixels: label: 画像の最大解像度 text: "画像ファイルに許可する最大メガピクセル数。" image_extensions: label: 認可された画像ファイルの拡張子 text: "イメージ表示に許可されるファイル拡張子のリスト(コンマで区切り)" attachment_extensions: label: 認可された添付ファイルの拡張子 text: "アップロードを許可するファイル拡張子のリスト(カンマ区切り)\n警告: アップロードを許可するとセキュリティ上の問題が発生する可能性があります。" seo: page_title: SEO permalink: label: 固定リンク text: カスタム URL 構造は、ユーザビリティとリンクの前方互換性を向上させることができます。 robots: label: robots.txt text: これにより、関連するサイト設定が永久に上書きされます。 themes: page_title: テーマ themes: label: テーマ text: 既存のテーマを選択してください color_scheme: label: 配色 navbar_style: label: ナビゲーションバーの背景スタイル primary_color: label: メインカラー text: テーマで使用される色を変更する layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS と HTML custom_css: label: カスタム CSS text: > head: label: ヘッド text: > header: label: ヘッダー text: > footer: label: フッター text: これは </body> の前に挿入されます。 sidebar: label: サイドバー text: サイドバーに挿入されます。 login: page_title: ログイン membership: title: メンバー label: 新しい登録を許可する text: 誰もが新しいアカウントを作成できないようにするには、オフにしてください。 email_registration: title: メールアドレスの登録 label: メールアドレスの登録を許可 text: オフにすると、メールで新しいアカウントを作成できなくなります。 allowed_email_domains: title: 許可されたメールドメイン text: ユーザーがアカウントを登録する必要があるメールドメインです。1行に1つのドメインがあります。空の場合は無視されます。 private: title: 非公開 label: ログインが必要です text: ログインしているユーザーのみがこのコミュニティにアクセスできます。 password_login: title: パスワードログイン label: メールアドレスとパスワードのログインを許可する text: "警告: オフにすると、他のログイン方法を設定していない場合はログインできない可能性があります。" installed_plugins: title: インストール済みプラグイン plugin_link: プラグインは機能を拡張します。<1>プラグインリポジトリにプラグインがあります。 filter: all: すべて active: アクティブ inactive: 非アクティブ outdated: 期限切れ plugins: label: プラグイン text: 既存のプラグインを選択します name: 名前 version: バージョン status: ステータス action: 操作 deactivate: 非アクティブ化 activate: アクティベート settings: 設定 settings_users: title: ユーザー avatar: label: デフォルトのアバター text: 自分のカスタムアバターのないユーザー向け。 gravatar_base_url: label: Gravatar Base URL text: GravatarプロバイダーのAPIベースのURL。空の場合は無視されます。 profile_editable: title: プロフィール編集可能 allow_update_display_name: label: ユーザーが表示名を変更できるようにする allow_update_username: label: ユーザー名の変更を許可する allow_update_avatar: label: ユーザーのプロフィール画像の変更を許可する allow_update_bio: label: ユーザーが自分についての変更を許可する allow_update_website: label: ユーザーのウェブサイトの変更を許可する allow_update_location: label: ユーザーの位置情報の変更を許可する privilege: title: 特権 level: label: 評判の必要レベル text: 特権に必要な評判を選択します msg: should_be_number: 入力は数値でなければなりません number_larger_1: 数値は 1 以上でなければなりません badges: action: 操作 active: アクティブ activate: アクティベート all: すべて awards: 賞 deactivate: 非アクティブ化 filter: placeholder: 名前、バッジ:id でフィルター group: グループ inactive: 非アクティブ name: 名前 show_logs: ログを表示 status: ステータス title: バッジ apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (任意) empty: 空にすることはできません invalid: 無効です btn_submit: 保存 not_found_props: "必須プロパティ {{ key }} が見つかりません。" select: 選択 page_review: review: レビュー proposed: 提案された question_edit: 質問の編集 answer_edit: 回答の編集 tag_edit: タグの編集 edit_summary: 概要を編集 edit_question: 質問を編集 edit_answer: 回答を編集 edit_tag: タグを編集 empty: レビュータスクは残っていません。 approve_revision_tip: この修正を承認しますか? approve_flag_tip: このフラグを承認しますか? approve_post_tip: この投稿を承認しますか? approve_user_tip: このユーザーを承認しますか? suggest_edits: 提案された編集 flag_post: 報告された投稿 flag_user: 報告されたユーザー queued_post: キューに入れられた投稿 queued_user: キューに入れられたユーザー filter_label: タイプ reputation: 評価 flag_post_type: この投稿は {{ type }} として報告されました flag_user_type: このユーザーは {{ type }} として報告されました edit_post: 投稿を編集 list_post: 投稿一覧 unlist_post: 限定公開投稿 timeline: undeleted: 復元する deleted: 削除済み downvote: 低評価 upvote: 高評価 accept: 承認 cancelled: キャンセル済み commented: コメントしました rollback: rollback edited: 編集済み answered: 回答済み asked: 質問済み closed: クローズ済み reopened: 再オープン created: 作成済み pin: ピン留め済 unpin: ピン留め解除 show: 限定公開解除済み hide: 限定公開済み title: "履歴:" tag_title: "タイムライン:" show_votes: "投票を表示" n_or_a: N/A title_for_question: "タイムライン:" title_for_answer: "{{ title }} の {{ author }} 回答のタイムライン" title_for_tag: "タグのタイムライン:" datetime: 日付時刻 type: タイプ by: By comment: コメント no_data: "何も見つけられませんでした" users: title: ユーザー users_with_the_most_reputation: 今週最も高い評価スコアを持つユーザ users_with_the_most_vote: 今週一番多く投票したユーザー staffs: コミュニティのスタッフ reputation: 評価 votes: 投票 prompt: leave_page: このページから移動してもよろしいですか? changes_not_save: 変更が保存されない可能性があります draft: discard_confirm: 下書きを破棄してもよろしいですか? messages: post_deleted: この投稿は削除されました。 post_cancel_deleted: この投稿の削除が取り消されました。 post_pin: この投稿はピン留めされています。 post_unpin: この投稿のピン留めが解除されました。 post_hide_list: この投稿は一覧から非表示になっています。 post_show_list: この投稿は一覧に表示されています。 post_reopen: この投稿は再オープンされました。 post_list: この投稿は一覧に表示されています。 post_unlist: この投稿は一覧に登録されていません。 post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: この投稿はクローズされました。 answer_deleted: この回答は削除されました。 answer_cancel_deleted: この回答の削除が取り消されました。 change_user_role: このユーザーのロールが変更されました。 user_inactive: このユーザーは既に無効です。 user_normal: このユーザーは既に有効です。 user_suspended: このユーザーは凍結されています。 user_deleted: このユーザーは削除されました。 user_added: User has been added successfully. badge_activated: このバッジは有効化されました。 badge_inactivated: このバッジは無効化されています。 users_deleted: このユーザーは削除されました。 posts_deleted: この質問は削除されています。 answers_deleted: この回答は削除されています。 copy: クリップボードにコピー copied: コピーしました external_content_warning: 外部の画像/メディアは表示されません。 ================================================ FILE: i18n/ko_KR.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: 성공. unknown: other: 알 수 없는 오류. request_format_error: other: 요청 형식이 잘못되었습니다. unauthorized_error: other: 권한이 없습니다. database_error: other: 데이터 서버 오류. forbidden_error: other: 접근 금지. duplicate_request_error: other: 중복된 제출입니다. action: report: other: 신고하기 edit: other: 편집 delete: other: 삭제 close: other: 닫기 reopen: other: 다시 열기 forbidden_error: other: 접근 금지. pin: other: 고정 hide: other: 리스트 제거 unpin: other: 고정 해제 show: other: 리스트 invite_someone_to_answer: other: 편집 undelete: other: 삭제 취소 merge: other: 병합 role: name: user: other: 유저 admin: other: 관리자 moderator: other: 중재자 description: user: other: 특별한 접근 권한이 없는 기본 사용자입니다. admin: other: 사이트에 대한 모든 권한을 가집니다. moderator: other: 관리자 설정을 제외한 모든 게시물에 접근할 수 있습니다. privilege: level_1: description: other: 레벨 1 (비공개 팀, 그룹에 적은 평판 필요) level_2: description: other: 레벨 2 (스타트업 커뮤니티에 낮은 평판 필요) level_3: description: other: 레벨 3 (성숙한 커뮤니티에 높은 평판 필요) level_custom: description: other: 커스텀 레벨 rank_question_add_label: other: 질문하기 rank_answer_add_label: other: 답변하기 rank_comment_add_label: other: 댓글 작성하기 rank_report_add_label: other: 신고하기 rank_comment_vote_up_label: other: 댓글 추천 rank_link_url_limit_label: other: 한번에 2개 이상의 링크를 게시하세요 rank_question_vote_up_label: other: 질문 추천 rank_answer_vote_up_label: other: 답변 추천 rank_question_vote_down_label: other: 질문 비추천 rank_answer_vote_down_label: other: 답변 비추천 rank_invite_someone_to_answer_label: other: 답변할 사람 초대 rank_tag_add_label: other: 새 태그 만들기 rank_tag_edit_label: other: 태그 설명 편집 (검토 필요) rank_question_edit_label: other: 다른 사람의 질문 편집 (검토 필요) rank_answer_edit_label: other: 다른 사람의 답변 편집 (검토 필요) rank_question_edit_without_review_label: other: 검토 없이 다른 사람의 질문 편집 rank_answer_edit_without_review_label: other: 검토 없이 다른 사람의 답변 편집 rank_question_audit_label: other: 질문 편집 검토하기 rank_answer_audit_label: other: 답변 편집 검토하기 rank_tag_audit_label: other: 태그 편집 검토하기 rank_tag_edit_without_review_label: other: 검토 없이 태그 설명 편집 rank_tag_synonym_label: other: 태그 동의어 관리 email: other: 이메일 e_mail: other: 이메일 password: other: 비밀번호 pass: other: 비밀번호 old_pass: other: 현재 비밀번호 original_text: other: 이 게시물 email_or_password_wrong_error: other: 이메일과 비밀번호가 일치하지 않습니다. error: common: invalid_url: other: 잘못된 URL 입니다. status_invalid: other: 유효하지 않은 상태. password: space_invalid: other: 암호에는 공백을 포함할 수 없습니다. admin: cannot_update_their_password: other: 암호를 변경할 수 없습니다. cannot_edit_their_profile: other: 수정할 수 없습니다. cannot_modify_self_status: other: 상태를 변경할 수 없습니다. email_or_password_wrong: other: 이메일과 비밀번호가 일치하지 않습니다. answer: not_found: other: 답변을 찾을 수 없습니다. cannot_deleted: other: 삭제 권한이 없습니다. cannot_update: other: 편집 권한이 없습니다. question_closed_cannot_add: other: 질문이 닫혔으며, 추가할 수 없습니다. content_cannot_empty: other: 답변 내용은 비워둘 수 없습니다. comment: edit_without_permission: other: 편집이 가능하지 않은 댓글입니다. not_found: other: 댓글을 찾을 수 없습니다. cannot_edit_after_deadline: other: 수정 허용 시간을 초과했습니다. content_cannot_empty: other: 댓글 내용은 비워둘 수 없습니다. email: duplicate: other: 이미 존재하는 이메일입니다. need_to_be_verified: other: 이메일을 확인해주세요. verify_url_expired: other: URL이 만료되었습니다. 이메일을 다시 보내주세요. illegal_email_domain_error: other: 해당 도메인을 사용할 수 없습니다. 다른 전자 메일을 사용하십시오. lang: not_found: other: 언어 파일을 찾을 수 없습니다. object: captcha_verification_failed: other: 잘못된 Captcha입니다. disallow_follow: other: 팔로우할 수 없습니다. disallow_vote: other: 추천할 수 없습니다. disallow_vote_your_self: other: 자신의 게시물에는 추천할 수 없습니다. not_found: other: 오브젝트를 찾을 수 없습니다. verification_failed: other: 확인 실패. email_or_password_incorrect: other: 이메일과 비밀번호가 일치하지 않습니다. old_password_verification_failed: other: 이전 암호 확인에 실패했습니다 new_password_same_as_previous_setting: other: 새 비밀번호는 이전 비밀번호와 다르게 입력해주세요. already_deleted: other: 이 게시물은 삭제되었습니다. meta: object_not_found: other: 메타 객체를 찾을 수 없습니다 question: already_deleted: other: 삭제된 게시물입니다. under_review: other: 검토를 기다리고 있습니다. 승인된 후에 볼 수 있습니다. not_found: other: 질문을 찾을 수 없습니다. cannot_deleted: other: 삭제 권한이 없습니다. cannot_close: other: 닫을 권한이 없습니다. cannot_update: other: 업데이트 권한이 없습니다. content_cannot_empty: other: 내용은 비워둘 수 없습니다. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: 등급이 조건을 충족하지 못합니다. vote_fail_to_meet_the_condition: other: 피드백 감사합니다. 투표를 하려면 최소 {{.Rank}} 평판이 필요합니다. no_enough_rank_to_operate: other: 이 작업을 하려면 최소 {{.Rank}} 평판이 필요합니다. report: handle_failed: other: 신고 처리에 실패했습니다. not_found: other: 신고를 찾을 수 없습니다. tag: already_exist: other: 이미 존재하는 태그입니다. not_found: other: 태그를 찾을 수 없습니다. recommend_tag_not_found: other: 추천 태그가 존재하지 않습니다. recommend_tag_enter: other: 하나 이상의 태그를 입력해주세요. not_contain_synonym_tags: other: 동일한 태그를 포함하지 않아야 합니다. cannot_update: other: 편집 권한이 없습니다. is_used_cannot_delete: other: 사용 중인 태그는 삭제할 수 없습니다. cannot_set_synonym_as_itself: other: 현재 태그의 동의어로 자기 자신을 설정할 수 없습니다. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: 발신자 이름은 이메일 주소가 될 수 없습니다. theme: not_found: other: 테마를 찾을 수 없습니다. revision: review_underway: other: 검토 대기열에 있기 때문에 현재 편집할 수 없습니다. no_permission: other: 수정권한이 없습니다. user: external_login_missing_user_id: other: 서드파티 플랫폼에서 고유 사용자 ID를 제공하지 않아 로그인할 수 없습니다. 웹사이트 관리자에게 문의하세요. external_login_unbinding_forbidden: other: 이 로그인을 제거하기 전에 계정에 로그인 비밀번호를 설정하세요. email_or_password_wrong: other: other: 이메일 또는 비밀번호가 일치하지 않습니다. not_found: other: 사용자를 찾을 수 없습니다. suspended: other: 사용자가 정지되었습니다. username_invalid: other: 유효하지 않은 이름입니다. username_duplicate: other: 이미 사용중인 이름입니다. set_avatar: other: 아바타 설정에 실패했습니다. cannot_update_your_role: other: 수정할 수 없습니다. not_allowed_registration: other: 현재 사이트는 등록할 수 없습니다. not_allowed_login_via_password: other: 현재 사이트는 비밀번호를 통해 로그인할 수 없습니다. access_denied: other: 접근이 거부당했습니다. page_access_denied: other: 이 페이지에 대한 접근 권한이 없습니다. add_bulk_users_format_error: other: "줄 {{.Line}}의 '{{.Content}}' 근처 {{.Field}} 형식 오류입니다. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "한 번에 추가할 수 있는 사용자 수는 1-{{.MaxAmount}} 범위 내에 있어야 합니다." status_suspended_forever: other: "이 사용자는 무기한 접근이 금지 되었습니다. 이 사용자는 커뮤니티의 지침을 충족시키지 않았습니다." status_suspended_until: other: "이 사용자는 {{.SuspendedUntil}} 까지 접근이 금지 되었습니다. 이 사용자는 커뮤니티의 지침을 충족시키지 않았습니다." status_deleted: other: "삭제된 사용자입니다." status_inactive: other: "비활성 사용자입니다." config: read_config_failed: other: 컨피그파일 읽기를 실패했습니다 database: connection_failed: other: 데이터베이스 연결을 실패했습니다 create_table_failed: other: 테이블 생성을 실패했습니다 install: create_config_failed: other: config.yaml 파일을 생성할 수 없습니다. upload: unsupported_file_format: other: 지원하지 않는 파일 형식입니다. site_info: config_not_found: other: 사이트 설정을 찾을 수 없습니다. badge: object_not_found: other: 배지 객체를 찾을 수 없습니다 reason: spam: name: other: 스팸 desc: other: 이 게시물은 광고로 인식되었습니다. 현재 주제와 관련이 없습니다. rude_or_abusive: name: other: 폭언 또는 무례한 언행입니다. desc: other: "대화에 부적절한 내용입니다." a_duplicate: name: other: 중복 desc: other: 이 질문은 이전에 질문한 적이 있으며 이미 답변이 있습니다. placeholder: other: 이미 존재하는 질문입니다 not_a_answer: name: other: 답변이 아닙니다 desc: other: "이 내용은 답변으로 게시 되었지만, 질문에 대한 답변 시도가 아닙니다. 이는 수정, 댓글, 다른 질문으로 올리는 것이 적절하거나, 삭제하는 것이 적절할 수도 있습니다." no_longer_needed: name: other: 더 이상 필요하지 않습니다. desc: other: 이 의견은 구식이거나 대화 중이거나 이 게시물과 관련이 없습니다. something: name: other: 다른 항목 desc: other: 이 게시물은 위에 나열되지 않은 다른 이유로 직원의 주의가 필요합니다. placeholder: other: 귀하가 우려하는 사항을 구체적으로 알려주세요. community_specific: name: other: 커뮤니티별 특정 이유 desc: other: 이 질문은 커뮤니티 가이드라인에 부합하지 않습니다. not_clarity: name: other: 세부사항 또는 명확성이 필요합니다. desc: other: 이 질문은 현재 하나에 여러 개의 질문이 포함되어 있습니다. 하나의 문제에만 초점을 맞추어야 합니다. looks_ok: name: other: 괜찮아 보입니다. desc: other: 이 게시물은 괜찮으며 품질이 낮지 않습니다. needs_edit: name: other: 편집이 필요하고, 제가 했습니다. desc: other: 이 게시물에 대한 문제를 직접 개선하고 수정합니다. needs_close: name: other: 닫혀야 함 desc: other: 닫힌 질문은 대답할 수 없지만 편집, 투표 및 댓글 작성을 할 수 있습니다. needs_delete: name: other: 삭제해야 함 desc: other: 이 게시물은 삭제되었습니다. question: close: duplicate: name: other: 스팸 desc: other: 이 질문은 이전에 질문한 적이 있으며 이미 답변이 있습니다. guideline: name: other: 커뮤니티 특정 이유 desc: other: 이 질문은 커뮤니티 가이드라인에 부합하지 않습니다. multiple: name: other: 세부사항 또는 명확성이 필요합니다. desc: other: 이 질문은 현재 하나에 여러 개의 질문이 포함되어 있습니다. 하나의 문제에만 초점을 맞추어야 합니다. other: name: other: 다른 이유 desc: other: 이 게시물에는 위에 나열되지 않은 다른 이유가 필요합니다. operation_type: asked: other: 질문됨 answered: other: 답변됨 modified: other: 수정됨 deleted_title: other: 이미 삭제된 게시물입니다. questions_title: other: 질문들 tag: tags_title: other: 태그 no_description: other: 이 태그에는 설명이 없습니다. notification: action: update_question: other: 수정된 질문 answer_the_question: other: 대답한 질문 update_answer: other: 수정된 대답 accept_answer: other: 채택된 답변 comment_question: other: 질문에 댓글 달림 comment_answer: other: 답변에 댓글 달림 reply_to_you: other: 당신에게 답변함 mention_you: other: 당신을 언급함 your_question_is_closed: other: 당신의 질문이 닫혔습니다 your_question_was_deleted: other: 당신의 질문이 삭제되었습니다 your_answer_was_deleted: other: 당신의 답변이 삭제되었습니다 your_comment_was_deleted: other: 당신의 댓글이 삭제되었습니다 up_voted_question: other: 질문에 추천함 down_voted_question: other: 질문에 비추천함 up_voted_answer: other: 답변에 추천함 down_voted_answer: other: 답변에 비추천함 up_voted_comment: other: 댓글에 추천함 invited_you_to_answer: other: 답변에 초대함 earned_badge: other: '"{{.BadgeName}}" 배지를 획득했습니다' email_tpl: change_email: title: other: "[{{.SiteName}}] 새 이메일 주소 확인" body: other: "다음 링크를 클릭하여 {{.SiteName}} 의 새 이메일 주소를 확인하세요:
\n{{.ChangeEmailUrl}}

\n\n이 변경을 요청하지 않았다면 이 이메일을 무시하세요.

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} 님이 답변을 작성했습니다" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n{{.SiteName}} 에서 보기

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.

\n\n구독 취소" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} 님이 답변을 요청했습니다" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
답변을 아실 것 같습니다.

\n{{.SiteName}} 에서 보기

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.

\n\n구독 취소" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} 님이 당신의 게시물에 댓글을 남겼습니다" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n{{.SiteName}} 에서 보기

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.

\n\n구독 취소" new_question: title: other: "[{{.SiteName}}] 새 질문: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.

\n\n구독 취소" pass_reset: title: other: "[{{.SiteName }}] 비밀번호 재설정" body: other: "누군가가 {{.SiteName}} 에서 당신의 비밀번호 재설정을 요청했습니다.

\n\n당신이 요청하지 않았다면 이 이메일을 무시해도 됩니다.

\n\n새 비밀번호를 선택하려면 다음 링크를 클릭하세요:
\n{{.PassResetUrl}}\n

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요." register: title: other: "[{{.SiteName}}] 새 계정 확인" body: other: "{{.SiteName}} 에 오신 것을 환영합니다!

\n\n새 계정을 확인하고 활성화하려면 다음 링크를 클릭하세요:
\n{{.RegisterUrl}}

\n\n위 링크가 동작하지 않으면 복사하여 브라우저의 주소 입력에 직접 붙여넣으세요.\n

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요." test: title: other: "[{{.SiteName}}] 테스트 이메일" body: other: "이것은 테스트 이메일입니다.\n

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요." action_activity_type: upvote: other: 추천 upvoted: other: 추천함 downvote: other: 비추천 downvoted: other: 비추천함 accept: other: 채택 accepted: other: 채택됨 edit: other: 수정 review: queued_post: other: 대기 중인 게시물 flagged_post: other: 신고된 게시물 suggested_post_edit: other: 건의된 수정 reaction: tooltip: other: "{{ .Names }} 외 {{ .Count }} 명..." badge: default_badges: autobiographer: name: other: 자서전 작가 desc: other: 프로필 정보를 작성했습니다. certified: name: other: 인증됨 desc: other: 신규 사용자 튜토리얼을 완료했습니다. editor: name: other: 에디터 desc: other: 첫 게시물 편집. first_flag: name: other: 첫 번째 플래그 desc: other: 첫 번째로 게시물에 플래그를 설정했습니다. first_upvote: name: other: 첫 번째 추천 desc: other: 첫 번째로 게시물을 추천했습니다. first_link: name: other: 첫 번째 링크 desc: other: 첫 번째로 다른 게시물에 링크를 추가했습니다. first_reaction: name: other: 첫 번째 반응 desc: other: 첫 번째로 게시물에 반응했습니다. first_share: name: other: 첫 번째 공유 desc: other: 첫 번째로 게시물을 공유했습니다. scholar: name: other: 학자 desc: other: 질문을 하고 답변을 채택했습니다. commentator: name: other: 해설자 desc: other: 댓글 5 개 남기기. new_user_of_the_month: name: other: 이달의 신규 사용자 desc: other: 첫 달의 뛰어난 기여. read_guidelines: name: other: 가이드라인 읽음 desc: other: '[커뮤니티 가이드라인] 을 읽어보세요.' reader: name: other: 리더 desc: other: 10 개 이상의 답변이 있는 주제의 모든 답변을 읽어보세요. welcome: name: other: 환영합니다 desc: other: 추천을 받았습니다. nice_share: name: other: 좋은 공유 desc: other: 25 명의 고유 방문자와 게시물을 공유했습니다. good_share: name: other: 더 좋은 공유 desc: other: 300 명의 고유 방문자와 게시물을 공유했습니다. great_share: name: other: 훌륭한 공유 desc: other: 1000 명의 고유 방문자와 게시물을 공유했습니다. out_of_love: name: other: 사랑이 식어서 desc: other: 하루에 50 개의 추천을 사용했습니다. higher_love: name: other: 더 큰 사랑 desc: other: 하루에 50 개의 추천을 5 번 사용했습니다. crazy_in_love: name: other: 사랑에 미치다 desc: other: 하루에 50 개의 추천을 20 번 사용했습니다. promoter: name: other: 홍보자 desc: other: 사용자를 초대했습니다. campaigner: name: other: 캠페인 담당자 desc: other: 3 명의 기본 사용자를 초대했습니다. champion: name: other: 챔피언 desc: other: 5 명의 멤버를 초대했습니다. thank_you: name: other: 감사합니다 desc: other: 20 개의 추천받은 게시물을 보유하고 10 개의 추천을 주었습니다. gives_back: name: other: 보답 desc: other: 100 개의 추천받은 게시물을 보유하고 100 개의 추천을 주었습니다. empathetic: name: other: 공감적 desc: other: 500 개의 추천받은 게시물을 보유하고 1000 개의 추천을 주었습니다. enthusiast: name: other: 열성팬 desc: other: 10 일 연속으로 방문했습니다. aficionado: name: other: 애호가 desc: other: 100 일 연속으로 방문했습니다. devotee: name: other: 열성 지지자 desc: other: 365 일 연속으로 방문했습니다. anniversary: name: other: 기념일 desc: other: 1 년 동안 활발한 멤버로 활동하며 최소 한 번 이상 게시했습니다. appreciated: name: other: 감사합니다 desc: other: 20 개의 게시물에서 1 번의 추천을 받았습니다. respected: name: other: 존경받는 desc: other: 100 개의 게시물에서 2 번의 추천을 받았습니다. admired: name: other: 존경받는 desc: other: 300 개의 게시물에서 5 번의 추천을 받았습니다. solved: name: other: 해결됨 desc: other: 답변이 채택되었습니다. guidance_counsellor: name: other: 지도 상담사 desc: other: 10 개의 답변이 채택되었습니다. know_it_all: name: other: 만물박사 desc: other: 50 개의 답변이 채택되었습니다. solution_institution: name: other: 솔루션 기관 desc: other: 150 개의 답변이 채택되었습니다. nice_answer: name: other: 좋은 답변 desc: other: 답변 점수가 10 점 이상입니다. good_answer: name: other: 더 좋은 답변 desc: other: 답변 점수가 25 점 이상입니다. great_answer: name: other: 훌륭한 답변 desc: other: 답변 점수가 50 점 이상입니다. nice_question: name: other: 좋은 질문 desc: other: 질문 점수가 10 점 이상입니다. good_question: name: other: 좋은 질문 desc: other: 질문 점수가 25 점 이상입니다. great_question: name: other: 훌륭한 질문 desc: other: 질문 점수가 50 점 이상입니다. popular_question: name: other: 인기 질문 desc: other: 500 회 조회된 질문입니다. notable_question: name: other: 중요 질문 desc: other: 1,000 회 조회된 질문입니다. famous_question: name: other: 유명 질문 desc: other: 5,000 회 조회된 질문입니다. popular_link: name: other: 인기 링크 desc: other: 50 번 클릭된 외부 링크를 게시했습니다. hot_link: name: other: 핫 링크 desc: other: 300 번 클릭된 외부 링크를 게시했습니다. famous_link: name: other: 유명한 링크 desc: other: 100 번 클릭된 외부 링크를 게시했습니다. default_badge_groups: getting_started: name: other: 시작하기 community: name: other: 커뮤니티 posting: name: other: 포스팅 # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: 포맷 방법 desc: >-
  • 게시물 언급: #post_id

  • 링크 만들기

    <https://url.com>

    [제목](https://url.com)
  • 단락 사이에 줄바꿈 넣기

  • _기울임체_ 또는 **굵게**

  • 코드는 4 칸 들여쓰기

  • 줄 시작에 > 를 넣어 인용

  • 백틱으로 이스케이프 `_이렇게_`

  • 백틱 ` 으로 코드 펜스 생성

    ```
    여기에 코드
    ```
pagination: prev: 이전 next: 다음 page_title: question: 질문 questions: 질문들 tag: 태그 tags: 태그들 tag_wiki: 태그 위키 create_tag: 태그 생성 edit_tag: 태그 수정 ask_a_question: 질문 생성 edit_question: 질문 수정 edit_answer: 답변 수정 search: 검색 posts_containing: 포함된 게시물 settings: 설정 notifications: 알림 login: 로그인 sign_up: 회원 가입 account_recovery: 계정 복구 account_activation: 계정 활성화 confirm_email: 이메일 확인 account_suspended: 계정 정지 admin: 관리자 change_email: 이메일 수정 install: 답변 설치 upgrade: 답변 업그레이드 maintenance: 웹사이트 유지보수 users: 사용자 oauth_callback: 처리 중 http_404: HTTP 오류 404 http_50X: HTTP 오류 500 http_403: HTTP 오류 403 logout: 로그아웃 posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: 알림 inbox: 받은 편지함 achievement: 업적 new_alerts: 새로운 알림 all_read: 모두 읽음 처리 show_more: 더 보기 someone: 누군가 inbox_type: all: 전체 posts: 게시물 invites: 초대 votes: 투표 answer: 답변 question: 질문 badge_award: 뱃지 suspended: title: 계정이 정지되었습니다 until_time: "당신의 계정은 {{ time }}까지 정지되었습니다." forever: 이 사용자는 영구 정지되었습니다. end: 커뮤니티 가이드라인을 준수하지 않았습니다. contact_us: 문의하기 editor: blockquote: text: 인용구 bold: text: 강조 chart: text: 차트 flow_chart: 플로우 차트 sequence_diagram: 시퀀스 다이어그램 class_diagram: 클래스 다이어그램 state_diagram: 상태 다이어그램 entity_relationship_diagram: 엔터티 관계 다이어그램 user_defined_diagram: 사용자 정의 다이어그램 gantt_chart: 간트 차트 pie_chart: 파이 차트 code: text: 코드 예시 add_code: 코드 예시 추가 form: fields: code: label: 코드 msg: empty: 코드를 입력하세요. language: label: 언어 placeholder: 자동 감지 btn_cancel: 취소 btn_confirm: 추가 formula: text: 수식 options: inline: 인라인 수식 block: 블록 수식 heading: text: 제목 options: h1: 제목 1 h2: 제목 2 h3: 제목 3 h4: 제목 4 h5: 제목 5 h6: 제목 6 help: text: 도움말 hr: text: 가로규칙 image: text: 이미지 add_image: 이미지 추가 tab_image: 이미지 업로드 form_image: fields: file: label: 이미지 파일 btn: 이미지 선택 msg: empty: 파일을 선택하세요. only_image: 이미지 파일만 허용됩니다. max_size: 파일 크기는 {{size}} MB 를 초과할 수 없습니다. desc: label: 설명 tab_url: 이미지 URL form_url: fields: url: label: 이미지 URL msg: empty: 이미지 URL을 입력하세요. name: label: 설명 btn_cancel: 취소 btn_confirm: 추가 uploading: 업로드 중 indent: text: 들여쓰기 outdent: text: 내어쓰기 italic: text: 이탤릭체 link: text: 링크 add_link: 링크 추가 form: fields: url: label: URL msg: empty: URL을 입력하세요. name: label: 설명 btn_cancel: 취소 btn_confirm: 추가 ordered_list: text: 번호 매긴 목록 unordered_list: text: 글머리 기호 목록 table: text: 표 heading: 제목 cell: 셀 file: text: 파일 첨부 not_supported: "해당 파일 형식을 지원하지 않습니다. {{file_type}} 로 다시 시도해 주세요." max_size: "첨부 파일 크기는 {{size}} MB 를 초과할 수 없습니다." close_modal: title: 이 게시물을 다음과 같은 이유로 닫습니다... btn_cancel: 취소 btn_submit: 제출 remark: empty: 비어 있을 수 없습니다. msg: empty: 이유를 선택해 주세요. report_modal: flag_title: 이 게시물을 신고합니다... close_title: 이 게시물을 다음과 같은 이유로 닫습니다... review_question_title: 질문 검토 review_answer_title: 답변 검토 review_comment_title: 댓글 검토 btn_cancel: 취소 btn_submit: 제출 remark: empty: 비어 있을 수 없습니다. msg: empty: 이유를 선택해 주세요. not_a_url: URL 형식이 올바르지 않습니다. url_not_match: URL 원본이 현재 웹사이트와 일치하지 않습니다. tag_modal: title: 새로운 태그 생성 form: fields: display_name: label: 표시 이름 msg: empty: 표시 이름을 입력하세요. range: 표시 이름은 최대 35자까지 입력 가능합니다. slug_name: label: URL 슬러그 desc: '"a-z", "0-9", "+ # - ." 문자 집합을 사용해야 합니다.' msg: empty: URL 슬러그를 입력하세요. range: URL 슬러그는 최대 35자까지 입력 가능합니다. character: 허용되지 않은 문자 집합이 포함되어 있습니다.' desc: label: 설명 revision: label: 개정 edit_summary: label: 편집 요약 placeholder: >- 수정 사항을 간략히 설명하세요 (철자 수정, 문법 수정, 서식 개선 등) btn_cancel: 취소 btn_submit: 제출 btn_post: 새 태그 게시 tag_info: created_at: 생성됨 edited_at: 편집됨 history: 히스토리 synonyms: title: 동의어 text: 다음 태그가 다음으로 다시 매핑됩니다 empty: 동의어가 없습니다. btn_add: 동의어 추가 btn_edit: 편집 btn_save: 저장 synonyms_text: 다음 태그가 다음으로 다시 매핑됩니다 delete: title: 이 태그 삭제 tip_with_posts: >-

게시물이 있는 태그 삭제는 허용되지 않습니다.

먼저 게시물에서 이 태그를 제거해 주세요.

tip_with_synonyms: >-

동의어가 있는 태그 삭제는 허용되지 않습니다.

먼저 이 태그에서 동의어를 제거해 주세요.

tip: 정말로 삭제하시겠습니까? close: 닫기 merge: title: 태그 병합 source_tag_title: 원본 태그 source_tag_description: 원본 태그와 관련 데이터가 대상 태그로 재매핑됩니다. target_tag_title: 대상 태그 target_tag_description: 병합 후 이 두 태그 간 동의어가 생성됩니다. no_results: 일치하는 태그가 없습니다 btn_submit: 제출 btn_close: 닫기 edit_tag: title: 태그 수정 default_reason: 태그 수정 default_first_reason: 태그 추가 btn_save_edits: 수정 저장 btn_cancel: 취소 dates: long_date: MMM D long_date_with_year: "YYYY년 M월 D일" long_date_with_time: "YYYY년 MMM D일 HH:mm" now: 방금 전 x_seconds_ago: "{{count}}초 전" x_minutes_ago: "{{count}}분 전" x_hours_ago: "{{count}}시간 전" hour: 시간 day: 일 hours: 시간 days: 일 month: 월 months: 개월 year: 년 reaction: heart: 하트 smile: 스마일 frown: 찡그린 표정 btn_label: 반응 추가 또는 제거 undo_emoji: '{{ emoji }} 반응 취소' react_emoji: '{{ emoji }} 로 반응' unreact_emoji: '{{ emoji }} 반응 취소' comment: btn_add_comment: 댓글 추가 reply_to: 답글 달기 btn_reply: 답글 btn_edit: 수정 btn_delete: 삭제 btn_flag: 신고 btn_save_edits: 수정 저장 btn_cancel: 취소 show_more: "{{count}}개의 댓글 더 보기" tip_question: >- 더 많은 정보를 요청하거나 개선을 제안하기 위해 댓글을 사용하세요. 댓글에서 질문에 답변하지는 마세요. tip_answer: >- 다른 사용자에게 답변하거나 변경 사항을 알릴 때 댓글을 사용하세요. 새로운 정보를 추가하는 경우에는 게시물을 수정하세요. tip_vote: 게시물에 유용한 정보를 추가합니다. edit_answer: title: 답변 수정 default_reason: 답변 수정 default_first_reason: 답변 추가 form: fields: revision: label: 개정 answer: label: 답변 feedback: characters: 내용은 최소 6자 이상이어야 합니다. edit_summary: label: 편집 요약 placeholder: >- 수정 사항을 간략히 설명하세요 (철자 수정, 문법 수정, 서식 개선 등) btn_save_edits: 수정 저장 btn_cancel: 취소 tags: title: 태그들 sort_buttons: popular: 인기순 name: 이름 newest: 최신순 button_follow: 팔로우 button_following: 팔로잉 중 tag_label: 질문들 search_placeholder: 태그 이름으로 필터링 no_desc: 이 태그에는 설명이 없습니다. more: 더 보기 wiki: 위키 ask: title: 질문 생성 edit_title: 질문 수정 default_reason: 질문 수정 default_first_reason: 질문 생성 similar_questions: 유사한 질문 form: fields: revision: label: 개정 title: label: 제목 placeholder: 주제는 무엇인가요? 상세하게 작성해주세요. msg: empty: 제목을 입력하세요. range: 제목은 최대 150자까지 입력 가능합니다. body: label: 본문 msg: empty: 본문을 입력하세요. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: 태그 msg: empty: 태그를 입력하세요. answer: label: 답변 msg: empty: 답변을 입력하세요. edit_summary: label: 편집 요약 placeholder: >- 수정 사항을 간략히 설명하세요 (철자 수정, 문법 수정, 서식 개선 등) btn_post_question: 질문 게시하기 btn_save_edits: 수정사항 저장 answer_question: 질문에 대한 답변 작성 post_question&answer: 질문과 답변 게시하기 tag_selector: add_btn: 태그 추가 create_btn: 새 태그 생성 search_tag: 태그 검색 hint: 질문의 주제를 설명하세요. 적어도 하나의 태그가 필요합니다. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: 일치하는 태그가 없습니다. tag_required_text: 필수 태그 (적어도 하나) header: nav: question: 질문 tag: 태그 user: 사용자 badges: 뱃지 profile: 프로필 setting: 설정 logout: 로그아웃 admin: 관리자 review: 리뷰 bookmark: 즐겨찾기 moderation: 운영 search: placeholder: 검색 footer: build_on: Powered by <1> Apache Answer upload_img: name: 변경 loading: 로딩 중... pic_auth_code: title: 캡차 placeholder: 위의 텍스트를 입력하세요 msg: empty: 캡차를 입력하세요. inactive: first: >- 거의 다 되었습니다! {{mail}}로 활성화 메일을 보냈습니다. 계정을 활성화하려면 메일 안의 지침을 따르세요. info: "메일이 도착하지 않았다면, 스팸 메일함도 확인해 주세요." another: >- {{mail}}로 또 다른 활성화 이메일을 보냈습니다. 메일이 도착하는 데 몇 분 정도 걸릴 수 있으니 스팸 메일함도 확인해 주세요. btn_name: 활성화 이메일 재전송 change_btn_name: 이메일 변경 msg: empty: 비어 있을 수 없습니다. resend_email: url_label: 활성화 이메일을 재전송하시겠습니까? url_text: 위의 활성화 링크를 사용자에게 제공할 수도 있습니다. login: login_to_continue: 계속하려면 로그인하세요 info_sign: 계정이 없으신가요? <1>가입하기 info_login: 이미 계정이 있으신가요? <1>로그인하기 agreements: 가입하면 <1>개인정보 보호 정책과 <3>이용 약관에 동의하게 됩니다. forgot_pass: 비밀번호를 잊으셨나요? name: label: 이름 msg: empty: 이름을 입력하세요. range: 이름은 2 자에서 30 자 사이여야 합니다. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: 이메일 msg: empty: 이메일을 입력하세요. password: label: 비밀번호 msg: empty: 비밀번호를 입력하세요. different: 입력된 비밀번호가 일치하지 않습니다. account_forgot: page_title: 비밀번호를 잊으셨나요? btn_name: 비밀번호 재설정 이메일 보내기 send_success: >- {{mail}}에 해당하는 계정이 있다면 곧 비밀번호 재설정 방법을 안내하는 이메일을 받으실 수 있습니다. email: label: 이메일 msg: empty: 이메일을 입력하세요. change_email: btn_cancel: 취소 btn_update: 이메일 주소 업데이트 send_success: >- {{mail}}에 해당하는 계정이 있다면 곧 이메일 주소 변경 방법을 안내하는 이메일을 받으실 수 있습니다. email: label: 새 이메일 msg: empty: 이메일을 입력하세요. oauth: connect: '{{ auth_name }}로 연결' remove: '{{ auth_name }} 연결 해제' oauth_bind_email: subtitle: 계정에 복구 이메일 추가 btn_update: 이메일 주소 업데이트 email: label: 이메일 msg: empty: 이메일을 입력하세요. modal_title: 이미 등록된 이메일 modal_content: 이 이메일 주소는 이미 등록되어 있습니다. 기존 계정에 연결하시겠습니까? modal_cancel: 이메일 변경 modal_confirm: 기존 계정에 연결하기 password_reset: page_title: 비밀번호 재설정 btn_name: 비밀번호 재설정 reset_success: >- 비밀번호가 성공적으로 변경되었습니다. 로그인 페이지로 이동합니다. link_invalid: >- 죄송합니다. 이 비밀번호 재설정 링크는 더 이상 유효하지 않습니다. 이미 비밀번호를 재설정하셨을 수 있습니다. to_login: 로그인 페이지로 이동 password: label: 비밀번호 msg: empty: 비밀번호를 입력하세요. length: 비밀번호는 8자에서 32자 사이여야 합니다. different: 입력한 비밀번호가 일치하지 않습니다. password_confirm: label: 새 비밀번호 확인 settings: page_title: 설정 goto_modify: 수정으로 이동 nav: profile: 프로필 notification: 알림 account: 계정 interface: 인터페이스 profile: heading: 프로필 btn_name: 저장 display_name: label: 표시 이름 msg: 표시 이름을 입력하세요. msg_range: 표시 이름은 2-30 자 길이여야 합니다. username: label: 사용자 이름 caption: 다른 사용자가 "@사용자이름"으로 멘션할 수 있습니다. msg: 사용자 이름을 입력하세요. msg_range: 유저 이름은 2-30 자 길이여야 합니다. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: 프로필 이미지 gravatar: Gravatar gravatar_text: Gravatar에서 이미지를 변경할 수 있습니다. custom: 사용자 정의 custom_text: 사용자 이미지를 업로드할 수 있습니다. default: 시스템 기본 이미지 msg: 프로필 이미지를 업로드하세요. bio: label: 자기 소개 website: label: 웹사이트 placeholder: "https://example.com" msg: 웹사이트 형식이 올바르지 않습니다. location: label: 위치 placeholder: "도시, 국가" notification: heading: 이메일 알림 turn_on: 켜기 inbox: label: 받은 편지함 알림 description: 질문에 대한 답변, 댓글, 초대 등을 받습니다. all_new_question: label: 모든 새 질문 description: 모든 새 질문에 대해 알림을 받습니다. 주당 최대 50개의 질문까지. all_new_question_for_following_tags: label: 팔로우 태그의 모든 새 질문 description: 팔로우하는 태그의 새로운 질문에 대해 알림을 받습니다. account: heading: 계정 change_email_btn: 이메일 변경 change_pass_btn: 비밀번호 변경 change_email_info: >- 해당 주소로 이메일을 보냈습니다. 확인 지침을 따라주세요. email: label: 이메일 new_email: label: 새 이메일 msg: 새 이메일을 입력하세요. pass: label: 현재 비밀번호 msg: 비밀번호를 입력하세요. password_title: 비밀번호 current_pass: label: 현재 비밀번호 msg: empty: 현재 비밀번호를 입력하세요. length: 비밀번호는 8자에서 32자 사이여야 합니다. different: 입력한 두 비밀번호가 일치하지 않습니다. new_pass: label: 새 비밀번호 pass_confirm: label: 새 비밀번호 확인 interface: heading: 인터페이스 lang: label: 인터페이스 언어 text: 사용자 인터페이스 언어입니다. 페이지를 새로고침하면 변경됩니다. my_logins: title: 내 로그인 정보 label: 이 사이트에서 이 계정으로 로그인하거나 가입하세요. modal_title: 로그인 제거 modal_content: 이 계정에서 이 로그인을 제거하시겠습니까? modal_confirm_btn: 제거 remove_success: 제거되었습니다. toast: update: 업데이트 성공 update_password: 비밀번호가 성공적으로 변경되었습니다. flag_success: 신고 감사합니다. forbidden_operate_self: 자신에 대한 작업은 금지되어 있습니다. review: 검토 후에 귀하의 수정 사항이 표시됩니다. sent_success: 전송 성공 related_question: title: 관련된 질문 answers: 답변 linked_question: title: 링크된 질문 description: 이 질문을 링크한 질문 no_linked_question: 이 질문에 연결된 질문 없음. invite_to_answer: title: 질문자 초대 desc: 답변을 알고 있을 것으로 생각되는 사람을 선택하세요. invite: 답변 초대 add: 사람 추가 search: 사람 검색 question_detail: action: 동작 created: Created Asked: 질문함 asked: 질문 작성 update: 수정됨 Edited: Edited edit: 편집됨 commented: 댓글 작성 Views: 조회수 Follow: 팔로우 Following: 팔로잉 중 follow_tip: 이 질문을 팔로우하여 알림을 받으세요. answered: 답변 작성 closed_in: 답변 종료 show_exist: 기존 질문 표시 useful: 유용함 question_useful: 유용하고 명확함 question_un_useful: 불명확하거나 유용하지 않음 question_bookmark: 이 질문 즐겨찾기 answer_useful: 유용함 answer_un_useful: 유용하지 않음 answers: title: 답변 score: 점수 newest: 최신순 oldest: 오래된 순 btn_accept: 채택 btn_accepted: 채택됨 write_answer: title: 당신의 답변 edit_answer: 내 답변 편집하기 btn_name: 답변 게시하기 add_another_answer: 다른 답변 추가 confirm_title: 답변 계속하기 continue: 계속 confirm_info: >-

다른 답변을 추가하시겠습니까?

대신 기존 답변을 향상시키고 개선할 수 있는 수정 링크를 사용할 수 있습니다.

empty: 답변을 입력해주세요. characters: 내용은 최소 6자 이상이어야 합니다. tips: header_1: 답변해 주셔서 감사합니다 li1_1: 질문에 답변을 제공하세요. 세부 사항을 설명하고 연구 결과를 공유하세요. li1_2: 발언을 뒷받침하는 자료나 개인적인 경험을 통해 주장을 뒷받침하세요. header_2: 하지만 피해야 할 것들 ... li2_1: 도움을 요청하거나 해명을 구하거나 다른 답변에 응답하는 것. reopen: confirm_btn: 다시 열기 title: 이 게시물 다시 열기 content: 정말 다시 열기를 원하시나요? list: confirm_btn: 목록 title: 이 게시물 목록에 추가하기 content: 정말 목록에 추가하시겠습니까? unlist: confirm_btn: 목록 해제 title: 이 게시물 목록에서 제외하기 content: 정말 목록에서 제외하시겠습니까? pin: title: 이 게시물 고정하기 content: 글로벌로 고정하시겠습니까? 이 게시물은 모든 게시물 목록 상단에 표시됩니다. confirm_btn: 고정하기 delete: title: 이 게시물 삭제하기 question: >-

답변이 있는 질문을 삭제하는 것은 권장하지 않습니다 이는 이 지식을 필요로 하는 사용자에게 정보를 제공하지 못하게 될 수 있습니다.

답변이 있는 질문을 반복적으로 삭제하는 경우 질문 권한이 차단될 수 있습니다. 정말 삭제하시겠습니까? answer_accepted: >-

채택된 답변을 삭제하는 것은 권장하지 않습니다 이는 이 지식을 필요로 하는 사용자에게 정보를 제공하지 못하게 될 수 있습니다.

채택된 답변을 반복적으로 삭제하는 경우 답변 권한이 차단될 수 있습니다. 정말 삭제하시겠습니까? other: 정말 삭제하시겠습니까? tip_answer_deleted: 이 답변은 삭제되었습니다. undelete_title: 이 게시물 복구하기 undelete_desc: 정말 복구하시겠습니까? btns: confirm: 확인 cancel: 취소 edit: 편집 save: 저장 delete: 삭제 undelete: 복구 list: 목록 unlist: 목록 해제 unlisted: 목록에서 해제됨 login: 로그인 signup: 가입하기 logout: 로그아웃 verify: 확인 create: 생성 approve: 승인 reject: 거부 skip: 건너뛰기 discard_draft: 임시 저장 삭제 pinned: 고정됨 all: 모두 question: 질문 answer: 답변 comment: 댓글 refresh: 새로 고침 resend: 재전송 deactivate: 비활성화 active: 활성화 suspend: 정지 unsuspend: 정지 해제 close: 닫기 reopen: 다시 열기 ok: 확인 light: 밝게 dark: 어둡게 system_setting: 시스템 설정 default: 기본 reset: 재설정 tag: 태그 post_lowercase: 게시물 filter: 필터 ignore: 무시 submit: 제출 normal: 일반 closed: 닫힘 deleted: 삭제됨 deleted_permanently: 영구 삭제 pending: 보류 중 more: 더 보기 view: 보기 card: 카드 compact: 간단히 display_below: 아래에 표시 always_display: 항상 표시 or: 또는 back_sites: 사이트로 돌아가기 search: title: 검색 결과 keywords: 키워드 options: 옵션 follow: 팔로우 following: 팔로잉 중 counts: "{{count}} 개의 결과" counts_loading: "... 개의 결과" more: 더 보기 sort_btns: relevance: 관련성 newest: 최신순 active: 활성순 score: 평점순 more: 더 보기 tips: title: 고급 검색 팁 tag: "<1>[태그] 태그로 검색" user: "<1>user:사용자명 작성자로 검색" answer: "<1>answers:0 답변이 없는 질문" score: "<1>score:3 평점이 3 이상인 글" question: "<1>is:question 질문만 검색" is_answer: "<1>is:answer 답변만 검색" empty: 아무것도 찾지 못했습니다.
다른 키워드를 사용하거나 덜 구체적인 검색을 시도하세요. share: name: 공유 copy: 링크 복사 via: 포스트 공유하기... copied: 복사됨 facebook: Facebook에 공유 twitter: X에 공유하기 cannot_vote_for_self: 자신의 글에 투표할 수 없습니다. modal_confirm: title: 오류... delete_permanently: title: 영구 삭제 content: 영구적으로 삭제하시겠습니까? account_result: success: 새 계정이 확인되었습니다. 홈페이지로 이동합니다. link: 홈페이지로 이동 oops: 이런! invalid: 사용하신 링크가 더 이상 작동하지 않습니다. confirm_new_email: 이메일이 업데이트되었습니다. confirm_new_email_invalid: >- 죄송합니다, 이 확인 링크는 더 이상 유효하지 않습니다. 이미 이메일이 변경된 상태일 수 있습니다. unsubscribe: page_title: 구독 해지 success_title: 구독 해지 완료 success_desc: 이 구독자 목록에서 성공적으로 제거되었으며, 더 이상 우리로부터 어떠한 이메일도 받지 않게 됩니다. link: 설정 변경하기 question: following_tags: 팔로우 태그 edit: 수정 save: 저장 follow_tag_tip: 질문 목록을 관리하기 위해 태그를 팔로우하세요. hot_questions: 인기 질문 all_questions: 모든 질문 x_questions: "{{ count }} 개의 질문" x_answers: "{{ count }} 개의 답변" x_posts: "{{ count }} 개의 글" questions: 질문 answers: 답변 newest: 최신순 active: 활성순 hot: 인기 frequent: 빈도 recommend: 추천 score: 평점순 unanswered: 답변이 없는 질문 modified: 수정됨 answered: 답변됨 asked: 질문됨 closed: 닫힘 follow_a_tag: 태그 팔로우하기 more: 더 보기 personal: overview: 개요 answers: 답변 answer: 답변 questions: 질문 question: 질문 bookmarks: 즐겨찾기 reputation: 평판 comments: 댓글 votes: 투표 badges: 뱃지 newest: 최신순 score: 평점순 edit_profile: 프로필 수정 visited_x_days: "{{ count }} 일 방문함" viewed: 조회됨 joined: 가입일 comma: "," last_login: 최근 접속 about_me: 자기 소개 about_me_empty: "// 안녕하세요, 세상아 !" top_answers: 최고 답변 top_questions: 최고 질문 stats: 통계 list_empty: 게시물을 찾을 수 없습니다.
다른 탭을 선택하실 수 있습니다. content_empty: 게시물을 찾을 수 없습니다. accepted: 채택됨 answered: 답변됨 asked: 질문됨 downvoted: 다운투표됨 mod_short: MOD mod_long: 관리자 x_reputation: 평판 x_votes: 받은 투표 x_answers: 답변 x_questions: 질문 recent_badges: 최근 배지 install: title: 설치 next: 다음 done: 완료 config_yaml_error: config.yaml 파일을 생성할 수 없습니다. lang: label: 언어 선택 db_type: label: 데이터베이스 엔진 db_username: label: 사용자 이름 placeholder: root msg: 사용자 이름은 비워둘 수 없습니다. db_password: label: 비밀번호 placeholder: root msg: 비밀번호는 비워둘 수 없습니다. db_host: label: 데이터베이스 호스트 placeholder: "db:3306" msg: 데이터베이스 호스트는 비워둘 수 없습니다. db_name: label: 데이터베이스 이름 placeholder: 답변 msg: 데이터베이스 이름은 비워둘 수 없습니다. db_file: label: 데이터베이스 파일 placeholder: /data/answer.db msg: 데이터베이스 파일은 비워둘 수 없습니다. ssl_enabled: label: SSL 활성화 ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL 모드 ssl_root_cert: placeholder: sslrootcert 파일 경로 msg: sslrootcert 파일 경로는 비워둘 수 없습니다 ssl_cert: placeholder: sslcert 파일 경로 msg: sslcert 파일 경로는 비워둘 수 없습니다 ssl_key: placeholder: sslkey 파일 경로 msg: sslkey 파일 경로는 비워둘 수 없습니다 config_yaml: title: config.yaml 파일 생성 label: config.yaml 파일이 생성되었습니다. desc: >- config.yaml 파일을 <1>/var/wwww/xxx/ 디렉터리에 수동으로 생성하고 아래 텍스트를 붙여넣을 수 있습니다. info: 위 작업을 완료한 후 "다음" 버튼을 클릭하세요. site_information: 사이트 정보 admin_account: 관리자 계정 site_name: label: 사이트 이름 msg: 사이트 이름을 입력하세요. msg_max_length: 사이트 이름은 최대 30자여야 합니다. site_url: label: 사이트 URL text: 사이트의 주소입니다. msg: empty: 사이트 URL을 입력하세요. incorrect: 올바른 형식의 사이트 URL을 입력하세요. max_length: 사이트 URL은 최대 512자여야 합니다. contact_email: label: 연락처 이메일 text: 이 사이트에 책임을 지는 주요 연락 이메일 주소입니다. msg: empty: 연락처 이메일을 입력하세요. incorrect: 올바른 형식의 연락처 이메일을 입력하세요. login_required: label: 비공개 switch: 로그인 필요 text: 로그인한 사용자만 이 커뮤니티에 접근할 수 있습니다. admin_name: label: 이름 msg: 이름을 입력하세요. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: 이름은 2 자 이상 30 자 이하여야 합니다. admin_password: label: 비밀번호 text: >- 로그인에 필요한 비밀번호입니다. 안전한 위치에 보관하세요. msg: 비밀번호를 입력하세요. msg_min_length: 비밀번호는 최소 8자여야 합니다. msg_max_length: 비밀번호는 최대 32자여야 합니다. admin_confirm_password: label: "비밀번호 확인" text: "확인을 위해 비밀번호를 다시 입력해주세요." msg: "비밀번호 확인이 일치하지 않습니다." admin_email: label: 이메일 text: 로그인에 필요한 이메일입니다. msg: empty: 이메일을 입력하세요. incorrect: 올바른 형식의 이메일을 입력하세요. ready_title: 귀하의 사이트가 준비되었습니다 ready_desc: >- 추가 설정을 원하시면 <1>관리자 섹션에서 찾아보세요; 사이트 메뉴에서 확인할 수 있습니다. good_luck: "재미있고 행운을 빕니다!" warn_title: 경고 warn_desc: >- 파일 <1>config.yaml이 이미 존재합니다. 이 파일의 구성 항목 중 재설정이 필요하면 먼저 삭제하세요. install_now: <1>지금 설치해보세요. installed: 이미 설치됨 installed_desc: >- 이미 설치된 것으로 보입니다. 재설치하려면 먼저 이전 데이터베이스 테이블을 삭제하세요. db_failed: 데이터베이스 연결 실패 db_failed_desc: >- <1>config.yaml 파일에 있는 데이터베이스 정보가 올바르지 않거나 데이터베이스 서버와 연결할 수 없습니다. 호스트의 데이터베이스 서버가 다운된 경우입니다. counts: views: 조회수 votes: 투표 answers: 답변 accepted: 채택됨 page_error: http_error: HTTP 오류 {{ code }} desc_403: 이 페이지에 접근할 권한이 없습니다. desc_404: 죄송합니다. 이 페이지는 존재하지 않습니다. desc_50X: 서버에서 오류가 발생하여 요청을 완료할 수 없습니다. back_home: 홈페이지로 돌아가기 page_maintenance: desc: "저희는 현재 유지보수 중입니다. 곧 돌아오겠습니다." nav_menus: dashboard: 대시보드 contents: 콘텐츠 questions: 질문 answers: 답변 users: 사용자 badges: 뱃지 flags: 신고하기 settings: 설정 general: 일반 interface: 인터페이스 smtp: SMTP branding: 브랜딩 legal: 법적 사항 write: 글 작성 terms: Terms tos: 이용 약관 privacy: 개인정보 보호 seo: 검색 엔진 최적화 customize: 사용자 정의 themes: 테마 login: 로그인 privileges: 권한 plugins: 플러그인 installed_plugins: 설치된 플러그인 apperance: 모양 community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: '{{site_name}}에 오신 것을 환영합니다' user_center: login: 로그인 qrcode_login_tip: '{{ agentName }}을(를) 사용하여 QR 코드를 스캔하고 로그인하세요.' login_failed_email_tip: 로그인 실패, 다시 시도하기 전에 이 앱이 이메일 정보에 접근할 수 있도록 허용하세요. badges: modal: title: 축하합니다 content: 새로운 뱃지를 획득했습니다. close: 닫기 confirm: 뱃지 보기 title: 뱃지 awarded: 수여됨 earned_×: '{{ number }} 개 획득' ×_awarded: "{{ number }} 개 수여됨" can_earn_multiple: 이 뱃지는 여러 번 획득할 수 있습니다. earned: 획득함 admin: admin_header: title: 관리자 dashboard: title: 대시보드 welcome: 관리자에 오신 것을 환영합니다! site_statistics: 사이트 통계 questions: "질문:" resolved: "해결됨:" unanswered: "답변이 없는 질문:" answers: "답변:" comments: "댓글:" votes: "투표:" users: "사용자:" flags: "신고:" reviews: "리뷰:" site_health: 사이트 상태 version: "버전:" https: "HTTPS:" upload_folder: "업로드 폴더:" run_mode: "실행 모드:" private: 비공개 public: 공개 smtp: "SMTP:" timezone: "시간대:" system_info: 시스템 정보 go_version: "Go 버전:" database: "데이터베이스:" database_size: "데이터베이스 크기:" storage_used: "사용 중인 저장 공간:" uptime: "가동 시간:" links: 링크 plugins: 플러그인 github: GitHub blog: 블로그 contact: 연락처 forum: 포럼 documents: 문서 feedback: 피드백 support: 지원 review: 검토 config: 설정 update_to: 업데이트 latest: 최신 버전 check_failed: 확인 실패 "yes": "예" "no": "아니요" not_allowed: 허용되지 않음 allowed: 허용됨 enabled: 활성화됨 disabled: 비활성화됨 writable: 쓰기 가능 not_writable: 쓰기 불가능 flags: title: 신고 pending: 처리 대기 중 completed: 완료됨 flagged: 신고됨 flagged_type: '{{ type }}로 신고됨' created: 생성됨 action: 동작 review: 검토 user_role_modal: title: 사용자 역할 변경 btn_cancel: 취소 btn_submit: 제출 new_password_modal: title: 새 비밀번호 설정 form: fields: password: label: 비밀번호 text: 사용자가 로그아웃되고 다시 로그인해야 합니다. msg: 비밀번호는 8-32자여야 합니다. btn_cancel: 취소 btn_submit: 제출 edit_profile_modal: title: 프로필 수정 form: fields: display_name: label: 표시 이름 msg_range: 표시 이름은 2-30 자 길이여야 합니다. username: label: 사용자 이름 msg_range: 유저 이름은 2-30 자 길이여야 합니다. email: label: 이메일 msg_invalid: 유효하지 않은 이메일 주소. edit_success: 성공적으로 수정되었습니다 btn_cancel: 취소 btn_submit: 제출 user_modal: title: 새 사용자 추가 form: fields: users: label: 대량 사용자 추가 placeholder: "홍길동, hong@example.com, BUSYopr2\n김철수, kim@example.com, fpDntV8q" text: 쉼표로 구분하여 “이름, 이메일, 비밀번호”를 입력하세요. 한 줄에 한 명의 사용자. msg: "사용자의 이메일을 입력하세요. 한 줄에 한 명씩 입력하세요." display_name: label: 표시 이름 msg: 표시 이름은 2-30 자 길이여야 합니다. email: label: 이메일 msg: 이메일이 유효하지 않습니다. password: label: 비밀번호 msg: 비밀번호는 8-32자여야 합니다. btn_cancel: 취소 btn_submit: 제출 users: title: 사용자 name: 이름 email: 이메일 reputation: 평판 created_at: 생성 시간 delete_at: 삭제된 시간 suspend_at: 정지된 시간 suspend_until: 정지 기한 status: 상태 role: 역할 action: 동작 change: 변경 all: 전체 staff: 스탭 more: 더 보기 inactive: 비활성화됨 suspended: 정지됨 deleted: 삭제됨 normal: 일반 Moderator: 관리자 Admin: 관리자 User: 사용자 filter: placeholder: "이름 또는 사용자 ID로 필터링" set_new_password: 새 비밀번호 설정 edit_profile: 프로필 수정 change_status: 상태 변경 change_role: 역할 변경 show_logs: 로그 표시 add_user: 사용자 추가 deactivate_user: title: 사용자 비활성화 content: 비활성화된 사용자는 이메일을 다시 확인해야 합니다. delete_user: title: 이 사용자 삭제 content: 정말로 이 사용자를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다! remove: 사용자의 모든 질문, 답변, 댓글 등을 삭제합니다. label: 사용자의 계정만 삭제하려면 이 옵션을 선택하지 마세요. text: 사용자의 계정만 삭제하려면 이 항목을 선택하지 마십시오. suspend_user: title: 이 사용자 정지 content: 정지된 사용자는 로그인할 수 없습니다. label: 사용자를 며칠 접근 금지 하시겠습니까? forever: 무기한 questions: page_title: 질문 unlisted: 비공개 post: 게시물 votes: 투표 answers: 답변 created: 생성됨 status: 상태 action: 동작 change: 변경 pending: 대기 중 filter: placeholder: "제목 또는 질문 ID로 필터링" answers: page_title: 답변 post: 게시물 votes: 투표 created: 생성됨 status: 상태 action: 동작 change: 변경 filter: placeholder: "제목 또는 답변 ID로 필터링" general: page_title: 일반 name: label: 사이트 이름 msg: 사이트 이름을 입력하세요. text: "사이트 이름, 타이틀 태그에 사용됩니다." site_url: label: 사이트 URL msg: 사이트 URL을 입력하세요. validate: 유효한 URL을 입력하세요. text: 사이트 주소입니다. short_desc: label: 짧은 사이트 설명 msg: 짧은 사이트 설명을 입력하세요. text: "홈페이지에서 사용되는 짧은 설명입니다." desc: label: 사이트 설명 msg: 사이트 설명을 입력하세요. text: "메타 설명 태그에 사용되는 한 문장 설명입니다." contact_email: label: 연락처 이메일 msg: 연락처 이메일을 입력하세요. validate: 유효한 이메일 주소를 입력하세요. text: 사이트를 책임지는 주요 연락처 이메일 주소입니다. check_update: label: 소프트웨어 업데이트 text: 소프트웨어 업데이트 자동 확인 interface: page_title: 인터페이스 language: label: 인터페이스 언어 msg: 인터페이스 언어를 선택하세요. text: 페이지를 새로고침하면 언어가 변경됩니다. time_zone: label: 시간대 msg: 시간대를 선택하세요. text: 본인과 같은 시간대의 도시를 선택하세요. avatar: label: 기본 아바타 text: 사용자 정의 아바타가 없는 사용자에게 표시됩니다. gravatar_base_url: label: Gravatar 기본 URL text: Gravatar 공급자의 API 기본 URL입니다. 비어 있으면 무시됩니다. smtp: page_title: SMTP from_email: label: 발신 이메일 msg: 발신 이메일을 입력하세요. text: 이메일 발신 주소입니다. from_name: label: 발신자 이름 msg: 발신자 이름을 입력하세요. text: 이메일 발신 시 사용될 이름입니다. smtp_host: label: SMTP 호스트 msg: SMTP 호스트를 입력하세요. text: 메일 서버 주소입니다. encryption: label: 암호화 msg: 암호화 방식을 선택하세요. text: 대부분의 서버에서 SSL을 권장합니다. ssl: SSL tls: TLS none: 없음 smtp_port: label: SMTP 포트 msg: SMTP 포트는 1에서 65535 사이의 숫자여야 합니다. text: 메일 서버의 포트 번호입니다. smtp_username: label: SMTP 사용자 이름 msg: SMTP 사용자 이름을 입력하세요. smtp_password: label: SMTP 비밀번호 msg: SMTP 비밀번호를 입력하세요. test_email_recipient: label: 테스트 이메일 수신자 text: 테스트 이메일을 받을 이메일 주소를 입력하세요. msg: 테스트 이메일 수신자가 유효하지 않습니다. smtp_authentication: label: 인증 사용 title: SMTP 인증 msg: SMTP 인증을 선택하세요. "yes": "예" "no": "아니오" branding: page_title: 브랜딩 logo: label: 로고 msg: 로고를 입력하세요. text: 사이트 좌측 상단에 표시될 로고 이미지입니다. 넓은 직사각형 이미지로, 높이는 56 이상이어야 하며 가로 세로 비율은 3:1 이상이어야 합니다. 비워 둘 경우 사이트 제목 텍스트가 표시됩니다. mobile_logo: label: 모바일 로고 text: 사이트의 모바일 버전에서 사용할 로고 이미지입니다. 넓은 직사각형 이미지로, 높이는 56 이상이어야 합니다. 비워 둘 경우 "로고" 설정에서 이미지가 사용됩니다. square_icon: label: 정사각형 아이콘 msg: 정사각형 아이콘을 입력하세요. text: 메타데이터 아이콘의 기본 이미지로 사용됩니다. 이상적으로는 512x512보다 큰 이미지여야 합니다. favicon: label: 파비콘 text: 사이트의 파비콘 이미지입니다. CDN에서 정상적으로 작동하려면 png 형식이어야 하며, 크기는 32x32로 조정됩니다. 비워 둘 경우 "정사각형 아이콘"이 사용됩니다. legal: page_title: 법적 고지 terms_of_service: label: 서비스 이용 약관 text: "여기에 서비스 이용 약관 내용을 추가할 수 있습니다. 이미 다른 곳에 문서가 호스팅되어 있다면 전체 URL을 여기에 제공하세요." privacy_policy: label: 개인정보 보호 정책 text: "여기에 개인정보 보호 정책 내용을 추가할 수 있습니다. 이미 다른 곳에 문서가 호스팅되어 있다면 전체 URL을 여기에 제공하세요." external_content_display: label: 외부 콘텐츠 text: "콘텐츠에는 외부 웹사이트에서 삽입된 이미지, 비디오 및 미디어가 포함됩니다." always_display: 항상 외부 콘텐츠 표시 ask_before_display: 외부 콘텐츠 표시 전 확인 write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: 답변 작성 label: 각 사용자는 각 질문에 대해 단 하나의 답변만 작성할 수 있습니다. text: "기존 답변을 개선하고 향상시키기 위해 편집 링크를 사용할 수 있습니다." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: 추천 태그 text: "추천 태그가 기본적으로 드롭다운 목록에 표시됩니다." msg: contain_reserved: "추천 태그에는 예약된 태그가 포함될 수 없습니다" required_tag: title: 필수 태그 설정 label: '"추천 태그" 를 필수 태그로 설정' text: "모든 새로운 질문은 최소한 하나의 추천 태그가 있어야 합니다." reserved_tags: label: 예약된 태그 text: "예약된 태그는 관리자만 사용할 수 있습니다." image_size: label: 최대 이미지 크기 (MB) text: "최대 이미지 업로드 크기입니다." attachment_size: label: 최대 첨부 파일 크기 (MB) text: "최대 첨부 파일 업로드 크기입니다." image_megapixels: label: 최대 이미지 메가픽셀 text: "이미지에 허용되는 최대 메가픽셀 수입니다." image_extensions: label: 허용된 이미지 확장자 text: "이미지 표시가 허용된 파일 확장자 목록입니다. 쉼표로 구분하세요." attachment_extensions: label: 인증된 첨부 파일 확장자 text: "업로드가 허용된 파일 확장자 목록입니다. 쉼표로 구분하세요. 경고: 업로드를 허용하면 보안 문제가 발생할 수 있습니다." seo: page_title: 검색 엔진 최적화 permalink: label: 영구 링크 text: 사용자 정의 URL 구조는 링크의 사용성과 미래 호환성을 향상시킬 수 있습니다. robots: label: robots.txt text: 이 설정은 사이트 설정과 관련된 내용을 영구적으로 덮어씁니다. themes: page_title: 테마 themes: label: 테마 text: 기존 테마를 선택하세요. color_scheme: label: 색상 스키마 navbar_style: label: 네비바 배경 스타일 primary_color: label: 주요 색상 text: 테마에서 사용할 색상을 수정합니다. layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS 및 HTML custom_css: label: 사용자 정의 CSS text: > head: label: 헤드 text: > header: label: 헤더 text: > footer: label: 푸터 text: 본문의 바로 앞에 삽입됩니다. sidebar: label: 사이드바 text: 사이드바에 삽입됩니다. login: page_title: 로그인 membership: title: 멤버십 label: 신규 등록 허용 text: 계정을 생성할 수 있는 사람을 제한하려면 끄세요. email_registration: title: 이메일 등록 label: 이메일 등록 허용 text: 이메일을 통한 새 계정 생성을 막으려면 끄세요. allowed_email_domains: title: 허용된 이메일 도메인 text: 사용자가 계정을 등록할 때 필수적으로 사용해야 하는 이메일 도메인입니다. 한 줄에 하나의 도메인을 입력하세요. 비어 있으면 무시됩니다. private: title: 비공개 label: 로그인 필수 text: 로그인한 사용자만이 이 커뮤니티에 접근할 수 있습니다. password_login: title: 비밀번호 로그인 label: 이메일과 비밀번호 로그인 허용 text: "경고: 끄면 다른 로그인 방법을 설정하지 않았다면 로그인할 수 없을 수 있습니다." installed_plugins: title: 설치된 플러그인 plugin_link: 플러그인은 기능을 확장하고 확장합니다. <1> 플러그인 저장소에서 플러그인을 찾을 수 있습니다. filter: all: 전체 active: 활성화됨 inactive: 비활성화됨 outdated: 오래된 상태 plugins: label: 플러그인 text: 기존 플러그인을 선택하세요. name: 이름 version: 버전 status: 상태 action: 작업 deactivate: 비활성화 activate: 활성화 settings: 설정 settings_users: title: 사용자 avatar: label: 기본 아바타 text: 사용자가 자신의 사용자 정의 아바타를 가지지 않았을 때 표시됩니다. gravatar_base_url: label: Gravatar 기본 URL text: Gravatar 공급자의 API 기본 URL입니다. 비어 있으면 무시됩니다. profile_editable: title: 프로필 편집 가능 allow_update_display_name: label: 사용자가 표시 이름을 변경할 수 있도록 허용 allow_update_username: label: 사용자가 사용자 이름을 변경할 수 있도록 허용 allow_update_avatar: label: 사용자가 프로필 이미지를 변경할 수 있도록 허용 allow_update_bio: label: 사용자가 자기 소개를 변경할 수 있도록 허용 allow_update_website: label: 사용자가 웹사이트를 변경할 수 있도록 허용 allow_update_location: label: 사용자가 위치 정보를 변경할 수 있도록 허용 privilege: title: 권한 level: label: 권한에 필요한 평판 레벨 text: 권한에 필요한 평판 레벨을 선택하세요. msg: should_be_number: 입력값은 숫자여야 합니다. number_larger_1: 숫자는 1 이상이어야 합니다. badges: action: 동작 active: 활성 activate: 활성화 all: 모두 awards: 수상 deactivate: 비활성화 filter: placeholder: 이름, 배지:id 로 필터링 group: 그룹 inactive: 비활성 name: 이름 show_logs: 로그 표시 status: 상태 title: 뱃지 apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (선택 사항) empty: 비어 있을 수 없습니다 invalid: 유효하지 않습니다 btn_submit: 저장 not_found_props: "필수 속성 {{ key }}을(를) 찾을 수 없습니다." select: 선택 page_review: review: 리뷰 proposed: 제안된 question_edit: 질문 편집 answer_edit: 답변 편집 tag_edit: 태그 편집 edit_summary: 편집 요약 edit_question: 질문 편집 edit_answer: 답변 편집 edit_tag: 태그 편집 empty: 남은 리뷰 작업이 없습니다. approve_revision_tip: 이 리비전을 승인하시겠습니까? approve_flag_tip: 이 신고를 승인하시겠습니까? approve_post_tip: 이 게시물을 승인하시겠습니까? approve_user_tip: 이 사용자를 승인하시겠습니까? suggest_edits: 제안된 편집 flag_post: 게시물 신고 flag_user: 사용자 신고 queued_post: 대기 중인 게시물 queued_user: 대기 중인 사용자 filter_label: 유형 reputation: 평판 flag_post_type: 이 게시물을 {{ type }}로 신고 처리했습니다. flag_user_type: 이 사용자를 {{ type }}로 신고 처리했습니다. edit_post: 게시물 편집 list_post: 게시물 목록 unlist_post: 게시물 비공개 timeline: undeleted: 복구됨 deleted: 삭제됨 downvote: 다운보트 upvote: 업보트 accept: 채택됨 cancelled: 취소됨 commented: 댓글 작성됨 rollback: 롤백 edited: 편집됨 answered: 답변됨 asked: 질문됨 closed: 닫힘 reopened: 다시 열림 created: 생성됨 pin: 고정됨 unpin: 고정 해제됨 show: 공개됨 hide: 비공개됨 title: "다음을 위한 히스토리" tag_title: "태그에 대한 타임라인" show_votes: "투표 보기" n_or_a: 없음 title_for_question: "질문에 대한 타임라인" title_for_answer: "{{ author }}가 {{ title }}에 대한 답변에 대한 타임라인" title_for_tag: "태그에 대한 타임라인" datetime: 날짜 및 시간 type: 유형 by: 작성자 comment: 코멘트 no_data: "아무 데이터도 찾을 수 없습니다." users: title: 사용자 users_with_the_most_reputation: 이번 주 평판이 가장 높은 사용자들 users_with_the_most_vote: 이번 주 투표를 가장 많이 한 사용자들 staffs: 우리 커뮤니티 스태프 reputation: 평판 votes: 투표 prompt: leave_page: 페이지를 떠나시겠습니까? changes_not_save: 변경 사항이 저장되지 않을 수 있습니다. draft: discard_confirm: 초안을 삭제하시겠습니까? messages: post_deleted: 이 게시물은 삭제되었습니다. post_cancel_deleted: 이 게시물이 삭제 취소되었습니다. post_pin: 이 게시물이 고정되었습니다. post_unpin: 이 게시물의 고정이 해제되었습니다. post_hide_list: 이 게시물이 목록에서 숨겨졌습니다. post_show_list: 이 게시물이 목록에 표시되었습니다. post_reopen: 이 게시물이 다시 열렸습니다. post_list: 이 게시물이 목록에 등록되었습니다. post_unlist: 이 게시물이 목록에서 등록 해제되었습니다. post_pending: 회원님의 게시물이 검토를 기다리고 있습니다. 미리보기입니다. 승인 후에 공개됩니다. post_closed: 이 게시물이 닫혔습니다. answer_deleted: 이 답변이 삭제되었습니다. answer_cancel_deleted: 이 답변이 삭제 취소되었습니다. change_user_role: 이 사용자의 역할이 변경되었습니다. user_inactive: 이 사용자는 이미 비활성 상태입니다. user_normal: 이 사용자는 이미 일반 사용자입니다. user_suspended: 이 사용자가 정지되었습니다. user_deleted: 이 사용자가 삭제되었습니다. user_added: User has been added successfully. badge_activated: 이 배지가 활성화되었습니다. badge_inactivated: 이 배지가 비활성화되었습니다. users_deleted: 이 사용자들이 삭제되었습니다. posts_deleted: 이 질문들이 삭제되었습니다. answers_deleted: 이 답변들이 삭제되었습니다. copy: 클립보드에 복사 copied: 복사됨 external_content_warning: 외부 이미지/미디어가 표시되지 않습니다. ================================================ FILE: i18n/ml_IN.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. forbidden_error: other: Forbidden. duplicate_request_error: other: Duplicate submission. action: report: other: Flag edit: other: Edit delete: other: Delete close: other: Close reopen: other: Reopen forbidden_error: other: Forbidden. pin: other: Pin hide: other: Unlist unpin: other: Unpin show: other: List invite_someone_to_answer: other: Edit undelete: other: Undelete merge: other: Merge role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. privilege: level_1: description: other: Level 1 (less reputation required for private team, group) level_2: description: other: Level 2 (low reputation required for startup community) level_3: description: other: Level 3 (high reputation required for mature community) level_custom: description: other: Custom Level rank_question_add_label: other: Ask question rank_answer_add_label: other: Write answer rank_comment_add_label: other: Write comment rank_report_add_label: other: Flag rank_comment_vote_up_label: other: Upvote comment rank_link_url_limit_label: other: Post more than 2 links at a time rank_question_vote_up_label: other: Upvote question rank_answer_vote_up_label: other: Upvote answer rank_question_vote_down_label: other: Downvote question rank_answer_vote_down_label: other: Downvote answer rank_invite_someone_to_answer_label: other: Invite someone to answer rank_tag_add_label: other: Create new tag rank_tag_edit_label: other: Edit tag description (need to review) rank_question_edit_label: other: Edit other's question (need to review) rank_answer_edit_label: other: Edit other's answer (need to review) rank_question_edit_without_review_label: other: Edit other's question without review rank_answer_edit_without_review_label: other: Edit other's answer without review rank_question_audit_label: other: Review question edits rank_answer_audit_label: other: Review answer edits rank_tag_audit_label: other: Review tag edits rank_tag_edit_without_review_label: other: Edit tag description without review rank_tag_synonym_label: other: Manage tag synonyms email: other: Email e_mail: other: Email password: other: Password pass: other: Password old_pass: other: Current password original_text: other: This post email_or_password_wrong_error: other: Email and password do not match. error: common: invalid_url: other: Invalid URL. status_invalid: other: Invalid status. password: space_invalid: other: Password cannot contain spaces. admin: cannot_update_their_password: other: You cannot modify your password. cannot_edit_their_profile: other: You cannot modify your profile. cannot_modify_self_status: other: You cannot modify your status. email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. question_closed_cannot_add: other: Questions are closed and cannot be added. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. illegal_email_domain_error: other: Email is not allowed from that email domain. Please use another one. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. already_deleted: other: This post has been deleted. meta: object_not_found: other: Meta object not found question: already_deleted: other: This post has been deleted. under_review: other: Your post is awaiting review. It will be visible after it has been approved. not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. vote_fail_to_meet_the_condition: other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. no_enough_rank_to_operate: other: You need at least {{.Rank}} reputation to do this. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: already_exist: other: Tag already exists. not_found: other: Tag not found. recommend_tag_not_found: other: Recommend tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. is_used_cannot_delete: other: You cannot delete a tag that is in use. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: The from name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to revise. user: external_login_missing_user_id: other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. external_login_unbinding_forbidden: other: Please set a login password for your account before you remove this login. email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration. not_allowed_login_via_password: other: Currently the site is not allowed to login via password. access_denied: other: Access denied page_access_denied: other: You do not have access to this page. add_bulk_users_format_error: other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. site_info: config_not_found: other: Site config not found. badge: object_not_found: other: Badge object not found reason: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude_or_abusive: name: other: rude or abusive desc: other: "A reasonable person would find this content inappropriate for respectful discourse." a_duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. placeholder: other: Enter the existing question link not_a_answer: name: other: not an answer desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." no_longer_needed: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. something: name: other: something else desc: other: This post requires staff attention for another reason not listed above. placeholder: other: Let us know specifically what you are concerned about community_specific: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. not_clarity: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. looks_ok: name: other: looks OK desc: other: This post is good as-is and not low quality. needs_edit: name: other: needs edit, and I did it desc: other: Improve and correct problems with this post yourself. needs_close: name: other: needs close desc: other: A closed question can't answer, but still can edit, vote and comment. needs_delete: name: other: needs delete desc: other: This post will be deleted. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified deleted_title: other: Deleted question questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted up_voted_question: other: upvoted question down_voted_question: other: downvoted question up_voted_answer: other: upvoted answer down_voted_answer: other: downvoted answer up_voted_comment: other: upvoted comment invited_you_to_answer: other: invited you to answer earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "[{{.SiteName}}] Confirm your new email address" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n

{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Password reset" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Confirm your new account" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Test Email" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: upvote upvoted: other: upvoted downvote: other: downvote downvoted: other: downvoted accept: other: accept accepted: other: accepted edit: other: edit review: queued_post: other: Queued post flagged_post: other: Flagged post suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: Editor desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki create_tag: Create Tag edit_tag: Edit Tag ask_a_question: Create Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users oauth_callback: Processing http_404: HTTP Error 404 http_50X: HTTP Error 500 http_403: HTTP Error 403 logout: Log Out posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notifications inbox: Inbox achievement: Achievements new_alerts: New alerts all_read: Mark all as read show_more: Show more someone: Someone inbox_type: all: All posts: Posts invites: Invites votes: Votes answer: Answer question: Question badge_award: Badge suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. contact_us: Contact us editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image file btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed {{size}} MB. desc: label: Description tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered list unordered_list: text: Bulleted list table: text: Table heading: Heading cell: Cell file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: Create new tag form: fields: display_name: label: Display name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description revision: label: Revision edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_cancel: Cancel btn_submit: Submit btn_post: Post new tag tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Are you sure you wish to delete? close: Close merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Edit Tag default_reason: Edit tag default_first_reason: Add tag btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day hours: hours days: days month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: "{{count}} more comments" tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. tip_vote: It adds something useful to the post edit_answer: title: Edit Answer default_reason: Edit answer default_first_reason: Add answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: Newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More wiki: Wiki ask: title: Create Question edit_title: Edit Question default_reason: Edit question default_first_reason: Create question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: What's your topic? Be specific. msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users badges: Badges profile: Profile setting: Settings logout: Log out admin: Admin review: Review bookmark: Bookmarks moderation: Moderation search: placeholder: Search footer: build_on: Powered by <1> Apache Answer upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. resend_email: url_label: Are you sure you want to resend the activation email? url_text: You can also give the activation link above to the user. login: login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New email msg: empty: Email cannot be empty. oauth: connect: Connect with {{ auth_name }} remove: Remove {{ auth_name }} oauth_bind_email: subtitle: Add a recovery email to your account. btn_update: Update email address email: label: Email msg: empty: Email cannot be empty. modal_title: Email already existes. modal_content: This email address already registered. Are you sure you want to connect to the existing account? modal_cancel: Change email modal_confirm: Connect to the existing account password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm new password settings: page_title: Settings goto_modify: Go to modify nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile image gravatar: Gravatar gravatar_text: You can change image on custom: Custom custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About me website: label: Website placeholder: "https://example.com" msg: Website incorrect format location: label: Location placeholder: "City, Country" notification: heading: Email Notifications turn_on: Turn on inbox: label: Inbox notifications description: Answers to your questions, comments, invites, and more. all_new_question: label: All new questions description: Get notified of all new questions. Up to 50 questions per week. all_new_question_for_following_tags: label: All new questions for following tags description: Get notified of new questions for following tags. account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. pass: label: Current password msg: Password cannot be empty. password_title: Password current_pass: label: Current password msg: empty: Current password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New password pass_confirm: label: Confirm new password interface: heading: Interface lang: label: Interface language text: User interface language. It will change when you refresh the page. my_logins: title: My logins label: Log in or sign up on this site using these accounts. modal_title: Remove login modal_content: Are you sure you want to remove this login from your account? modal_confirm_btn: Remove remove_success: Removed successfully toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. sent_success: Sent successfully related_question: title: Related answers: answers linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Invite People desc: Invite people you think can answer. invite: Invite to answer add: Add people search: Search people question_detail: action: Action created: Created Asked: Asked asked: asked update: Modified Edited: Edited edit: edited commented: commented Views: Viewed Follow: Follow Following: Following follow_tip: Follow this question to receive notifications answered: answered closed_in: Closed in show_exist: Show existing question. useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: title: Answers score: Score newest: Newest oldest: Oldest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer edit_answer: Edit my existing answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. tips: header_1: Thanks for your answer li1_1: Please be sure to answer the question. Provide details and share your research. li1_2: Back up any statements you make with references or personal experience. header_2: But avoid ... li2_1: Asking for help, seeking clarification, or responding to other answers. reopen: confirm_btn: Reopen title: Reopen this post content: Are you sure you want to reopen? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. confirm_btn: Pin delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_answer_deleted: This answer has been deleted undelete_title: Undelete this post undelete_desc: Are you sure you wish to undelete? btns: confirm: Confirm cancel: Cancel edit: Edit save: Save delete: Delete undelete: Undelete list: List unlist: Unlist unlisted: Unlisted login: Log in signup: Sign up logout: Log out verify: Verify create: Create approve: Approve reject: Reject skip: Skip discard_draft: Discard draft pinned: Pinned all: All question: Question answer: Answer comment: Comment refresh: Refresh resend: Resend deactivate: Deactivate active: Active suspend: Suspend unsuspend: Unsuspend close: Close reopen: Reopen ok: OK light: Light dark: Dark system_setting: System setting default: Default reset: Reset tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" counts_loading: "... Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Error... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage oops: Oops! invalid: The link you used no longer works. confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" x_posts: "{{ count }} Posts" questions: Questions answers: Answers newest: Newest active: Active hot: Hot frequent: Frequent recommend: Recommend score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes badges: Badges newest: Newest score: Score edit_profile: Edit profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined comma: "," last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? content_empty: No posts found. accepted: Accepted answered: answered asked: asked downvoted: downvoted mod_short: MOD mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions recent_badges: Recent Badges install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please choose a language db_type: label: Database engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database host placeholder: "db:3306" msg: Database host cannot be empty. db_name: label: Database name placeholder: answer msg: Database name cannot be empty. db_file: label: Database file placeholder: /data/answer.db msg: Database file cannot be empty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site name msg: Site name cannot be empty. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. max_length: Site URL must be at maximum 512 characters in length. contact_email: label: Contact email text: Email address of key contact responsible for this site. msg: empty: Contact email cannot be empty. incorrect: Contact email incorrect format. login_required: label: Private switch: Login required text: Only logged in users can access this community. admin_name: label: Name msg: Name cannot be empty. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_error: http_error: HTTP Error {{ code }} desc_403: You don't have permission to access this page. desc_404: Unfortunately, this page doesn't exist. desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users badges: Badges flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write terms: Terms tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes login: Login privileges: Privileges plugins: Plugins installed_plugins: Installed Plugins apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Welcome to {{site_name}} user_center: login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site statistics questions: "Questions:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Answers:" comments: "Comments:" votes: "Votes:" users: "Users:" flags: "Flags:" reviews: "Reviews:" site_health: Site health version: "Version:" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Private public: Public smtp: "SMTP:" timezone: "Timezone:" system_info: System info go_version: "Go version:" database: "Database:" database_size: "Database size:" storage_used: "Storage used:" uptime: "Uptime:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Contact forum: Forum documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled writable: Writable not_writable: Not writable flags: title: Flags pending: Pending completed: Completed flagged: Flagged flagged_type: Flagged {{ type }} created: Created action: Action review: Review user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: users: label: Bulk add user placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separate “name, email, password” with commas. One user per line. msg: "Please enter the user's email, one per line." display_name: label: Display name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Status role: Role action: Action change: Change all: All staff: Staff more: More inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password edit_profile: Edit profile change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user deactivate_user: title: Deactivate user content: An inactive user must re-validate their email. delete_user: title: Delete this user content: Are you sure you want to delete this user? This is permanent! remove: Remove their content label: Remove all questions, answers, comments, etc. text: Don’t check this if you wish to only delete the user’s account. suspend_user: title: Suspend this user content: A suspended user can't log in. label: How long will the user be suspended for? forever: Forever questions: page_title: Questions unlisted: Unlisted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change pending: Pending filter: placeholder: "Filter by title, question:id" answers: page_title: Answers post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short site description msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site description msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. check_update: label: Software updates text: Automatically check for updates interface: page_title: Interface language: label: Interface language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: From email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL tls: TLS none: None smtp_port: label: SMTP port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP username msg: SMTP username cannot be empty. smtp_password: label: SMTP password msg: SMTP password cannot be empty. test_email_recipient: label: Test email recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile logo text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square icon msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for the same question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. color_scheme: label: Color scheme navbar_style: label: Navbar background style primary_color: label: Primary color text: Modify the colors used by your themes layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: > head: label: Head text: > header: label: Header text: > footer: label: Footer text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: Allowed email domains text: Email domains that users must register accounts with. One domain per line. Ignored when empty. private: title: Private label: Login required text: Only logged in users can access this community. password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: All active: Active inactive: Inactive outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Name version: Version status: Status action: Action deactivate: Deactivate activate: Activate settings: Settings settings_users: title: Users avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Allow users to change their display name allow_update_username: label: Allow users to change their username allow_update_avatar: label: Allow users to change their profile image allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optional) empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." select: Select page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created pin: pinned unpin: unpinned show: listed hide: unlisted title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: Our community staff reputation: reputation votes: votes prompt: leave_page: Are you sure you want to leave the page? changes_not_save: Your changes may not be saved. draft: discard_confirm: Are you sure you want to discard your draft? messages: post_deleted: This post has been deleted. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/nl_NL.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. email: other: Email password: other: Password email_or_password_wrong_error: other: Email and password do not match. error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: not_found: other: Tag not found. recommend_tag_not_found: other: Recommend Tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: other: The From Name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to Revision. user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. report: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude: name: other: rude or abusive desc: other: A reasonable person would find this content inappropriate for respectful discourse. duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. not_answer: name: other: not an answer desc: other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. not_need: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. other: name: other: something else desc: other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comments tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to {{site_name}} btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to {{site_name}} success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/no_NO.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. email: other: Email password: other: Password email_or_password_wrong_error: other: Email and password do not match. error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: not_found: other: Tag not found. recommend_tag_not_found: other: Recommend Tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: other: The From Name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to Revision. user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. report: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude: name: other: rude or abusive desc: other: A reasonable person would find this content inappropriate for respectful discourse. duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. not_answer: name: other: not an answer desc: other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. not_need: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. other: name: other: something else desc: other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comments tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to {{site_name}} btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to {{site_name}} success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot recommend: Recommend score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/pl_PL.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Sukces. unknown: other: Nieznany błąd. request_format_error: other: Format żądania jest nieprawidłowy. unauthorized_error: other: Niezautoryzowany. database_error: other: Błąd serwera danych. forbidden_error: other: Zakazane. duplicate_request_error: other: Duplikat zgłoszenia. action: report: other: Zgłoś edit: other: Edytuj delete: other: Usuń close: other: Zamknij reopen: other: Otwórz ponownie forbidden_error: other: Zakazane. pin: other: Przypnij hide: other: Usuń z listy unpin: other: Odepnij show: other: Lista invite_someone_to_answer: other: Edytuj undelete: other: Przywróć merge: other: Merge role: name: user: other: Użytkownik admin: other: Administrator moderator: other: Moderator description: user: other: Domyślnie bez specjalnego dostępu. admin: other: Posiadać pełne uprawnienia dostępu do strony. moderator: other: Ma dostęp do wszystkich postów z wyjątkiem ustawień administratora. privilege: level_1: description: other: Poziom 1 (mniejsza reputacja wymagana dla prywatnego zespołu, grupy) level_2: description: other: Poziom 2 (niska reputacja wymagana dla społeczności startującej) level_3: description: other: Poziom 3 (wysoka reputacja wymagana dla dojrzałej społeczności) level_custom: description: other: Poziom niestandardowy rank_question_add_label: other: Zadaj pytanie rank_answer_add_label: other: Napisz odpowiedź rank_comment_add_label: other: Napisz komentarz rank_report_add_label: other: Zgłoś rank_comment_vote_up_label: other: Wyróżnij komentarz rank_link_url_limit_label: other: Opublikuj więcej niż 2 linki na raz rank_question_vote_up_label: other: Wyróżnij pytanie rank_answer_vote_up_label: other: Wyróżnij odpowiedź rank_question_vote_down_label: other: Oceń pytanie negatywnie rank_answer_vote_down_label: other: Oceń odpowiedź negatywnie rank_invite_someone_to_answer_label: other: Zaproś kogoś do odpowiedzi rank_tag_add_label: other: Utwórz nowy tag rank_tag_edit_label: other: Edytuj opis tagu (wymaga akceptacji) rank_question_edit_label: other: Edytuj pytanie innych (wymaga akceptacji) rank_answer_edit_label: other: Edytuj odpowiedź innych (wymaga akceptacji) rank_question_edit_without_review_label: other: Edytuj pytanie innych bez akceptacji rank_answer_edit_without_review_label: other: Edytuj odpowiedź innych bez akceptacji rank_question_audit_label: other: Przejrzyj edycje pytania rank_answer_audit_label: other: Przejrzyj edycje odpowiedzi rank_tag_audit_label: other: Przejrzyj edycje tagu rank_tag_edit_without_review_label: other: Edytuj opis tagu bez akceptacji rank_tag_synonym_label: other: Zarządzaj synonimami tagów email: other: E-mail e_mail: other: Email password: other: Hasło pass: other: Hasło old_pass: other: Current password original_text: other: Ten wpis email_or_password_wrong_error: other: Email lub hasło nie są poprawne. error: common: invalid_url: other: Nieprawidłowy URL. status_invalid: other: Nieprawidłowy status. password: space_invalid: other: Hasło nie może zawierać spacji. admin: cannot_update_their_password: other: Nie możesz zmieniać swojego hasła. cannot_edit_their_profile: other: Nie możesz modyfikować swojego profilu. cannot_modify_self_status: other: Nie możesz modyfikować swojego statusu. email_or_password_wrong: other: Emil lub hasło nie są zgodne. answer: not_found: other: Odpowiedź nie została odnaleziona. cannot_deleted: other: Brak uprawnień do usunięcia. cannot_update: other: Brak uprawnień do aktualizacji. question_closed_cannot_add: other: Pytania są zamknięte i nie można ich dodawać. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Komentarz nie może edytować. not_found: other: Komentarz nie został odnaleziony. cannot_edit_after_deadline: other: Czas komentowania był zbyt długi, aby go zmodyfikować. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: E-mail już istnieje. need_to_be_verified: other: E-mail powinien zostać zweryfikowany. verify_url_expired: other: Adres URL zweryfikowanej wiadomości e-mail wygasł, prosimy o ponowne wysłanie wiadomości e-mail. illegal_email_domain_error: other: Wysyłanie wiadomości e-mail z tej domeny jest niedozwolone. Użyj innej domeny. lang: not_found: other: Nie znaleziono pliku językowego. object: captcha_verification_failed: other: Nieprawidłowa captcha. disallow_follow: other: Nie wolno ci podążać za nimi. disallow_vote: other: Nie masz uprawnień do głosowania. disallow_vote_your_self: other: Nie możesz głosować na własne posty. not_found: other: Obiekt nie został odnaleziony. verification_failed: other: Weryfikacja nie powiodła się. email_or_password_incorrect: other: Email lub hasło są nieprawidłowe. old_password_verification_failed: other: Stara weryfikacja hasła nie powiodła się new_password_same_as_previous_setting: other: Nowe hasło jest takie samo jak poprzednie. already_deleted: other: Ten wpis został usunięty. meta: object_not_found: other: Meta obiekt nie został odnaleziony question: already_deleted: other: Ten post został usunięty. under_review: other: Twój post oczekuje na recenzje. Będzie widoczny po jej akceptacji. not_found: other: Pytanie nie zostało odnalezione. cannot_deleted: other: Brak uprawnień do usunięcia. cannot_close: other: Brak uprawnień do zamknięcia. cannot_update: other: Brak uprawnień do edycji. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Ranga nie spełnia warunku. vote_fail_to_meet_the_condition: other: Dziękujemy za opinię. Potrzebujesz co najmniej {{.Rank}} reputacji, aby oddać głos. no_enough_rank_to_operate: other: Potrzebujesz co najmniej {{.Rank}} reputacji, aby to zrobić. report: handle_failed: other: Nie udało się obsłużyć raportu. not_found: other: Raport nie został znaleziony. tag: already_exist: other: Tag już istnieje. not_found: other: Tag nie został znaleziony. recommend_tag_not_found: other: Zalecany tag nie istnieje. recommend_tag_enter: other: Proszę wprowadzić przynajmniej jeden wymagany tag. not_contain_synonym_tags: other: Nie powinno zawierać tagów synonimów. cannot_update: other: Brak uprawnień do aktualizacji. is_used_cannot_delete: other: Nie możesz usunąć tagu, który jest w użyciu. cannot_set_synonym_as_itself: other: Nie można ustawić synonimu aktualnego tagu jako takiego. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: Nazwą nadawcy nie może być adresem e-mail. theme: not_found: other: Nie znaleziono motywu. revision: review_underway: other: Nie można teraz edytować, istnieje wersja w kolejce sprawdzeń. no_permission: other: Brak uprawnień do wersji. user: external_login_missing_user_id: other: Platforma zewnętrzna nie dostarcza unikalnego identyfikatora użytkownika (UserID), dlatego nie możesz się zalogować. Skontaktuj się z administratorem witryny. external_login_unbinding_forbidden: other: Proszę ustawić hasło logowania dla swojego konta przed usunięciem tego logowania. email_or_password_wrong: other: other: Adres email i hasło nie są zgodne. not_found: other: Użytkownik nie został znaleziony. suspended: other: Użytkownik został zawieszony. username_invalid: other: Nazwa użytkownika jest nieprawidłowa. username_duplicate: other: Nazwa użytkownika jest już zajęta. set_avatar: other: Nie udało się ustawić awatara. cannot_update_your_role: other: Nie możesz zmienić swojej roli. not_allowed_registration: other: Obecnie strona nie zezwala na rejestracje. not_allowed_login_via_password: other: Obecnie strona nie zezwala na logowanie się za pomocą hasła. access_denied: other: Odmowa dostępu. page_access_denied: other: Nie masz dostępu do tej strony. add_bulk_users_format_error: other: "Błąd {{.Field}} w pobliżu '{{.Content}}' w linii {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Liczba użytkowników, których dodasz na raz, powinna mieścić się w przedziale 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Nie udało się odczytać pliku konfiguracyjnego. database: connection_failed: other: Nie udało się połączyć z bazą danych. create_table_failed: other: Nie udało się utworzyć tabeli. install: create_config_failed: other: Nie można utworzyć pliku config.yaml. upload: unsupported_file_format: other: Nieobsługiwany format pliku. site_info: config_not_found: other: Nie znaleziono konfiguracji strony. badge: object_not_found: other: Nie znaleziono obiektu odznaki reason: spam: name: other: spam desc: other: Ten post jest reklamą lub wandalizmem. Nie jest przydatny ani istotny dla bieżącego tematu. rude_or_abusive: name: other: niegrzeczny lub obraźliwy desc: other: "Rozsądna osoba uznałaby tę treść za nieodpowiednią do dyskusji opartej na szacunku." a_duplicate: name: other: duplikat desc: other: To pytanie zostało już wcześniej zadane i ma już odpowiedź. placeholder: other: Wprowadź link do istniejącego pytania not_a_answer: name: other: nie jest odpowiedzią desc: other: "Ta wiadomość została zamieszczona jako odpowiedź, ale nie próbuje odpowiedzieć na pytanie. Powinna być prawdopodobnie edycją, komentarzem, kolejnym pytaniem lub całkowicie usunięta." no_longer_needed: name: other: nie jest już potrzebne desc: other: Ten komentarz jest przestarzały, prowadzi do rozmowy lub nie jest związany z tą wiadomością. something: name: other: coś innego desc: other: Ta wiadomość wymaga uwagi personelu z innego powodu, który nie jest wymieniony powyżej. placeholder: other: Poinformuj nas dokładnie, o co Ci chodzi community_specific: name: other: powód specyficzny dla społeczności desc: other: To pytanie nie spełnia wytycznych społeczności. not_clarity: name: other: wymaga szczegółów lub wyjaśnienia desc: other: To pytanie obecnie zawiera wiele pytań w jednym. Powinno skupić się tylko na jednym problemie. looks_ok: name: other: Wygląda poprawnie desc: other: Ta wiadomość jest dobra w obecnej formie i nie jest niskiej jakości. needs_edit: name: other: wymaga edycji, a ja to zrobiłem/am desc: other: Popraw i skoryguj problemy w tej wiadomości samodzielnie. needs_close: name: other: wymaga zamknięcia desc: other: Na zamknięte pytanie nie można odpowiadać, ale wciąż można edytować, głosować i komentować. needs_delete: name: other: wymaga usunięcia desc: other: Ta wiadomość zostanie usunięta. question: close: duplicate: name: other: spam desc: other: To pytanie zostało już wcześniej zadane i ma już odpowiedź. guideline: name: other: powód specyficzny dla społeczności desc: other: To pytanie nie spełnia wytycznych społeczności. multiple: name: other: wymaga szczegółów lub wyjaśnienia desc: other: To pytanie obecnie zawiera wiele pytań w jednym. Powinno się skupić tylko na jednym problemie. other: name: other: coś innego desc: other: Ten post wymaga jeszcze jednego powodu, który nie został wymieniony powyżej. operation_type: asked: other: zapytano answered: other: odpowiedziano modified: other: zmodyfikowane deleted_title: other: Usunięte pytanie questions_title: other: Pytania tag: tags_title: other: Tagi no_description: other: Tag nie posiada opisu. notification: action: update_question: other: zaktualizował/a pytanie answer_the_question: other: odpowiedz na pytanie update_answer: other: aktualizuj odpowiedź accept_answer: other: zaakceptował/a odpowiedź comment_question: other: skomentował/a pytanie comment_answer: other: skomentował/a odpowiedź reply_to_you: other: odpowiedział/a tobie mention_you: other: wspomniał/a o tobie your_question_is_closed: other: Twoje pytanie zostało zamknięte your_question_was_deleted: other: Twoje pytanie zostało usunięte your_answer_was_deleted: other: Twoja odpowiedź została usunięta your_comment_was_deleted: other: Twój komentarz został usunięty up_voted_question: other: pytanie przegłosowane down_voted_question: other: pytanie odrzucone up_voted_answer: other: odpowiedź przegłosowana down_voted_answer: other: odrzucona odpowiedź up_voted_comment: other: komentarz upvote invited_you_to_answer: other: zaproszono Cię do odpowiedzi earned_badge: other: Zdobyłeś odznakę "{{.BadgeName}}" email_tpl: change_email: title: other: "[{{.SiteName}}] Potwierdź swój nowy adres e-mail" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} odpowiedział(-a) na pytanie" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} zaprosił(a) Cię do odpowiedzi" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} skomentował/-a Twój wpis" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] Nowe pytanie: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Reset hasła" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Potwierdź swoje nowe konto" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Wiadomość testowa" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: oceń pozytywnie upvoted: other: polubione downvote: other: oceń negatywnie downvoted: other: oceniono negatywnie accept: other: akceptuj accepted: other: zaakceptowane edit: other: edytuj review: queued_post: other: Post w kolejce flagged_post: other: Post oznaczony suggested_post_edit: other: Sugerowane zmiany reaction: tooltip: other: "{{ .Names }} już {{ .Count }} razy ..." badge: default_badges: autobiographer: name: other: Autobiografista desc: other: Wypełniono informacje profil. certified: name: other: Certyfikowany desc: other: Ukończono nasz nowy samouczek. editor: name: other: Edytor desc: other: Pierwsza edycja posta. first_flag: name: other: Pierwsza flaga desc: other: Po raz pierwszy oznaczono post. first_upvote: name: other: Pierwszy pozytywny głos desc: other: First up voted a post. first_link: name: other: Pierwszy odnośnik desc: other: First added a link to another post. first_reaction: name: other: Pierwsza Reakcja desc: other: First reacted to the post. first_share: name: other: Pierwsze udostępnianie desc: other: First shared a post. scholar: name: other: Scholar desc: other: Zadane pytania i zaakceptowane odpowiedź. commentator: name: other: Commentator desc: other: Pozostaw 5 komentarzy. new_user_of_the_month: name: other: Nowy użytkownik miesiąca desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Jak formatować desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Poprzedni next: Następny page_title: question: Pytanie questions: Pytania tag: Tag tags: Tagi tag_wiki: wiki tagu create_tag: Utwórz tag edit_tag: Edytuj tag ask_a_question: Create Question edit_question: Edytuj pytanie edit_answer: Edytuj odpowiedź search: Szukaj posts_containing: Posty zawierające settings: Ustawienia notifications: Powiadomienia login: Zaloguj się sign_up: Zarejestruj się account_recovery: Odzyskiwanie konta account_activation: Aktywacja konta confirm_email: Potwierdź adres e-mail account_suspended: Konto zawieszone admin: Administrator change_email: Zmień e-mail install: Instalacja Answer upgrade: Aktualizacja Answer maintenance: Przerwa techniczna users: Użytkownicy oauth_callback: Przetwarzanie http_404: Błąd HTTP 404 http_50X: Błąd HTTP 500 http_403: Błąd HTTP 403 logout: Wyloguj się posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Powiadomienia inbox: Skrzynka odbiorcza achievement: Osiągnięcia new_alerts: Nowe powiadomienia all_read: Oznacz wszystkie jako przeczytane show_more: Pokaż więcej someone: Ktoś inbox_type: all: Wszystko posts: Posty invites: Zaproszenia votes: Głosy answer: Answer question: Question badge_award: Badge suspended: title: Twoje konto zostało zawieszone until_time: "Twoje konto zostało zawieszone do {{ time }}." forever: Ten użytkownik został na zawsze zawieszony. end: Nie spełniasz wytycznych społeczności. contact_us: Skontaktuj się z nami editor: blockquote: text: Cytat bold: text: Pogrubienie chart: text: Wykres flow_chart: Wykres przepływu sequence_diagram: Diagram sekwencji class_diagram: Diagram klas state_diagram: Diagram stanów entity_relationship_diagram: Diagram związków encji user_defined_diagram: Diagram zdefiniowany przez użytkownika gantt_chart: Wykres Gantta pie_chart: Wykres kołowy code: text: Przykład kodu add_code: Dodaj przykład kodu form: fields: code: label: Kod msg: empty: Kod nie może być pusty. language: label: Język placeholder: Wykrywanie automatyczne btn_cancel: Anuluj btn_confirm: Dodaj formula: text: Formuła options: inline: Formuła w linii block: Formuła blokowa heading: text: Nagłówek options: h1: Nagłówek 1 h2: Nagłówek 2 h3: Nagłówek 3 h4: Nagłówek 4 h5: Nagłówek 5 h6: Nagłówek 6 help: text: Pomoc hr: text: Linia pozioma image: text: Obrazek add_image: Dodaj obrazek tab_image: Prześlij obrazek form_image: fields: file: label: Plik graficzny btn: Wybierz obrazek msg: empty: Plik nie może być pusty. only_image: Dozwolone są tylko pliki obrazków. max_size: File size cannot exceed {{size}} MB. desc: label: Opis tab_url: Adres URL obrazka form_url: fields: url: label: Adres URL obrazka msg: empty: Adres URL obrazka nie może być pusty. name: label: Opis btn_cancel: Anuluj btn_confirm: Dodaj uploading: Przesyłanie indent: text: Wcięcie outdent: text: Wcięcie zewnętrzne italic: text: Podkreślenie link: text: Hiperłącze add_link: Dodaj hiperłącze form: fields: url: label: Adres URL msg: empty: Adres URL nie może być pusty. name: label: Opis btn_cancel: Anuluj btn_confirm: Dodaj ordered_list: text: Lista numerowana unordered_list: text: Lista wypunktowana table: text: Tabela heading: Nagłówek cell: Komórka file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: Zamykam ten post jako... btn_cancel: Anuluj btn_submit: Prześlij remark: empty: Nie może być puste. msg: empty: Proszę wybrać powód. report_modal: flag_title: Oznaczam ten post jako... close_title: Zamykam ten post jako... review_question_title: Przegląd pytania review_answer_title: Przegląd odpowiedzi review_comment_title: Przegląd komentarza btn_cancel: Anuluj btn_submit: Prześlij remark: empty: Nie może być puste. msg: empty: Proszę wybrać powód. not_a_url: Format adresu URL jest nieprawidłowy. url_not_match: Wskazany URL nie pasuje do bieżącej witryny. tag_modal: title: Utwórz nowy tag form: fields: display_name: label: Nazwa wyświetlana msg: empty: Nazwa wyświetlana nie może być pusta. range: Nazwa wyświetlana może zawierać maksymalnie 35 znaków. slug_name: label: Adres URL slug desc: Odnośnik URL może zawierać maksymalnie 35 znaków. msg: empty: Odnośnik URL nie może być pusty. range: Odnośnik URL może zawierać maksymalnie 35 znaków. character: Slug adresu URL zawiera niedozwolony zestaw znaków. desc: label: Opis revision: label: Wersja edit_summary: label: Podsumowanie edycji placeholder: >- Krótkie wyjaśnienie zmian (poprawa pisowni, naprawa gramatyki, poprawa formatowania) btn_cancel: Anuluj btn_submit: Prześlij btn_post: Opublikuj nowy tag tag_info: created_at: Utworzony edited_at: Edytowany history: Historia synonyms: title: Synonimy text: Następujące tagi zostaną przekierowane na empty: Nie znaleziono synonimów. btn_add: Dodaj synonim btn_edit: Edytuj btn_save: Zapisz synonyms_text: Następujące tagi zostaną przekierowane na delete: title: Usuń ten tag tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Czy na pewno chcesz usunąć? close: Zamknij merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Edytuj tag default_reason: Edytuj tag default_first_reason: Dodaj tag btn_save_edits: Zapisz edycje btn_cancel: Anuluj dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [o] HH:mm" now: teraz x_seconds_ago: "{{count}} s temu" x_minutes_ago: "{{count}} min temu" x_hours_ago: "{{count}} h temu" hour: godzina day: dzień hours: godziny days: dni month: month months: months year: year reaction: heart: serce smile: uśmiech frown: niezadowolenie btn_label: dodaj lub usuń reakcje undo_emoji: cofnij reakcje {{ emoji }} react_emoji: zareaguj {{ emoji }} unreact_emoji: usuń reakcje {{ emoji }} comment: btn_add_comment: Dodaj komentarz reply_to: Odpowiedź na btn_reply: Odpowiedz btn_edit: Edytuj btn_delete: Usuń btn_flag: Zgłoś btn_save_edits: Zapisz edycje btn_cancel: Anuluj show_more: "Jest {{count}} komentarzy, pokaż więcej" tip_question: >- Użyj komentarzy, aby poprosić o dodatkowe informacje lub sugerować poprawki. Unikaj udzielania odpowiedzi na pytania w komentarzach. tip_answer: >- Użyj komentarzy, aby odpowiedzieć innym użytkownikom lub powiadomić ich o zmianach. Jeśli dodajesz nowe informacje, edytuj swój post zamiast komentować. tip_vote: Dodaje coś wartościowego do posta edit_answer: title: Edytuj odpowiedź default_reason: Edytuj odpowiedź default_first_reason: Dodaj odpowiedź form: fields: revision: label: Rewizja answer: label: Odpowiedź feedback: characters: Treść musi mieć co najmniej 6 znaków. edit_summary: label: Podsumowanie edycji placeholder: >- Pokrótce opisz swoje zmiany (poprawa pisowni, naprawa gramatyki, poprawa formatowania) btn_save_edits: Zapisz edycje btn_cancel: Anuluj tags: title: Tagi sort_buttons: popular: Popularne name: Nazwa newest: Najnowsze button_follow: Obserwuj button_following: Obserwowane tag_label: pytania search_placeholder: Filtruj według nazwy tagu no_desc: Tag nie posiada opisu. more: Więcej wiki: Wiki ask: title: Create Question edit_title: Edytuj pytanie default_reason: Edytuj pytanie default_first_reason: Create question similar_questions: Podobne pytania form: fields: revision: label: Rewizja title: label: Tytuł placeholder: What's your topic? Be specific. msg: empty: Tytuł nie może być pusty. range: Tytuł do 150 znaków body: label: Treść msg: empty: Treść nie może być pusta. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Tagi msg: empty: Tagi nie mogą być puste. answer: label: Odpowiedź msg: empty: Odpowiedź nie może być pusta. edit_summary: label: Podsumowanie edycji placeholder: >- Pokrótce opisz swoje zmiany (poprawa pisowni, naprawa gramatyki, poprawa formatowania) btn_post_question: Opublikuj swoje pytanie btn_save_edits: Zapisz edycje answer_question: Odpowiedz na swoje pytanie post_question&answer: Opublikuj swoje pytanie i odpowiedź tag_selector: add_btn: Dodaj tag create_btn: Utwórz nowy tag search_tag: Wyszukaj tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Nie znaleziono pasujących tagów tag_required_text: Wymagany tag (co najmniej jeden) header: nav: question: Pytania tag: Tagi user: Użytkownicy badges: Badges profile: Profil setting: Ustawienia logout: Wyloguj admin: Administrator review: Recenzja bookmark: Zakładki moderation: Moderacja search: placeholder: Szukaj footer: build_on: Powered by <1> Apache Answer upload_img: name: Zmień loading: Wczytywanie... pic_auth_code: title: Captcha placeholder: Wpisz tekst z obrazka powyżej msg: empty: Captcha nie może być pusty. inactive: first: >- Czas na ostatni krok! Wysłaliśmy wiadomość aktywacyjną na adres {{mail}}. Prosimy postępować zgodnie z instrukcjami zawartymi w wiadomości w celu aktywacji Twojego konta. info: "Jeśli nie dotarła, sprawdź folder ze spamem." another: >- Wysłaliśmy kolejną wiadomość aktywacyjną na adres {{mail}}. Może to potrwać kilka minut, zanim dotrze; upewnij się, że sprawdzasz folder ze spamem. btn_name: Ponownie wyślij wiadomość aktywacyjną change_btn_name: Zmień adres e-mail msg: empty: Nie może być puste. resend_email: url_label: Czy na pewno chcesz ponownie wysłać e-mail aktywacyjny? url_text: Możesz również podać powyższy link aktywacyjny użytkownikowi. login: login_to_continue: Zaloguj się, aby kontynuować info_sign: Nie masz jeszcze konta? <1>Zarejestruj się info_login: Masz już konto? <1>Zaloguj się agreements: Rejestrując się, wyrażasz zgodę na <1>politykę prywatności i <3>warunki korzystania z usługi. forgot_pass: Zapomniałeś hasła? name: label: Imię msg: empty: Imię nie może być puste. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Adres e-mail msg: empty: Adres e-mail nie może być pusty. password: label: Hasło msg: empty: Hasło nie może być puste. different: Wprowadzone hasła są niezgodne account_forgot: page_title: Zapomniałeś hasła btn_name: Wyślij mi e-mail odzyskiwania send_success: >- Jeśli istnieje konto powiązane z adresem {{mail}}, wkrótce otrzymasz wiadomość e-mail z instrukcjami dotyczącymi resetowania hasła. email: label: Adres e-mail msg: empty: Adres e-mail nie może być pusty. change_email: btn_cancel: Anuluj btn_update: Zaktualizuj adres e-mail send_success: >- Jeśli istnieje konto powiązane z adresem {{mail}}, wkrótce otrzymasz wiadomość e-mail z instrukcjami dotyczącymi zmiany adresu e-mail. email: label: Nowy email msg: empty: Adres e-mail nie może być pusty. oauth: connect: Połącz z {{ auth_name }} remove: Usuń {{ auth_name }} oauth_bind_email: subtitle: Dodaj e-mail odzyskiwania do swojego konta. btn_update: Zaktualizuj adres e-mail email: label: Adres e-mail msg: empty: Adres e-mail nie może być pusty. modal_title: Adres e-mail już istnieje. modal_content: Ten adres e-mail jest już zarejestrowany. Czy na pewno chcesz połączyć się z istniejącym kontem? modal_cancel: Zmień adres e-mail modal_confirm: Połącz z istniejącym kontem password_reset: page_title: Resetowanie hasła btn_name: Zresetuj moje hasło reset_success: >- Pomyślnie zmieniono hasło; zostaniesz przekierowany na stronę logowania. link_invalid: >- Przepraszamy, ten link do resetowania hasła jest już nieaktualny. Być może Twoje hasło jest już zresetowane? to_login: Przejdź do strony logowania password: label: Hasło msg: empty: Hasło nie może być puste. length: Długość musi wynosić od 8 do 32 znaków. different: Wprowadzone hasła są niezgodne. password_confirm: label: Potwierdź nowe hasło settings: page_title: Ustawienia goto_modify: Przejdź do modyfikacji nav: profile: Profil notification: Powiadomienia account: Konto interface: Interfejs profile: heading: Profil btn_name: Zapisz display_name: label: Nazwa wyświetlana msg: Wyświetlana nazwa nie może być pusta. msg_range: Display name must be 2-30 characters in length. username: label: Nazwa użytkownika caption: Ludzie mogą oznaczać Cię jako "@nazwa_użytkownika". msg: Nazwa użytkownika nie może być pusta. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Zdjęcie profilowe gravatar: Gravatar gravatar_text: Możesz zmienić obraz na stronie custom: Własne custom_text: Możesz przesłać własne zdjęcie. default: Systemowe msg: Prosimy o przesłanie awatara bio: label: O mnie website: label: Strona internetowa placeholder: "https://przyklad.com" msg: Nieprawidłowy format strony internetowej location: label: Lokalizacja placeholder: "Miasto, Kraj" notification: heading: Powiadomienia email turn_on: Włącz inbox: label: Powiadomienia skrzynki odbiorczej description: Odpowiedzi na Twoje pytania, komentarze, zaproszenia i inne. all_new_question: label: Wszystkie nowe pytania description: Otrzymuj powiadomienia o wszystkich nowych pytaniach. Do 50 pytań tygodniowo. all_new_question_for_following_tags: label: Wszystkie nowe pytania dla obserwowanych tagów description: Otrzymuj powiadomienia o nowych pytaniach do obserwowanych tagów. account: heading: Konto change_email_btn: Zmień adres e-mail change_pass_btn: Zmień hasło change_email_info: >- Wysłaliśmy e-mail na ten adres. Prosimy postępować zgodnie z instrukcjami potwierdzającymi. email: label: Email new_email: label: Nowy Email msg: Nowy Email nie może być pusty. pass: label: Aktualne hasło msg: Hasło nie może być puste. password_title: Hasło current_pass: label: Aktualne hasło msg: empty: Obecne hasło nie może być puste. length: Długość musi wynosić od 8 do 32 znaków. different: Dwa wprowadzone hasła nie są zgodne. new_pass: label: Nowe hasło pass_confirm: label: Potwierdź nowe hasło interface: heading: Interfejs lang: label: Język Interfejsu text: Język interfejsu użytkownika. Zmieni się po odświeżeniu strony. my_logins: title: Moje logowania label: Zaloguj się lub zarejestruj na tej stronie za pomocą tych kont. modal_title: Usuń logowanie modal_content: Czy na pewno chcesz usunąć to logowanie z Twojego konta? modal_confirm_btn: Usuń remove_success: Pomyślnie usunięto toast: update: pomyślnie zaktualizowane update_password: Hasło zostało pomyślnie zmienione. flag_success: Dzięki za zgłoszenie. forbidden_operate_self: Zakazane działanie na sobie review: Twoja poprawka zostanie wyświetlona po zatwierdzeniu. sent_success: Wysyłanie zakończone powodzeniem related_question: title: Related answers: odpowiedzi linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Ludzie pytali desc: Wybierz osoby, które mogą znać odpowiedź. invite: Zaproś do odpowiedzi add: Dodaj osoby search: Wyszukaj osoby question_detail: action: Akcja created: Created Asked: Zadane asked: zadał(a) update: Zmodyfikowane Edited: Edited edit: edytowany commented: skomentowano Views: Wyświetlone Follow: Obserwuj Following: Obserwuje follow_tip: Obserwuj to pytanie, aby otrzymywać powiadomienia answered: odpowiedziano closed_in: Zamknięte za show_exist: Pokaż istniejące pytanie. useful: Przydatne question_useful: Jest przydatne i jasne question_un_useful: Jest niejasne lub nieprzydatne question_bookmark: Dodaj do zakładek to pytanie answer_useful: Jest przydatna answer_un_useful: To nie jest użyteczne answers: title: Odpowiedzi score: Ocena newest: Najnowsze oldest: Najstarsze btn_accept: Akceptuj btn_accepted: Zaakceptowane write_answer: title: Twoja odpowiedź edit_answer: Edytuj moją obecną odpowiedź btn_name: Wyślij swoją odpowiedź add_another_answer: Dodaj kolejną odpowiedź confirm_title: Kontynuuj odpowiedź continue: Kontynuuj confirm_info: >-

Czy na pewno chcesz dodać kolejną odpowiedź?

Możesz zamiast tego użyć linku edycji, aby udoskonalić i poprawić istniejącą odpowiedź.

empty: Odpowiedź nie może być pusta. characters: Treść musi mieć co najmniej 6 znaków. tips: header_1: Dziękujemy za Twoją odpowiedź li1_1: Prosimy, upewnij się, że odpowiadasz na pytanie. Podaj szczegóły i podziel się swoimi badaniami. li1_2: Popieraj swoje stwierdzenia referencjami lub osobistym doświadczeniem. header_2: Ale unikaj ... li2_1: Prośby o pomoc, pytania o wyjaśnienie lub odpowiadanie na inne odpowiedzi. reopen: confirm_btn: Ponowne otwarcie title: Otwórz ponownie ten post content: Czy na pewno chcesz go ponownie otworzyć? list: confirm_btn: Lista title: Pokaż ten post content: Are you sure you want to list? unlist: confirm_btn: Usuń z listy title: Usuń ten post z listy content: Czy na pewno chcesz usunąć z listy? pin: title: Przypnij ten post content: Czy na pewno chcesz przypiąć go globalnie? Ten post będzie wyświetlany na górze wszystkich list postów. confirm_btn: Przypnij delete: title: Usuń ten post question: >- Nie zalecamy usuwanie pytań wraz z udzielonymi, ponieważ pozbawia to przyszłych czytelników tej wiedzy.

Powtarzające się usuwanie pytań z odpowiedziami może skutkować zablokowaniem Twojego konta w zakresie zadawania pytań. Czy na pewno chcesz usunąć? answer_accepted: >-

Nie zalecamy usuwania zaakceptowanych już odpowiedzi, ponieważ pozbawia to przyszłych czytelników tej wiedzy.

Powtarzające się usuwanie zaakceptowanych odpowiedzi może skutkować zablokowaniem Twojego konta w zakresie udzielania odpowiedzi. Czy na pewno chcesz usunąć? other: Czy na pewno chcesz usunąć? tip_answer_deleted: Ta odpowiedź została usunięta undelete_title: Cofnij usunięcie tego posta undelete_desc: Czy na pewno chcesz cofnąć usunięcie? btns: confirm: Potwierdź cancel: Anuluj edit: Edytuj save: Zapisz delete: Usuń undelete: Przywróć list: Lista unlist: Usuń z listy unlisted: Usunięte z listy login: Zaloguj się signup: Zarejestruj się logout: Wyloguj się verify: Zweryfikuj create: Create approve: Zatwierdź reject: Odrzuć skip: Pominięcie discard_draft: Odrzuć szkic pinned: Przypięte all: Wszystkie question: Pytanie answer: Odpowiedź comment: Komentarz refresh: Odśwież resend: Wyślij ponownie deactivate: Deaktywuj active: Aktywne suspend: Zawieś unsuspend: Cofnij zawieszenie close: Zamknij reopen: Otwórz ponownie ok: Ok light: Jasny dark: Ciemny system_setting: Ustawienia systemowe default: Domyślne reset: Reset tag: Tag post_lowercase: wpis filter: Filtry ignore: Ignoruj submit: Prześlij normal: Normalny closed: Zamknięty deleted: Usunięty deleted_permanently: Deleted permanently pending: Oczekujący more: Więcej view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Wyniki wyszukiwania keywords: Słowa kluczowe options: Opcje follow: Obserwuj following: Obserwuje counts: "Liczba wyników: {{count}}" counts_loading: "... Results" more: Więcej sort_btns: relevance: Relewantność newest: Najnowsze active: Aktywne score: Ocena more: Więcej tips: title: Porady dotyczące wyszukiwania zaawansowanego tag: "<1>[tag] search with a tag" user: "<1>user:username wyszukiwanie według autora" answer: "<1> answers:0 pytania bez odpowiedzi" score: "<1>score:3 posty z oceną 3+" question: "<1>is:question wyszukiwanie pytań" is_answer: "<1>is:answer wyszukiwanie odpowiedzi" empty: Nie mogliśmy niczego znaleźć.
Wypróbuj inne lub mniej konkretne słowa kluczowe. share: name: Udostępnij copy: Skopiuj link via: Udostępnij post za pośrednictwem... copied: Skopiowano facebook: Udostępnij na Facebooku twitter: Share to X cannot_vote_for_self: Nie możesz głosować na własne posty. modal_confirm: title: Błąd... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Twoje nowe konto zostało potwierdzone; zostaniesz przekierowany na stronę główną. link: Kontynuuj do strony głównej oops: Oops! invalid: The link you used no longer works. confirm_new_email: Twój adres e-mail został zaktualizowany. confirm_new_email_invalid: >- Przepraszamy, ten link potwierdzający jest już nieaktualny. Być może twój adres e-mail został już zmieniony? unsubscribe: page_title: Wypisz się success_title: Pomyślne anulowanie subskrypcji success_desc: Zostałeś pomyślnie usunięty z listy subskrybentów i nie będziesz otrzymywać dalszych wiadomości e-mail od nas. link: Zmień ustawienia question: following_tags: Obserwowane tagi edit: Edytuj save: Zapisz follow_tag_tip: Obserwuj tagi, aby dostosować listę pytań. hot_questions: Gorące pytania all_questions: Wszystkie pytania x_questions: "{{ count }} pytań" x_answers: "{{ count }} odpowiedzi" x_posts: "{{ count }} Posts" questions: Pytania answers: Odpowiedzi newest: Najnowsze active: Aktywne hot: Gorące frequent: Frequent recommend: Polecane score: Ocena unanswered: Bez odpowiedzi modified: zmodyfikowane answered: udzielone odpowiedzi asked: zadane closed: zamknięte follow_a_tag: Podążaj za tagiem more: Więcej personal: overview: Przegląd answers: Odpowiedzi answer: odpowiedź questions: Pytania question: pytanie bookmarks: Zakładki reputation: Reputacja comments: Komentarze votes: Głosy badges: Odznaczenia newest: Najnowsze score: Ocena edit_profile: Edytuj Profil visited_x_days: "Odwiedzone przez {{ count }} dni" viewed: Wyświetlone joined: Dołączył comma: "," last_login: Widziano about_me: O mnie about_me_empty: "// Hello, World !" top_answers: Najlepsze odpowiedzi top_questions: Najlepsze pytania stats: Statystyki list_empty: Nie znaleziono wpisów.
Być może chcesz wybrać inną kartę? content_empty: No posts found. accepted: Zaakceptowane answered: Udzielone odpowiedzi asked: zapytano downvoted: oceniono negatywnie mod_short: MOD mod_long: Moderatorzy x_reputation: reputacja x_votes: otrzymane głosy x_answers: odpowiedzi x_questions: pytania recent_badges: Recent Badges install: title: Instalacja next: Dalej done: Zakończono config_yaml_error: Nie można utworzyć pliku config.yaml. lang: label: Wybierz język db_type: label: Silnik bazy danych db_username: label: Nazwa użytkownika placeholder: root msg: Nazwa użytkownika nie może być pusta. db_password: label: Hasło placeholder: turbo-tajne-hasło msg: Hasło nie może być puste. db_host: label: Host bazy danych (ewentualnie dodatkowo port) placeholder: "db.domena:3306" msg: Host bazy danych nie może być pusty. db_name: label: Nazwa bazy danych placeholder: odpowiedź msg: Nazwa bazy danych nie może być pusta. db_file: label: Plik bazy danych placeholder: /data/answer.db msg: Plik bazy danych nie może być pusty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Utwórz plik config.yaml label: Plik config.yaml utworzony. desc: >- Możesz ręcznie utworzyć plik <1>config.yaml w katalogu <1>/var/wwww/xxx/ i wkleić do niego poniższy tekst. info: Gdy już to zrobisz, kliknij przycisk "Dalej". site_information: Informacje o witrynie admin_account: Konto administratora site_name: label: Nazwa witryny msg: Nazwa witryny nie może być pusta. msg_max_length: Nazwa witryny musi mieć maksymalnie 30 znaków. site_url: label: Adres URL text: Adres twojej strony. msg: empty: Adres URL nie może być pusty. incorrect: Niepoprawny format adresu URL. max_length: Adres URL witryny musi mieć maksymalnie 512 znaków. contact_email: label: Email kontaktowy text: Email do osób odpowiedzialnych za tą witrynę. msg: empty: Email kontaktowy nie może być pusty. incorrect: Email do kontaktu ma niepoprawny format. login_required: label: Prywatne switch: Wymagane logowanie text: Dostęp do tej społeczności mają tylko zalogowani użytkownicy. admin_name: label: Imię msg: Imię nie może być puste. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Hasło text: >- Będziesz potrzebować tego hasła do logowania. Przechowuj je w bezpiecznym miejscu. msg: Hasło nie może być puste. msg_min_length: Hasło musi mieć co najmniej 8 znaków. msg_max_length: Hasło musi mieć maksymalnie 32 znaki. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: Będziesz potrzebować tego adresu e-mail do logowania. msg: empty: Adres e-mail nie może być pusty. incorrect: Niepoprawny format adresu e-mail. ready_title: Twoja strona jest gotowa ready_desc: >- Jeśli kiedykolwiek zechcesz zmienić więcej ustawień, odwiedź <1>sekcję administratora; znajdziesz ją w menu strony. good_luck: "Baw się dobrze i powodzenia!" warn_title: Ostrzeżenie warn_desc: >- Plik <1>config.yaml już istnieje. Jeśli chcesz zresetować którekolwiek z elementów konfiguracji w tym pliku, proszę go najpierw usunąć. install_now: Możesz teraz <1>rozpocząć instalację. installed: Już zainstalowane installed_desc: >- Wygląda na to, że masz już zainstalowaną instancję Answer. Aby zainstalować ponownie, proszę najpierw wyczyścić tabele bazy danych. db_failed: Połączenie z bazą danych nie powiodło się db_failed_desc: >- Oznacza to, że informacje o bazie danych w pliku <1>config.yaml są nieprawidłowe lub że nie można nawiązać połączenia z serwerem bazy danych. Może to oznaczać, że serwer bazy danych wskazanego hosta nie działa. counts: views: widoki votes: głosów answers: odpowiedzi accepted: Zaakceptowane page_error: http_error: Błąd HTTP {{ code }} desc_403: Nie masz uprawnień do dostępu do tej strony. desc_404: Niestety, ta strona nie istnieje. desc_50X: Serwer napotkał błąd i nie mógł zrealizować twojego żądania. back_home: Powrót do strony głównej page_maintenance: desc: "Trwa konserwacja, wrócimy wkrótce." nav_menus: dashboard: Panel kontrolny contents: Zawartość questions: Pytania answers: Odpowiedzi users: Użytkownicy badges: Badges flags: Flagi settings: Ustawienia general: Ogólne interface: Interfejs smtp: SMTP branding: Marka legal: Prawne write: Pisanie terms: Terms tos: Warunki korzystania z usługi privacy: Prywatność seo: SEO customize: Dostosowywanie themes: Motywy login: Logowanie privileges: Uprawnienia plugins: Wtyczki installed_plugins: Zainstalowane wtyczki apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Witamy w serwisie {{site_name}} user_center: login: Zaloguj się qrcode_login_tip: Zeskanuj kod QR za pomocą {{ agentName }} i zaloguj się. login_failed_email_tip: Logowanie nie powiodło się, przed ponowną próbą zezwól na dostęp tej aplikacji do informacji o Twojej skrzynce pocztowej. badges: modal: title: Gratulacje content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Administrator dashboard: title: Panel welcome: Witaj Administratorze! site_statistics: Statystyki witryny questions: "Pytania:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Odpowiedzi:" comments: "Komentarze:" votes: "Głosy:" users: "Użytkownicy:" flags: "Flagi:" reviews: "Reviews:" site_health: Site health version: "Wersja:" https: "HTTPS:" upload_folder: "Prześlij folder:" run_mode: "Tryb pracy:" private: Prywatne public: Publiczne smtp: "SMTP:" timezone: "Strefa czasowa:" system_info: Informacje o systemie go_version: "Wersja Go:" database: "Baza danych:" database_size: "Wielkość bazy danych:" storage_used: "Wykorzystane miejsce:" uptime: "Czas pracy:" links: Linki plugins: Wtyczki github: GitHub blog: Blog contact: Kontakt forum: Forum documents: Dokumenty feedback: Opinie support: Wsparcie review: Przegląd config: Konfiguracja update_to: Zaktualizuj do latest: Najnowszej check_failed: Sprawdzanie nie powiodło się "yes": "Tak" "no": "Nie" not_allowed: Nie dozwolone allowed: Dozwolone enabled: Włączone disabled: Wyłączone writable: Zapis i odczyt not_writable: Nie można zapisać flags: title: Flagi pending: Oczekujące completed: Zakończone flagged: Oznaczone flagged_type: Oznaczone {{ type }} created: Utworzone action: Akcja review: Przegląd user_role_modal: title: Zmień rolę użytkownika na... btn_cancel: Anuluj btn_submit: Wyślij new_password_modal: title: Ustaw nowe hasło form: fields: password: label: Hasło text: Użytkownik zostanie wylogowany i musi zalogować się ponownie. msg: Hasło musi mieć od 8 do 32 znaków. btn_cancel: Anuluj btn_submit: Prześlij edit_profile_modal: title: Edytuj profil form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Nazwa msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Błędny adresy email. edit_success: Edycja zakończona pomyślnie btn_cancel: Anuluj btn_submit: Prześlij user_modal: title: Dodaj nowego użytkownika form: fields: users: label: Masowo dodaj użytkownika placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Oddziel "nazwa, e-mail, hasło" przecinkami. Jeden użytkownik na linię. msg: "Podaj adresy e-mail użytkowników, jeden w każdej linii." display_name: label: Nazwa wyświetlana msg: Display name must be 2-30 characters in length. email: label: E-mail msg: Email nie jest prawidłowy. password: label: Hasło msg: Hasło musi mieć od 8 do 32 znaków. btn_cancel: Anuluj btn_submit: Prześlij users: title: Użytkownicy name: Imię email: E-mail reputation: Reputacja created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Status role: Rola action: Akcja change: Zmień all: Wszyscy staff: Personel more: Więcej inactive: Nieaktywni suspended: Zawieszeni deleted: Usunięci normal: Normalni Moderator: Moderator Admin: Administrator User: Użytkownik filter: placeholder: "Filtruj według imienia, użytkownik:id" set_new_password: Ustaw nowe hasło edit_profile: Edytuj profil change_status: Zmień status change_role: Zmień rolę show_logs: Pokaż logi add_user: Dodaj użytkownika deactivate_user: title: Dezaktywuj użytkownika content: Nieaktywny użytkownik musi ponownie potwierdzić swój adres email. delete_user: title: Usuń tego użytkownika content: Czy na pewno chcesz usunąć tego użytkownika? Ta operacja jest nieodwracalna! remove: Usuń zawartość label: Usuń wszystkie pytania, odpowiedzi, komentarze itp. text: Nie zaznaczaj tego, jeśli chcesz usunąć tylko konto użytkownika. suspend_user: title: Zawieś tego użytkownika content: Zawieszony użytkownik nie może się logować. label: How long will the user be suspended for? forever: Forever questions: page_title: Pytania unlisted: Unlisted post: Wpis votes: Głosy answers: Odpowiedzi created: Utworzone status: Status action: Akcja change: Zmień pending: Oczekuje filter: placeholder: "Filtruj według tytułu, pytanie:id" answers: page_title: Odpowiedzi post: Wpis votes: Głosy created: Utworzone status: Status action: Akcja change: Zmień filter: placeholder: "Filtruj według tytułu, odpowiedź:id" general: page_title: Ogólne name: label: Nazwa witryny msg: Nazwa strony nie może być pusta. text: "Nazwa tej strony, używana w tagu tytułu." site_url: label: URL strony msg: Adres Url witryny nie może być pusty. validate: Podaj poprawny adres URL. text: Adres Twojej strony. short_desc: label: Krótki opis witryny msg: Krótki opis strony nie może być pusty. text: "Krótki opis, używany w tagu tytułu na stronie głównej." desc: label: Opis witryny msg: Opis strony nie może być pusty. text: "Opisz tę witrynę w jednym zdaniu, użytym w znaczniku meta description." contact_email: label: Email kontaktowy msg: Email kontaktowy nie może być pusty. validate: Email kontaktowy nie jest poprawny. text: Adres email głównego kontaktu odpowiedzialnego za tę stronę. check_update: label: Aktualizacjia oprogramowania text: Automatycznie sprawdzaj dostępność aktualizacji interface: page_title: Interfejs language: label: Język Interfejsu msg: Język interfejsu nie może być pusty. text: Język interfejsu użytkownika. Zmieni się po odświeżeniu strony. time_zone: label: Strefa czasowa msg: Strefa czasowa nie może być pusta. text: Wybierz miasto w tej samej strefie czasowej, co Ty. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: Email nadawcy msg: Email nadawcy nie może być pusty. text: Adres email, z którego są wysyłane wiadomości. from_name: label: Nazwa w polu Od msg: Nazwa nadawcy nie może być pusta. text: Nazwa, z której są wysyłane wiadomości. smtp_host: label: Serwer SMTP msg: Host SMTP nie może być pusty. text: Twój serwer poczty. encryption: label: Szyfrowanie msg: Szyfrowanie nie może być puste. text: Dla większości serwerów zalecana jest opcja SSL. ssl: SSL tls: TLS none: Brak smtp_port: label: Port SMTP msg: Port SMTP musi być liczbą od 1 do 65535. text: Port Twojego serwera poczty. smtp_username: label: Nazwa użytkownika SMTP msg: Nazwa użytkownika SMTP nie może być pusta. smtp_password: label: Hasło SMTP msg: Hasło SMTP nie może być puste. test_email_recipient: label: Odbiorcy testowych wiadomości email text: Podaj adres email, który otrzyma testowe wiadomości. msg: Odbiorcy testowych wiadomości email są nieprawidłowi smtp_authentication: label: Wymagaj uwierzytelniania title: Uwierzytelnianie SMTP msg: Uwierzytelnianie SMTP nie może być puste. "yes": "Tak" "no": "Nie" branding: page_title: Marka logo: label: Logo msg: Logo nie może być puste. text: Obrazek logo znajdujący się na górze lewej strony Twojej strony. Użyj szerokiego prostokątnego obrazka o wysokości 56 pikseli i proporcjach większych niż 3:1. Jeśli zostanie puste, wyświetlony zostanie tekst tytułu strony. mobile_logo: label: Logo mobilne text: Logo używane w wersji mobilnej Twojej strony. Użyj szerokiego prostokątnego obrazka o wysokości 56 pikseli. Jeśli zostanie puste, użyty zostanie obrazek z ustawienia "logo". square_icon: label: Kwadratowa ikona msg: Kwadratowa ikona nie może być pusta. text: Obrazek używany jako podstawa dla ikon metadanych. Powinien mieć idealnie większe wymiary niż 512x512 pikseli. favicon: label: Ikona (favicon) text: Ulubiona ikona witryny. Aby działać poprawnie przez CDN, musi to być png. Rozmiar zostanie zmieniony na 32x32. Jeśli pozostanie puste, zostanie użyta "kwadratowa ikona". legal: page_title: Prawne terms_of_service: label: Warunki korzystania z usługi text: "Możesz tutaj dodać treść regulaminu. Jeśli masz dokument hostowany gdzie indziej, podaj tutaj pełny URL." privacy_policy: label: Polityka prywatności text: "Możesz tutaj dodać treść polityki prywatności. Jeśli masz dokument hostowany gdzie indziej, podaj tutaj pełny URL." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Każdy użytkownik może napisać tylko jedną odpowiedź na każde pytanie text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Rekomendowane tagi text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Każde nowe pytanie musi mieć przynajmniej jeden rekomendowany tag." reserved_tags: label: Zarezerwowane tagi text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Link bezpośredni text: Dostosowane struktury URL mogą poprawić użyteczność i kompatybilność w przyszłości Twoich linków. robots: label: robots.txt text: To trwale zastąpi wszelkie związane z witryną ustawienia. themes: page_title: Motywy themes: label: Motywy text: Wybierz istniejący motyw. color_scheme: label: Schemat kolorów navbar_style: label: Navbar background style primary_color: label: Kolor podstawowy text: Zmodyfikuj kolory używane przez Twoje motywy. layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS i HTML custom_css: label: Własny CSS text: > head: label: Głowa text: > header: label: Nagłówek text: > footer: label: Stopka text: Zostanie wstawione przed </body>. sidebar: label: Pasek boczny text: Będzie wstawiony w pasku bocznym. login: page_title: Logowanie membership: title: Członkostwo label: Zezwalaj na nowe rejestracje text: Wyłącz, aby uniemożliwić komukolwiek tworzenie nowego konta. email_registration: title: Rejestracja przez email label: Zezwalaj na rejestrację przez email text: Wyłącz, aby uniemożliwić tworzenie nowego konta poprzez email. allowed_email_domains: title: Dozwolone domeny email text: Domeny email, z których użytkownicy muszą rejestrować konta. Jeden domena na linię. Ignorowane, gdy puste. private: title: Prywatne label: Wymagane logowanie text: Dostęp do tej społeczności mają tylko zalogowani użytkownicy. password_login: title: Hasło logowania label: Zezwalaj na logowanie email i hasłem text: "OSTRZEŻENIE: Jeśli wyłączone, już się nie zalogujesz, jeśli wcześniej nie skonfigurowałeś innej metody logowania." installed_plugins: title: Zainstalowane wtyczki plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: Wszystkie active: Aktywne inactive: Nieaktywne outdated: Przestarzałe plugins: label: Wtyczki text: Wybierz istniejącą wtyczkę. name: Nazwa version: Wersja status: Status action: Akcja deactivate: Deaktywuj activate: Aktywuj settings: Ustawienia settings_users: title: Użytkownicy avatar: label: Domyślny Awatar text: Dla użytkowników bez własnego awatara. gravatar_base_url: label: Adres URL bazy Gravatar text: Adres URL bazy API dostawcy Gravatar. Ignorowane, gdy puste. profile_editable: title: Edycja profilu allow_update_display_name: label: Zezwalaj użytkownikom na zmianę wyświetlanej nazwy allow_update_username: label: Zezwalaj użytkownikom na zmianę nazwy użytkownika allow_update_avatar: label: Zezwalaj użytkownikom na zmianę obrazka profilowego allow_update_bio: label: Zezwalaj użytkownikom na zmianę opisu allow_update_website: label: Zezwalaj użytkownikom na zmianę witryny allow_update_location: label: Zezwalaj użytkownikom na zmianę lokalizacji privilege: title: Uprawnienia level: label: Wymagany poziom reputacji text: Wybierz reputację wymaganą dla uprawnień msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Grupa inactive: Inactive name: Name show_logs: Wyświetl dzienniki status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (opcjonalne) empty: nie może być puste invalid: jest nieprawidłowe btn_submit: Zapisz not_found_props: "Nie znaleziono wymaganej właściwości {{ key }}." select: Wybierz page_review: review: Przegląd proposed: zaproponowany question_edit: Edycja pytania answer_edit: Edycja odpowiedzi tag_edit: Edycja tagu edit_summary: Podsumowanie edycji edit_question: Edytuj pytanie edit_answer: Edytuj odpowiedź edit_tag: Edytuj tag empty: Nie ma więcej zadań do przeglądu. approve_revision_tip: Czy akceptujesz tę wersję? approve_flag_tip: Czy akceptujesz tę flagę? approve_post_tip: Czy zatwierdzasz ten post? approve_user_tip: Czy zatwierdzasz tego użytkownika? suggest_edits: Suggested edits flag_post: Oznacz wpis flag_user: Oznacz użytkownika queued_post: Queued post queued_user: Queued user filter_label: Typ reputation: reputacja flag_post_type: Oznaczono ten wpis jako {{ type }}. flag_user_type: Oznaczono tego użytkownika jako {{ type }}. edit_post: Edytuj wpis list_post: List post unlist_post: Unlist post timeline: undeleted: nieusunięte deleted: usunięty downvote: odrzucenie upvote: głos za accept: akceptacja cancelled: anulowane commented: skomentowano rollback: wycofaj edited: edytowany answered: odpowiedziano asked: zapytano closed: zamknięty reopened: ponownie otwarty created: utworzony pin: przypięty unpin: odpięty show: wymieniony hide: niewidoczne title: "Historia dla" tag_title: "Historia dla" show_votes: "Pokaż głosy" n_or_a: Nie dotyczy title_for_question: "Historia dla" title_for_answer: "Oś czasu dla odpowiedzi na {{ tytuł }} autorstwa {{ autor }}" title_for_tag: "Oś czasu dla tagu" datetime: Data i czas type: Typ by: Przez comment: Komentarz no_data: "Nie mogliśmy nic znaleźć." users: title: Użytkownicy users_with_the_most_reputation: Użytkownicy o najwyższej reputacji w tym tygodniu users_with_the_most_vote: Użytkownicy, którzy oddali najwięcej głosów w tym tygodniu staffs: Nasz personel społeczności reputation: reputacja votes: głosy prompt: leave_page: Czy na pewno chcesz opuścić stronę? changes_not_save: Twoje zmiany mogą nie zostać zapisane. draft: discard_confirm: Czy na pewno chcesz odrzucić swoją wersję roboczą? messages: post_deleted: Ten post został usunięty. post_cancel_deleted: This post has been undeleted. post_pin: Ten post został przypięty. post_unpin: Ten post został odpięty. post_hide_list: Ten post został ukryty na liście. post_show_list: Ten post został wyświetlony na liście. post_reopen: Ten post został ponownie otwarty. post_list: Ten wpis został umieszczony na liście. post_unlist: Ten wpis został usunięty z listy. post_pending: Twój wpis oczekuje na recenzje. Będzie widoczny po jej akceptacji. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/pt_BR.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Sucesso. unknown: other: Erro desconhecido. request_format_error: other: O formato da Requisição não é válido. unauthorized_error: other: Não autorizado. database_error: other: Erro no servidor de dados. role: name: user: other: Usuário admin: other: Administrador moderator: other: Moderador description: user: other: Padrão sem acesso especial. admin: other: Possui controle total para acessar o site. moderator: other: Não possui controle a todas as postagens exceto configurações de administrador. email: other: E-mail password: other: Senha email_or_password_wrong_error: other: E-mail e Senha inválidos. error: admin: email_or_password_wrong: other: E-mail e Senha inválidos. answer: not_found: other: Resposta não encontrada. cannot_deleted: other: Permissão insuficiente para deletar. cannot_update: other: Permissão insuficiente para atualizar. comment: edit_without_permission: other: Não é permitido atualizar comentários. not_found: other: Comentário não encontrado. cannot_edit_after_deadline: other: O tempo para comentários expirou. email: duplicate: other: E-mail já existe na base de dados. need_to_be_verified: other: E-mail precisa ser verificado. verify_url_expired: other: A URL de validação do e-mail expirou, por favor, re-envie o e-mail. lang: not_found: other: Arquivo de idioma não encontrado. object: captcha_verification_failed: other: Captcha inválido. disallow_follow: other: Você não possui permissão para seguir. disallow_vote: other: Você não possui permissão para votar. disallow_vote_your_self: other: Você não pode votar na sua própria postagem. not_found: other: Objeto não encontrado. verification_failed: other: Falha na verificação. email_or_password_incorrect: other: E-mail e Senha não conferem. old_password_verification_failed: other: Verficação de senhas antigas falhou. new_password_same_as_previous_setting: other: A nova senha é idêntica à senha anterior. question: not_found: other: Pergunta não encontrada. cannot_deleted: other: Não possui permissão para deletar. cannot_close: other: Não possui permissão para fechar. cannot_update: other: Não possui permissão para atualizar.. rank: fail_to_meet_the_condition: other: A classificação não atende à condição. report: handle_failed: other: Falha no processamento do relatório. not_found: other: Relatório não encontrado. tag: not_found: other: Marcador não encontada. recommend_tag_not_found: other: Marcador recomendado não existe. recommend_tag_enter: other: Por favor, insira ao menos um marcador obrigatório. not_contain_synonym_tags: other: Não pode contar marcadores de sinônimos. cannot_update: other: Não possui permissão para atualizar. cannot_set_synonym_as_itself: other: Você não pode definir o sinônimo do marcador atual como ela mesma. smtp: config_from_name_cannot_be_email: other: O De Nome não pode ser um endereço de e-mail. Tema: not_found: other: Tema não encontrado. revision: review_underway: other: Não é possível editar no momento, há uma versão na fila de revisão. no_permission: other: Sem pemissão para revisar. user: email_or_password_wrong: other: other: E-mail e senha não correspondem. not_found: other: Usuário não encontrado. suspended: other: Usuário foi suspenso. username_invalid: other: Nome de usuário inválido. username_duplicate: other: Nome de usuário já está em uso. set_avatar: other: Falha ao configurar Avatar. cannot_update_your_role: other: Você não pode modificar a sua função. not_allowed_registration: other: Atualmente o site não está aberto para cadastro config: read_config_failed: other: A leitura da configuração falhou database: connection_failed: other: Falha na conexão com o banco de dados create_table_failed: other: Falha na criação de tabela install: create_config_failed: other: Não foi possível criar o arquivo config.yaml. upload: unsupported_file_format: other: Formato de arquivo não suportado. report: spam: name: other: spam desc: other: Este post é um anúncio, ou vandalismo. Não é útil ou relevante para o tópico atual. rude: name: other: rude ou abusivo desc: other: Uma pessoa razoável acharia este conteúdo impróprio para um discurso respeitoso. duplicate: name: other: uma duplicidade desc: other: Esta pergunta já foi feita antes e já tem uma resposta. not_answer: name: other: não é uma resposta desc: other: Isso foi postado como uma resposta, mas não tenta responder à pergunta. Possivelmente deve ser uma edição, um comentário, outra pergunta ou excluído completamente. not_need: name: other: não é mais necessário desc: other: Este comentário está desatualizado, coloquial ou não é relevante para esta postagem. other: name: other: algo mais desc: other: Esta postagem requer atenção da equipe por outro motivo não listado acima. question: close: duplicate: name: other: spam desc: other: Esta pergunta já foi feita antes e já tem uma resposta. guideline: name: other: um motivo específico da comunidade desc: other: Esta pergunta não atende a uma diretriz da comunidade. multiple: name: other: precisa de detalhes ou clareza desc: other: Esta pergunta atualmente inclui várias perguntas em uma. Deve se concentrar em apenas um problema. other: name: other: algo mais desc: other: Este post requer outro motivo não listado acima. operation_type: asked: other: perguntado answered: other: respondido modified: other: modificado notification: action: update_question: other: pergunta atualizada answer_the_question: other: pergunta respondida update_answer: other: resposta atualizada accept_answer: other: resposta aceita comment_question: other: pergunta comentada comment_answer: other: resposta comentada reply_to_you: other: respondeu a você mention_you: other: mencionou você your_question_is_closed: other: A sua pergunta foi fechada your_question_was_deleted: other: A sua pergunta foi deletada your_answer_was_deleted: other: A sua resposta foi deletada your_comment_was_deleted: other: O seu comentário foi deletado #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Como formatar desc: >-
  • para mais links

    <https://url.com>

    [Título](https://url.com)
  • colocar retornos entre parágrafos

  • _italic_ ou **bold**

  • identar código por 4 espaços

  • cotação colocando > no início da linha

  • aspas invertidas `tipo_isso`

  • criar cercas de código com aspas invertidas `

    ```
    código aqui
    ```
pagination: prev: Anterior next: Próximo page_title: question: Pergunta questions: Perguntas tag: Marcador tags: Marcadores tag_wiki: marcador wiki edit_tag: Editar Marcador ask_a_question: Adicionar Pergunta edit_question: Editar Pergunta edit_answer: Editar Resposta search: Busca posts_containing: Postagens contendo settings: Configurações notifications: Notificações login: Entrar sign_up: Criar conta account_recovery: Recuperação de conta account_activation: Ativação de conta confirm_email: Confirmação de e-mail account_suspended: Conta suspensa admin: Administrador change_email: Modificar e-mail install: Instalação do Resposta upgrade: Atualização do Resposta maintenance: Manutenção do Website users: Usuários notifications: title: Notificações inbox: Caixa de entrada achievement: Conquistas all_read: Marcar todas como lidas show_more: Mostrar mais suspended: title: A sua conta foi suspensa until_time: "A sua conta foi suspensa até {{ time }}." forever: Este usuário foi suspenso por tempo indeterminado. end: Você não atende a uma diretriz da comunidade. editor: blockquote: text: Bloco de citação bold: text: Forte chart: text: Gráfico flow_chart: Fluxograma sequence_diagram: Diagrama de sequência class_diagram: Diagrama de classe state_diagram: Diagrama de estado entity_relationship_diagram: Diagrama de relacionamento de entidade user_defined_diagram: Diagrama definido pelo usuário gantt_chart: Diagrama de Gantt pie_chart: Gráfico de pizza code: text: Código de exemplo add_code: Adicione código de exemplo form: fields: code: label: Código msg: empty: Código não pode ser vazio. language: label: Idioma (opcional) placeholder: Detecção automática btn_cancel: Cancelar btn_confirm: Adicionar formula: text: Fórmula options: inline: Fórmula em linha block: Fórmula em bloco Cabeçalho: text: Cabeçalho options: h1: Cabeçalho 1 h2: Cabeçalho 2 h3: Cabeçalho 3 h4: Cabeçalho 4 h5: Cabeçalho 5 h6: Cabeçalho 6 help: text: Ajuda hr: text: Régua horizontal image: text: Imagem add_image: Adicionar imagem tab_image: Enviar imagem form_image: fields: file: label: Arquivo de imagem btn: Selecione imagem msg: empty: Arquivo não pode ser vazio. only_image: Somente um arquivo de imagem é permitido. max_size: O tamanho do arquivo não pode exceder 4 MB. desc: label: Descrição (opcional) tab_url: URL da imagem form_url: fields: url: label: URL da imagem msg: empty: URL da imagem não pode ser vazia. name: label: Descrição (opcional) btn_cancel: Cancelar btn_confirm: Adicionar uploading: Enviando indent: text: Identação outdent: text: Não identado italic: text: Ênfase link: text: Superlink (Hyperlink) add_link: Adicionar superlink (hyperlink) form: fields: url: label: URL msg: empty: URL não pode ser vazia. name: label: Descrição (opcional) btn_cancel: Cancelar btn_confirm: Adicionar ordered_list: text: Lista numerada unordered_list: text: Lista com marcadores table: text: Tabela Cabeçalho: Cabeçalho cell: Célula close_modal: title: Estou fechando este post como... btn_cancel: Cancelar btn_submit: Enviar remark: empty: Não pode ser vazio. msg: empty: Por favor selecione um motivo. report_modal: flag_title: I am flagging to report this post as... close_title: Estou fechando este post como... review_question_title: Revisar pergunta review_answer_title: Revisar resposta review_comment_title: Revisar comentário btn_cancel: Cancelar btn_submit: Enviar remark: empty: Não pode ser vazio. msg: empty: Por favor selecione um motivo. tag_modal: title: Criar novo marcador form: fields: display_name: label: Nome de exibição msg: empty: Nome de exibição não pode ser vazio. range: Nome de exibição tem que ter até 35 caracteres. slug_name: label: Slug de URL desc: 'Deve usar o conjunto de caracteres "a-z", "0-9", "+ # - ."' msg: empty: URL slug não pode ser vazio. range: URL slug até 35 caracteres. character: URL slug contém conjunto de caracteres não permitido. desc: label: Descrição (opcional) btn_cancel: Cancelar btn_submit: Enviar tag_info: created_at: Criado edited_at: Editado history: Histórico synonyms: title: Sinônimos text: Os marcadores a seguir serão re-mapeados para empty: Sinônimos não encotrados. btn_add: Adicionar um sinônimo btn_edit: Editar btn_save: Salvar synonyms_text: Os marcadores a seguir serão re-mapeados para delete: title: Deletar este marcador content: >-

Não permitimos a exclusão de marcadores com postagens.

Por favor, remova este marcador das postagens primeiro.

content2: Você tem certeza que deseja deletar? close: Fechar edit_tag: title: Editar marcador default_reason: Editar marcador form: fields: revision: label: Revisão display_name: label: Nome de exibição slug_name: label: Slug de URL info: 'Deve usar o conjunto de caracteres "a-z", "0-9", "+ # - ."' desc: label: Descrição edit_summary: label: Resumo da edição placeholder: >- Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) btn_save_edits: Salvar edições btn_cancel: Cancelar dates: long_date: D MMM long_date_with_year: "D MMM, YYYY" long_date_with_time: "D MMM, YYYY [at] HH:mm" now: agora x_seconds_ago: "{{count}}s atrás" x_minutes_ago: "{{count}}m atrás" x_hours_ago: "{{count}}h atrás" hour: hora day: dia comment: btn_add_comment: Adicionar comentário reply_to: Responder a btn_reply: Responder btn_edit: Editar btn_delete: Deletar btn_flag: Marcador btn_save_edits: Salvar edições btn_cancel: Cancelar show_more: Mostrar mais comentários tip_question: >- Use os comentários para pedir mais informações ou sugerir melhorias. Evite responder perguntas nos comentários. tip_answer: >- Use comentários para responder a outros usuários ou notificá-los sobre alterações. Se você estiver adicionando novas informações, edite sua postagem em vez de comentar. edit_answer: title: Editar Resposta default_reason: Editar Resposta form: fields: revision: label: Revisão answer: label: Resposta feedback: caracteres: conteúdo deve ser pelo menos 6 caracteres em comprimento. edit_summary: label: Resumo da edição placeholder: >- Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) btn_save_edits: Salvar edições btn_cancel: Cancelar tags: title: Marcadores sort_buttons: popular: Popular name: Nome newest: mais recente button_follow: Seguir button_following: Seguindo tag_label: perguntas search_placeholder: Filtrar por nome de marcador no_desc: O marcador não possui descrição. more: Mais ask: title: Adicionar Pergunta edit_title: Editar Pergunta default_reason: Editar pergunta similar_questions: Perguntas similares form: fields: revision: label: Revisão title: label: Título placeholder: Seja específico e tenha em mente que está fazendo uma pergunta a outra pessoa msg: empty: Título não pode ser vazio. range: Título até 150 caracteres body: label: Corpo msg: empty: Corpo da mensagem não pode ser vazio. tags: label: Marcadores msg: empty: Marcadores não podes ser vazios. answer: label: Resposta msg: empty: Resposta não pode ser vazia. edit_summary: label: Resumo da edição placeholder: >- Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) btn_post_question: Publicar a sua pergunta btn_save_edits: Salvar edições answer_question: Responda a sua própria pergunta post_question&answer: Publicar a sua pergunta e resposta tag_selector: add_btn: Adicionar marcador create_btn: Criar novo marcador search_tag: Procurar marcador hint: "Descreva do quê se trata a sua pergunta, ao menos um marcador é requirido." no_result: Nenhum marcador correspondente tag_required_text: Marcador obrigatório (ao menos um) header: nav: question: Perguntas tag: Marcadores user: Usuários profile: Perfil setting: Configurações logout: Sair admin: Administrador review: Revisar search: placeholder: Procurar footer: build_on: >- Desenvolvido com base no <1> Answer — o software de código aberto que alimenta comunidades de perguntas e respostas.
Feito com amor © {{cc}}. upload_img: name: Mudar loading: carregando... pic_auth_code: title: Captcha placeholder: Escreva o texto acima msg: empty: Captcha não pode ser vazio. inactive: first: >- Você está quase pronto! Enviamos um e-mail de ativação para {{mail}}. Por favor, siga as instruções no e-mail para ativar uma conta sua. info: "Se não chegar, verifique sua pasta de spam." another: >- Enviamos outro e-mail de ativação para você em {{mail}}. Pode levar alguns minutos para chegar; certifique-se de verificar sua pasta de spam. btn_name: Reenviar e-mail de ativação change_btn_name: Mudar email msg: empty: Não pode ser vazio. login: page_title: Bem vindo ao {{site_name}} login_to_continue: Entre para continuar info_sign: Não possui uma conta? <1>Cadastrar-se info_login: Já possui uma conta? <1>Entre agreements: Ao se registrar, você concorda com as <1>políticas de privacidades e os <3>termos de serviços. forgot_pass: Esqueceu a sua senha? name: label: Nome msg: empty: Nome não pode ser vazio. range: Nome até 30 caracteres. email: label: E-mail msg: empty: E-mail não pode ser vazio. password: label: Senha msg: empty: Senha não pode ser vazia. different: As senhas inseridas em ambos os campos são inconsistentes account_forgot: page_title: Esqueceu a sua senha btn_name: Enviar e-mail de recuperação de senha send_success: >- Se uma conta corresponder {{mail}}, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve. email: label: E-mail msg: empty: E-mail não pode ser vazio. change_email: page_title: Bem vindo ao {{site_name}} btn_cancel: Cancelar btn_update: Atualiza email address send_success: >- Se uma conta corresponder {{mail}}, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve. email: label: Novo E-mail msg: empty: E-mail não pode ser vazio. password_reset: page_title: Redefinir senha btn_name: Redefinir minha senha reset_success: >- Você alterou com sucesso uma senha sua; você será redirecionado para a página de login. link_invalid: >- Desculpe, este link de redefinição de senha não é mais válido. Talvez uma senha sua já tenha sido redefinida? to_login: Continuar para a tela de login password: label: Senha msg: empty: Senha não pode ser vazio. length: O comprimento deve estar entre 8 e 32 different: As senhas inseridas em ambos os campos são inconsistentes password_confirm: label: Confirmar Nova senha settings: page_title: Configurações nav: profile: Perfil notification: Notificações account: Conta interface: Interface profile: Cabeçalho: Perfil btn_name: Salvar display_name: label: Nome de exibição msg: Nome de exibição não pode ser vazio. msg_range: Nome de exibição tem que ter até 30 caracteres. username: label: Nome de usuário caption: As pessoas poderão mensionar você com "@usuário". msg: Nome de usuário não pode ser vazio. msg_range: Nome de usuário até 30 caracteres. character: 'Deve usar o conjunto de caracteres "a-z", "0-9", "- . _"' avatar: label: Perfil Imagem gravatar: Gravatar gravatar_text: Você pode mudar a imagem em <1>gravatar.com custom: Customizado btn_refresh: Atualizar custom_text: Você pode enviar a sua image. default: System msg: Por favor envie um avatar bio: label: Sobre mim (opcional) website: label: Website (opcional) placeholder: "https://exemplo.com.br" msg: Formato incorreto de endereço de Website location: label: Localização (opcional) placeholder: "Cidade, País" notification: Cabeçalho: Notificações email: label: E-mail Notificações radio: "Responda as suas perguntas, comentários, e mais" account: Cabeçalho: Conta change_email_btn: Mudar e-mail change_pass_btn: Mudar senha change_email_info: >- Enviamos um e-mail para esse endereço. Siga as instruções de confirmação. email: label: E-mail msg: E-mail não pode ser vazio. password_title: Senha current_pass: label: Senha atual msg: empty: A Senha não pode ser vazia. length: O comprimento deve estar entre 8 and 32. different: As duas senhas inseridas não correspondem. new_pass: label: Nova Senha pass_confirm: label: Confirmar nova Senha interface: Cabeçalho: Interface lang: label: Idioma da Interface text: Idioma da interface do usuário. A interface mudará quando você atualizar a página. toast: update: atualização realizada com sucesso update_password: Senha alterada com sucesso. flag_success: Obrigado por marcar. forbidden_operate_self: Proibido para operar por você mesmo review: A sua resposta irá aparecer após a revisão. related_question: title: Perguntas relacionadas btn: Adicionar pegunta answers: respostas question_detail: Perguntado: Perguntado asked: perguntado update: Modificado edit: modificado Views: Visualizado Seguir: Seguir Seguindo: Seguindo answered: respondido closed_in: Fechado em show_exist: Mostrar pergunta existente. answers: title: Respostas score: Pontuação newest: Mais recente btn_accept: Aceito btn_accepted: Aceito write_answer: title: A sua Resposta btn_name: Publicação a sua resposta add_another_answer: Adicionar outra resposta confirm_title: Continuar a responder continue: Continuar confirm_info: >-

Tem certeza de que deseja adicionar outra resposta?

Você pode usar o link de edição para refinar e melhorar uma resposta existente.

empty: Resposta não pode ser vazio. caracteres: conteúdo deve ser pelo menos 6 caracteres em comprimento. reopen: title: Reabrir esta postagem content: Tem certeza que deseja reabrir? success: Esta postagem foi reaberta delete: title: Excluir esta postagem question: >- Nós não recomendamos excluir perguntas com respostas porque isso priva os futuros leitores desse conhecimento.

A exclusão repetida de perguntas respondidas pode resultar no bloqueio de perguntas de sua conta. Você tem certeza que deseja excluir? answer_accepted: >-

Não recomendamos excluir resposta aceita porque isso priva os futuros leitores desse conhecimento.

A exclusão repetida de respostas aceitas pode resultar no bloqueio de respostas de uma conta sua. Você tem certeza que deseja excluir? other: Você tem certeza que deseja deletar? tip_question_deleted: Esta postagem foi deletada tip_answer_deleted: Esta resposta foi deletada btns: confirm: Confirmar cancel: Cancelar save: Salvar delete: Deletar login: Entrar signup: Cadastrar-se logout: Sair verify: Verificar add_question: Adicionar pergunta approve: Aprovar reject: Rejetar skip: Pular search: title: Procurar Resultados keywords: Palavras-chave options: Opções follow: Seguir following: Seguindo counts: "{{count}} Resultados" more: Mais sort_btns: relevance: Relevância newest: Mais recente active: Ativar score: Pontuação more: Mais tips: title: Dicas de Pesquisa Avançada tag: "<1>[tag] pesquisar dentro de um marcador" user: "<1>user:username buscar por autor" answer: "<1>answers:0 perguntas não respondidas" score: "<1>score:3 postagens com mais de 3+ placares" question: "<1>is:question buscar perguntas" is_answer: "<1>is:answer buscar respostas" empty: Não conseguimos encontrar nada.
Tente palavras-chave diferentes ou menos específicas. share: name: Compartilhar copy: Copiar link via: Compartilhar postagem via... copied: Copiado facebook: Compartilhar no Facebook twitter: Compartilhar no X cannot_vote_for_self: Você não pode votar na sua própria postagem modal_confirm: title: Erro... account_result: page_title: Bem vindo ao {{site_name}} success: A sua nova conta está confirmada; você será redirecionado para a página inicial. link: Continuar para a página inicial. invalid: >- Desculpe, este link de confirmação não é mais válido. Talvez a sua já está ativa. confirm_new_email: Seu e-mail foi atualizado. confirm_new_email_invalid: >- Desculpe, este link de confirmação não é mais válido. Talvez o seu e-mail já tenha sido alterado. unsubscribe: page_title: Cancelar subscrição success_title: Cancelamento de inscrição bem-sucedido success_desc: Você foi removido com sucesso desta lista de assinantes e não receberá mais nenhum e-mail nosso. link: Mudar configurações question: following_tags: Seguindo Marcadores edit: Editar save: Salvar follow_tag_tip: Siga as tags para selecionar sua lista de perguntas. hot_questions: Perguntas quentes all_questions: Todas Perguntas x_questions: "{{ count }} perguntas" x_answers: "{{ count }} respostas" questions: Perguntas answers: Respostas newest: Mais recente active: Ativo hot: Hot score: Pontuação unanswered: Não Respondido modified: modificado answered: respondido asked: perguntado closed: fechado follow_a_tag: Seguir o marcador more: Mais personal: overview: Visão geral answers: Respostas answer: resposta questions: Perguntas question: pergunta bookmarks: Favoritas reputation: Reputação comments: Comentários Votos: Votos newest: Mais recente score: Pontuação edit_profile: Editar Perfil visited_x_days: "Visitado {{ count }} dias" viewed: Visualizado joined: Ingressou last_login: Visto about_me: Sobre mim about_me_empty: "// Olá, Mundo !" top_answers: Melhores Respostas top_questions: Melhores Perguntas stats: Estatísticas list_empty: Postagens não encontradas.
Talvez você queira selecionar uma guia diferente? accepted: Aceito answered: respondido asked: perguntado upvote: voto positivo downvote: voto negativo mod_short: Moderador mod_long: Moderadores x_reputation: reputação x_Votos: votos recebidos x_answers: respostas x_questions: perguntas install: title: Instalação next: Próximo done: Completo config_yaml_error: Não é possível criar o arquivo config.yaml. lang: label: Por favor Escolha um Idioma db_type: label: Mecanismo de banco de dados db_username: label: Nome de usuário placeholder: root msg: Nome de usuário não pode ser vazio. db_password: label: Senha placeholder: root msg: Senha não pode ser vazio. db_host: label: Host do banco de dados placeholder: "db:3306" msg: Host de banco de dados não pode ficar vazio. db_name: label: Nome do banco de dados placeholder: answer msg: O nome do banco de dados não pode ficar vazio. db_file: label: Arquivo de banco de dados placeholder: /data/answer.db msg: O arquivo de banco de dados não pode ficar vazio. config_yaml: title: Criar config.yaml label: O arquivo config.yaml foi criado. desc: >- Você pode criar o arquivo <1>config.yaml manualmente no diretório <1>/var/www/xxx/ e colar o seguinte texto nele. info: Depois de fazer isso, clique no botão "Avançar". site_information: Informações do site admin_account: Administrador Conta site_name: label: Site Nome msg: Site Nome não pode ser vazio. site_url: label: Site URL text: O endereço do seu site. msg: empty: Site URL não pode ser vazio. incorrect: Formato incorreto da URL do site. contact_email: label: E-mail para contato text: Endereço de e-mail do contato principal responsável por este site. msg: empty: E-mail para contato não pode ser vazio. incorrect: E-mail para contato em formato incorreto. admin_name: label: Nome msg: Nome não pode ser vazio. admin_password: label: Senha text: >- Você precisará dessa senha para efetuar login. Por favor, guarde-a em um local seguro. msg: Senha não pode ser vazia. admin_email: label: Email text: Você precisará deste e-mail para fazer login. msg: empty: Email não pode ser vazio. incorrect: Formato de e-mail incorreto. ready_title: Sua resposta está pronta! ready_desc: >- Se você quiser alterar mais configurações, visite a <1>seção de administração; encontre-a no menu do site. good_luck: "Divirta-se e boa sorte!" warn_title: Aviso warn_desc: >- O arquivo <1>config.yaml já existe. Se precisar redefinir algum item de configuração neste arquivo, exclua-o primeiro. install_now: Você pode tentar <1>instalar agora. installed: Já instalado installed_desc: >- Parece que você já instalou. Para reinstalar, limpe primeiro as tabelas antigas do seu banco de dados. db_failed: Falha na conexão do banco de dados db_failed_desc: >- Isso significa que as informações do banco de dados em um arquivo <1>config.yaml do SUA estão incorretas ou que o contato com o servidor do banco de dados não pôde ser estabelecido. Isso pode significar que o servidor de banco de dados de um host SUA está inativo. counts: views: visualizações Votos: votos answers: respostas accepted: Aceito page_404: desc: "Infelizmente, esta postagem não existe mais." back_home: Voltar para a página inicial page_50X: desc: O servidor encontrou um erro e não pôde concluir sua solicitação. back_home: Voltar para a página inicial page_maintenance: desc: "Estamos em manutenção, voltaremos em breve." nav_menus: dashboard: Painel contents: Conteúdos questions: Perguntas answers: Respostas users: Usuários flags: Marcadores settings: Configurações general: Geral interface: Interface smtp: SMTP branding: Marca legal: Legal write: Escrever tos: Termos de Serviços privacy: Privacidade seo: SEO customize: Personalização Temas: Temas css-html: CSS/HTML login: Login admin: admin_header: title: Administrador dashboard: title: Painel welcome: Bem vindo ao Administrador! site_statistics: Estatísticas do Site questions: "Perguntas:" answers: "Respostas:" comments: "Comentários:" Votos: "Votos:" active_users: "Usuários ativos:" flags: "Marcadores:" site_health_status: Estatísticas da saúde do site version: "Versão:" https: "HTTPS:" uploading_files: "Enviando arquivos:" smtp: "SMTP:" timezone: "Fuso horário:" system_info: Informações do Sistema storage_used: "Armazenamento usado:" uptime: "Tempo de atividade:" answer_links: Links das Respostas documents: Documentos feedback: Opinião support: Suporte review: Revisar config: Configurações update_to: Atualizar ao latest: Ultimo check_failed: Falha na verificação "yes": "Sim" "no": "Não" not_allowed: Não permitido allowed: Permitido enabled: Ativo disabled: Disponível flags: title: Marcadores pending: Pendente completed: Completo flagged: Marcado created: Criado action: Ação review: Revisar change_modal: title: Mudar user status to... btn_cancel: Cancelar btn_submit: Enviar normal_name: normal normal_desc: Um usuário normal pode fazer e responder perguntas. suspended_name: suspenso suspended_desc: Um usuário suspenso não pode fazer login. deleted_name: removido deleted_desc: "Deletar perfil, associações de autenticação." inactive_name: inativo inactive_desc: Um usuário inativo deve revalidar seu e-mail. confirm_title: Remover este usuário confirm_content: Tem certeza de que deseja excluir este usuário? Isso é permanente! confirm_btn: Deletar msg: empty: Por favor selecione um motivo. status_modal: title: "Mudar {{ type }} status para..." normal_name: normal normal_desc: Um post normal disponível para todos. closed_name: fechado closed_desc: "Uma pergunta fechada não pode responder, mas ainda pode editar, votar e comentar." deleted_name: removido deleted_desc: Toda reputação ganha e perdida será restaurada. btn_cancel: Cancelar btn_submit: Enviar btn_next: Próximo user_role_modal: title: Altere a função do usuário para... btn_cancel: Cancelar btn_submit: Enviar users: title: Usuários name: Nome email: Email reputation: Reputação created_at: Hora de criação delete_at: Hora da remoção suspend_at: Hora da suspensão status: Status role: Função action: Ação change: Mudar all: Todos staff: Funcionários inactive: Inativo suspended: Suspenso deleted: Removido normal: Normal Moderador: Moderador Administrador: Administrador Usuário: Usuário filter: placeholder: "Filtrar por nome, user:id" set_new_password: Configurar nova senha change_status: Mudar status change_role: Mudar função show_logs: Mostrar registros add_user: Adicionar usuário new_password_modal: title: Configurar nova senha form: fields: password: label: Senha text: O usuário será desconectado e precisará fazer login novamente. msg: Senha de ver entre 8-32 caracteres em comprimento. btn_cancel: Cancelar btn_submit: Enviar user_modal: title: Adicionar novo usuário form: fields: display_name: label: Nome de exibição msg: Nome de exibição deve ser 2-30 caracteres em comprimento. email: label: Email msg: Email não é válido. password: label: Senha msg: Senha deve ser 8-32 caracteres em comprimento. btn_cancel: Cancelar btn_submit: Enviar questions: page_title: Perguntas normal: Normal closed: Fechado deleted: Removido post: Publicação Votos: Votos answers: Respostas created: Criado status: Status action: Ação change: Mudar filter: placeholder: "Filtrar por título, question:id" answers: page_title: Respostas normal: Normal deleted: Removido post: Publicação Votos: Votos created: Criado status: Status action: Ação change: Mudar filter: placeholder: "Filtrar por título, answer:id" general: page_title: General name: label: Site Nome msg: Site name não pode ser vazio. text: "O nome deste site, conforme usado na tag de título." site_url: label: URL do Site msg: Site url não pode ser vazio. validate: Por favor digite uma URL válida. text: O endereço do seu site. short_desc: label: Breve Descrição do site (opcional) msg: Breve Descrição do site não pode ser vazio. text: "Breve descrição, conforme usado na tag de título na página inicial." desc: label: Site Descrição (opcional) msg: Descrição do site não pode ser vazio. text: "Descreva este site em uma única sentença, conforme usado na meta tag de descrição." contact_email: label: E-mail para contato msg: E-mail par contato não pode ser vazio. validate: E-mail par contato não é válido. text: Endereço de e-mail do principal contato responsável por este site. interface: page_title: Interface logo: label: Logo (opcional) msg: Site logo não pode ser vazio. text: Você pode enviar a sua image ou <1>reiniciar ao texto do título do site. Tema: label: Tema msg: Tema não pode ser vazio. text: Selecione um tema existente. language: label: Idioma da interface msg: Idioma da Interface não pode ser vazio. text: Idioma da interface do Usuário. Ele mudará quando você atualizar a página. time_zone: label: Fuso horário msg: Fuso horário não pode ser vazio. text: Escolha a cidade no mesmo fuso horário que você. smtp: page_title: SMTP from_email: label: E-mail de origem msg: E-mail de origem não pode ser vazio. text: O endereço de e-mail de onde os e-mails são enviados. from_name: label: Nome de origem msg: Nome de origem não pode ser vazio. text: O nome de onde os e-mails são enviados. smtp_host: label: SMTP Host msg: SMTP host não pode ser vazio. text: O seu servidor de e-mails. encryption: label: Criptografia msg: Criptografia não pode ser vazio. text: Para a maioria dos servidores SSL é a opção recomendada. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: A porta para seu servidor de e-mail. smtp_username: label: SMTP Nome de usuário msg: SMTP username não pode ser vazio. smtp_password: label: SMTP Senha msg: SMTP password não pode ser vazio. test_email_recipient: label: Destinatários de e-mail de teste text: Forneça o endereço de e-mail que receberá os envios de testes. msg: Os destinatários do e-mail de teste são inválidos smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication não pode ser vazio. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (opcional) msg: Logo não pode ser vazio. text: A imagem do logotipo no canto superior esquerdo do seu site. Use uma imagem retangular larga com altura de 56 e proporção maior que 3:1. Se deixada em branco, o texto do título do site será exibido. mobile_logo: label: Mobile Logo (opcional) text: O logotipo usado na versão mobile do seu site. Use uma imagem retangular larga com altura de 56. Se deixado em branco, a imagem da configuração "logotipo" será usada. square_icon: label: Square Icon (opcional) msg: Square icon não pode ser vazio. text: Imagem usada como base para ícones de metadados. Idealmente, deve ser maior que 512x512. favicon: label: Favicon (opcional) text: Um favicon para o seu site. Para funcionar corretamente em uma CDN, ele deve ser um png. Será redimensionado para 32x32. Se deixado em branco, o "ícone quadrado" será usado. legal: page_title: Legal terms_of_service: label: Termos de Serviço text: "Você pode adicionar conteúdo dos termos de serviço aqui. Se você já possui um documento hospedado em outro lugar, informe o URL completo aqui." privacy_policy: label: Política de Privacidade text: "Você pode adicionar o conteúdo da política de privacidade aqui. Se você já possui um documento hospedado em outro lugar, informe o URL completo aqui." write: page_title: Escrever recommend_tags: label: Recomendar Marcadores text: "Por favor, insira o slug da tag acima, uma tag por linha." required_tag: title: Tag necessária label: Definir tag recomendada como necessária text: "Cada nova pergunta deve ter pelo menos uma tag de recomendação." reserved_tags: label: Marcadores Reservados text: "Tags reservadas só podem ser adicionadas a uma postagem pelo moderador." seo: page_title: SEO permalink: label: Permalink text: Estruturas de URL personalizadas podem melhorar a usabilidade e a compatibilidade futura de seus links. robots: label: robots.txt text: Isso substituirá permanentemente todas as configurações relacionadas do site. Temas: page_title: Temas Temas: label: Temas text: Selecione um tema existente. navbar_style: label: Estilo da barra de navegação text: Selecione um tema existente. primary_color: label: Cor primária text: Modifique as cores usadas por seus Temas css_and_html: page_title: CSS e HTML custom_css: label: Custom CSS text: Isto será inserido como head: label: Head text: Isto será inserido antes de header: label: Header text: Isto será inserido após footer: label: Footer text: Isso será inserido antes de . login: page_title: Login membership: title: Associação label: Permitir novos registros text: Desative para impedir que alguém crie uma nova conta. private: title: Privado label: Login requirido text: Somente usuários logados podem acessar esta comunidade. form: empty: não pode ser vazio invalid: é inválido btn_submit: Salvar not_found_props: "Propriedade necessária {{ key }} não encontrada." page_review: review: Revisar proposed: proposta question_edit: Editar Pergunta answer_edit: Editar Resposta tag_edit: Editar Tag edit_summary: Editar resumo edit_question: Editar pergunta edit_answer: Editar resposta edit_tag: Editar tag empty: Não há mais tarefas de revisão. timeline: undeleted: não excluído deleted: apagado downvote: voto negativo upvote: voto positivo accept: aceitar cancelled: cancelado commented: comentado rollback: rollback edited: editado answered: respondido asked: perguntado closed: fechado reopened: reaberto created: criado title: "Histórico para" tag_title: "Linha do tempo para" show_Votos: "Mostrar votos" n_or_a: N/A title_for_question: "Linha do tempo para" title_for_answer: "Linha do tempo para resposta a {{ title }} por {{ author }}" title_for_tag: "Linha do tempo para tag" datetime: Datetime type: Tipo by: Por comment: Comentário no_data: "Não conseguimos encontrar nada." users: title: Usuários users_with_the_most_reputation: Usuários com as maiores pontuações de reputação users_with_the_most_vote: Usuários que mais votaram staffs: Nossa equipe comunitária reputation: reputação Votos: Votos ================================================ FILE: i18n/pt_PT.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Sucesso. unknown: other: Erro desconhecido. request_format_error: other: Formato de solicitação inválido. unauthorized_error: other: Não autorizado. database_error: other: Erro no servidor de dados. forbidden_error: other: Proibido. duplicate_request_error: other: Solicitação duplicada. action: report: other: Bandeira edit: other: Editar delete: other: Excluir close: other: Fechar reopen: other: Reabrir forbidden_error: other: Proibido. pin: other: Fixar hide: other: Não listar unpin: other: Desafixar show: other: Lista invite_someone_to_answer: other: Editar undelete: other: Desfazer merge: other: Merge role: name: user: other: Usuário admin: other: Administrador moderator: other: Moderador description: user: other: Padrão sem acesso especial. admin: other: Possui acesso total. moderator: other: Acesso a todas as postagens, exceto configurações administrativas. privilege: level_1: description: other: Nível 1 (Menos reputação necessária para a equipe privado, grupo) level_2: description: other: Nível 2 (pouca reputação necessária para a comunidade de inicialização) level_3: description: other: Nível 3 (Alta reputação necessária para a comunidade adulta) level_custom: description: other: Nível customizado rank_question_add_label: other: Perguntar rank_answer_add_label: other: Escrever resposta rank_comment_add_label: other: Comentar rank_report_add_label: other: Bandeira rank_comment_vote_up_label: other: Aprovar um comentário rank_link_url_limit_label: other: Poste mais de 2 links por vez rank_question_vote_up_label: other: Avaliar perguntar rank_answer_vote_up_label: other: Votar na resposta rank_question_vote_down_label: other: Desaprovar pergunta rank_answer_vote_down_label: other: Desaprovar resposta rank_invite_someone_to_answer_label: other: Convide alguém para responder rank_tag_add_label: other: Criar marcador rank_tag_edit_label: other: Editar descrição de um marcador (revisão necessária) rank_question_edit_label: other: Editar pergunta do outro (revisão necessária) rank_answer_edit_label: other: Editar a resposta do outro (revisão necessária) rank_question_edit_without_review_label: other: Editar a pergunta do outro sem revisar rank_answer_edit_without_review_label: other: Editar a resposta do outro sem revisar rank_question_audit_label: other: Rever edições de perguntas rank_answer_audit_label: other: Revisar edições de respostas rank_tag_audit_label: other: Revisar edições de marcadores rank_tag_edit_without_review_label: other: Editar descrições de marcadores sem revisar rank_tag_synonym_label: other: Gerenciar sinônimos de marcação email: other: E-mail e_mail: other: Email password: other: Senha pass: other: Senha old_pass: other: Current password original_text: other: Esta publicação email_or_password_wrong_error: other: O e-mail e a palavra-passe não coincidem. error: common: invalid_url: other: URL inválida. status_invalid: other: Estado inválido. password: space_invalid: other: A senha não pode conter espaços. admin: cannot_update_their_password: other: Você não pode modificar sua senha. cannot_edit_their_profile: other: Você não pode alterar o seu perfil. cannot_modify_self_status: other: Você não pode modificar seu status email_or_password_wrong: other: O e-mail e a palavra-passe não coincidem. answer: not_found: other: Resposta não encontrada. cannot_deleted: other: Sem permissão para remover. cannot_update: other: Sem permissão para atualizar. question_closed_cannot_add: other: Perguntas são fechadas e não podem ser adicionadas. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Não é possível alterar comentários. not_found: other: Comentário não encontrado. cannot_edit_after_deadline: other: O tempo do comentário foi muito longo para ser modificado. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: O e-mail já existe. need_to_be_verified: other: O e-mail deve ser verificado. verify_url_expired: other: O e-mail verificado URL expirou, por favor, reenvie o e-mail. illegal_email_domain_error: other: O email não é permitido a partir desse email de domínio. Por favor, use outro. lang: not_found: other: Arquivo de idioma não encontrado. object: captcha_verification_failed: other: O Captcha está incorreto. disallow_follow: other: Você não tem permissão para seguir. disallow_vote: other: Você não possui permissão para votar. disallow_vote_your_self: other: Você não pode votar na sua própria postagem. not_found: other: Objeto não encontrado. verification_failed: other: A verificação falhou. email_or_password_incorrect: other: O e-mail e a senha não correspondem. old_password_verification_failed: other: Falha na verificação de senha antiga new_password_same_as_previous_setting: other: A nova senha é a mesma que a anterior. already_deleted: other: Esta publicação foi removida. meta: object_not_found: other: Objeto meta não encontrado question: already_deleted: other: Essa publicação foi deletado. under_review: other: Sua postagem está aguardando revisão. Ela ficará visível depois que for aprovada. not_found: other: Pergunta não encontrada. cannot_deleted: other: Sem permissão para remover. cannot_close: other: Sem permissão para fechar. cannot_update: other: Sem permissão para atualizar. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: A classificação não atende à condição. vote_fail_to_meet_the_condition: other: Obrigado pela sugestão. Você precisa ser {{.Rank}} para poder votar. no_enough_rank_to_operate: other: Você precisa ser pelo menos {{.Rank}} para fazer isso. report: handle_failed: other: Falha ao manusear relatório. not_found: other: Relatório não encontrado. tag: already_exist: other: Marcação já existe. not_found: other: Marca não encontrada. recommend_tag_not_found: other: Marcador recomendado não existe. recommend_tag_enter: other: Por favor, insira pelo menos um marcador obrigatório. not_contain_synonym_tags: other: Não deve conter marcadores sinónimos. cannot_update: other: Sem permissão para atualizar. is_used_cannot_delete: other: Não é possível excluir um marcador em uso. cannot_set_synonym_as_itself: other: Você não pode definir o sinônimo do marcador atual como a si mesmo. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: O De Nome não pode ser um endereço de e-mail. theme: not_found: other: Tema não encontrado. revision: review_underway: other: Não é possível editar atualmente, há uma versão na fila de análise. no_permission: other: Sem pemissão para revisar. user: external_login_missing_user_id: other: A plataforma de terceiros não fornece um ID único de usuário, então você não pode fazer login, entre em contato com o administrador do site. external_login_unbinding_forbidden: other: Por favor, defina uma senha de login para sua conta antes de remover esta conta. email_or_password_wrong: other: other: O e-mail e a senha não conferem. not_found: other: Usuário não encontrado. suspended: other: O utilizador foi suspenso. username_invalid: other: Nome de usuário inválido. username_duplicate: other: O nome de usuário já em uso. set_avatar: other: Configuração de avatar falhou. cannot_update_your_role: other: Você não pode modificar a sua função. not_allowed_registration: other: Atualmente o site não está aberto para cadastro not_allowed_login_via_password: other: Atualmente o site não tem permissão para acessar utilizando senha. access_denied: other: Acesso negado page_access_denied: other: Você não tem permissão de acesso para esta página. add_bulk_users_format_error: other: "Erro no formato {{.Field}} próximo '{{.Content}}' na linha {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "O número de usuários que você adicionou de uma vez deve estar no intervalo de 1 -{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Falha ao ler configuração database: connection_failed: other: Falha ao conectar-se ao banco de dados create_table_failed: other: Falha ao criar tabela install: create_config_failed: other: Não foi possível criar o arquivo de configuração. upload: unsupported_file_format: other: Formato de arquivo não suportado. site_info: config_not_found: other: Configuração do site não encontrada. badge: object_not_found: other: Objeto emblema não encontrado reason: spam: name: other: spam desc: other: Essa postagem é um anúncio, ou vandalismo. Não é útil ou relevante para o tópico atual. rude_or_abusive: name: other: rude ou abusivo desc: other: "Uma pessoa razoável consideraria esse conteúdo inapropriado para um discurso respeitoso." a_duplicate: name: other: uma duplicação desc: other: Esta pergunta já foi feita antes e já possui uma resposta. placeholder: other: Insira o link existente para a pergunta not_a_answer: name: other: não é uma resposta desc: other: "Foi apresentada como uma resposta, mas não tenta responder à pergunta. Talvez deva ser uma edição, um comentário, outra pergunta ou totalmente apagada." no_longer_needed: name: other: não é mais necessário desc: other: Este comentário está desatualizado, conversacional ou não é relevante para esta publicação. something: name: other: outro assunto desc: other: Esta postagem requer atenção da equipe por outra razão não listada acima. placeholder: other: Conte-nos especificamente com o que você está preocupado community_specific: name: other: um motivo específico para a comunidade desc: other: Esta questão não atende a uma orientação da comunidade. not_clarity: name: other: precisa de detalhes ou clareza desc: other: Atualmente esta pergunta inclui várias perguntas em um só. Deve se concentrar apenas em um problema. looks_ok: name: other: parece estar OK desc: other: Este post é bom como está e não de baixa qualidade. needs_edit: name: other: necessitava de edição, assim o fiz desc: other: Melhore e corrija problemas com este post você mesmo. needs_close: name: other: precisa ser fechado desc: other: Uma pergunta fechada não pode responder, mas ainda pode editar, votar e comentar. needs_delete: name: other: precisa ser excluído desc: other: Esta postagem será excluída. question: close: duplicate: name: other: spam desc: other: Esta pergunta já foi feita antes e já tem uma resposta. guideline: name: other: um motivo específico da comunidade desc: other: Esta pergunta não atende a uma diretriz da comunidade. multiple: name: other: precisa de detalhes ou clareza desc: other: Esta pergunta atualmente inclui várias perguntas em uma só. Por isso, deve focar em apenas um problema. other: name: other: algo mais desc: other: Este post requer outro motivo não listado acima. operation_type: asked: other: perguntado answered: other: respondido modified: other: modificado deleted_title: other: Questão excluída questions_title: other: Questões tag: tags_title: other: Marcadores no_description: other: O marcador não possui descrição. notification: action: update_question: other: pergunta atualizada answer_the_question: other: pergunta respondida update_answer: other: resposta atualizada accept_answer: other: resposta aceita comment_question: other: pergunta comentada comment_answer: other: resposta comentada reply_to_you: other: respondeu a você mention_you: other: mencionou você your_question_is_closed: other: A sua pergunta foi fechada your_question_was_deleted: other: A sua pergunta foi deletada your_answer_was_deleted: other: A sua resposta foi deletada your_comment_was_deleted: other: O seu comentário foi deletado up_voted_question: other: votos positivos da pergunta down_voted_question: other: votos negativos da pergunta up_voted_answer: other: votos positivos da resposta down_voted_answer: other: votos negativos da resposta up_voted_comment: other: votos positivos do comentário invited_you_to_answer: other: lhe convidou para responder earned_badge: other: Ganhou o emblema "{{.BadgeName}}" emblema email_tpl: change_email: title: other: "[{{.SiteName}}] Confirme seu novo endereço de e-mail" body: other: "Confirme seu novo endereço de e-mail para {{.SiteName}} clicando no seguinte link:
\n{{.ChangeEmailUrl}}

\n\nSe você não solicitou esta alteração, por favor, ignore este email.

\n\n--
\nNota: Este é um e-mail automático do sistema, por favor, não responda a esta mensagem, pois a sua resposta não será vista." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} respondeu à sua pergunta" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nVisualizar no {{.SiteName}}

\n\n--
\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista.

\n\nCancelar inscrição" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} convidou-lhe para responder" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
acho que você pode saber a resposta.

\nVisualizar na {{.SiteName}}

\n\n--
\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista.

\n\nCancelar Inscrição" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} comentou em sua publicação" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nVisualizar no {{.SiteName}}

\n\n--
\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista.

\n\nCancelar inscrição" new_question: title: other: "[{{.SiteName}}] Nova pergunta: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Redefinição de senha" body: other: "Alguém pediu para redefinir a sua senha em {{.SiteName}}.

\n\nSe não foi você, você pode ignorar este e-mail.

\n\nClique no seguinte link para escolher uma nova senha:
\n{{.PassResetUrl}}

\n\n--
\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista." register: title: other: "[{{.SiteName}}] Confirme seu novo endereço de e-mail" body: other: "Bem-vindo a {{.SiteName}}!

\n\nClique no seguinte link para confirmar e ativar sua nova conta:
\n{{.RegisterUrl}}

\n\nSe o link acima não for clicável, tente copiá-lo e colá-lo na barra de endereços do seu navegador web.\n

\n\n--
\nNota: Este é um e-mail de sistema automático. por favor, não responda a esta mensagem, pois a sua resposta não será vista." test: title: other: "[{{.SiteName}}] E-mail de teste" body: other: "Este é um e-mail de teste.\n

\n\n--
\nNota: Este é um e-mail automático do sistema, por favor, não responda a esta mensagem, pois a sua resposta não será vista." action_activity_type: upvote: other: voto positivo upvoted: other: votos positivos downvote: other: voto negativo downvoted: other: voto negativo accept: other: aceito accepted: other: aceito edit: other: editar review: queued_post: other: Publicação na fila flagged_post: other: Postagem sinalizada suggested_post_edit: other: Edições sugeridas reaction: tooltip: other: "{{ .Names }} e mais {{ .Count }}..." badge: default_badges: autobiographer: name: other: Autobiógrafo desc: other: Preenchido com perfil . certified: name: other: Certificado desc: other: Completou o nosso tutorial de novo usuário. editor: name: other: Editor desc: other: Primeira edição em publicação. first_flag: name: other: Primeira Sinalização desc: other: Primeiro sinalização numa publicação. first_upvote: name: other: Primeiro voto desc: other: Primeiro post votado. first_link: name: other: Primeiro link desc: other: First added a link to another post. first_reaction: name: other: Primeira Reação desc: other: Primeira reação a um post. first_share: name: other: Primeiro Compartilhamento desc: other: Primeiro a compartilhar um post. scholar: name: other: Académico desc: other: Fez uma pergunta e aceitou uma resposta. commentator: name: other: Comentador desc: other: Fez 5 comentários. new_user_of_the_month: name: other: Novo usuário do mês desc: other: Contribuições pendentes no seu primeiro mês. read_guidelines: name: other: Ler diretrizes desc: other: Leia as [diretrizes da comunidade]. reader: name: other: Leitor desc: other: Leia todas as respostas num tópico com mais de 10 respostas. welcome: name: other: Bem-vindo desc: other: Recebeu um voto positivo. nice_share: name: other: Bom compartilhador desc: other: Compartilhou um post com 25 visitantes únicos. good_share: name: other: Bom compartilhador desc: other: Compartilhou um post com 300 visitantes únicos. great_share: name: other: Grande Compartilhador desc: other: Compartilhou um post com 1000 visitantes únicos. out_of_love: name: other: Por amor desc: other: Cinquenta votos positivos em um dia. higher_love: name: other: Amor Superior desc: other: Usou 50 votos positivos em um dia — 5 vezes. crazy_in_love: name: other: Amor Louco desc: other: Usou 50 votos positivos em um dia — 20 vezes. promoter: name: other: Promotor desc: other: Convidou um usuário. campaigner: name: other: Ativista desc: other: Foram convidados 3 usuários básicos. champion: name: other: Campeão desc: other: Cinco membros convidados. thank_you: name: other: Obrigado desc: other: Recebeu 20 votos positivos e deu 10 votos. gives_back: name: other: Dar de volta desc: other: Recebeu100 votos positivos e deu 100 votos a favor. empathetic: name: other: Empático desc: other: Recebeu 500 votos e deu 1000 votos. enthusiast: name: other: Entusiasta desc: other: . aficionado: name: other: Aficionado desc: other: Visitou 100 dias consecutivos. devotee: name: other: Devoto desc: other: Visitou 365 dias consecutivos. anniversary: name: other: Aniversário desc: other: Membro ativo por um ano, postando pelo menos uma vez. appreciated: name: other: Apreciado desc: other: Recebeu 1 voto positivo em 20 posts. respected: name: other: Respeitado desc: other: Novos 2 votos em 100 posts. admired: name: other: Admirado desc: other: Recebeu 5 votos positivos em 300 posts. solved: name: other: Resolvido desc: other: Ter uma resposta aceita. guidance_counsellor: name: other: Orientador Educacional desc: other: Tenham 10 respostas aceitas. know_it_all: name: other: Sabichão desc: other: Tenha 50 respostas aceitas. solution_institution: name: other: Instituição de Soluções desc: other: Tenham 150 respostas aceitas. nice_answer: name: other: Resposta legal desc: other: Resposta com pontuação maior que 10. good_answer: name: other: Boa resposta desc: other: Resposta com pontuação maior que 25. great_answer: name: other: Ótima Resposta desc: other: Resposta com pontuação maior que 50. nice_question: name: other: Questão legal desc: other: Pergunta com pontuação maior que 10. good_question: name: other: Boa questão desc: other: Questão com pontuação maior que 25. great_question: name: other: Otima questão desc: other: Questão com pontuação maior que 50. popular_question: name: other: Pergunta Popular desc: other: Pergunta com 500 visualizações. notable_question: name: other: Pergunta Notável desc: other: Pergunta com 1000 visualizações. famous_question: name: other: Pergunta Famosa desc: other: Pergunta com 5000 visualizações. popular_link: name: other: Link Popular desc: other: Postou um link externo com 50 cliques. hot_link: name: other: Link Quente desc: other: Postou um link externo com 300 cliques. famous_link: name: other: Link Famoso desc: other: Postou um link externo com 100 cliques. default_badge_groups: getting_started: name: other: Começando community: name: other: Comunidade posting: name: other: Postando # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Como formatar desc: >-
  • mencionar uma postagem: #post_id

  • para mais links

    <https://url.com>

    [Título](https://url.com)
  • colocar retornos entre parágrafos

  • _italic_ ou **bold**

  • identar código por 4 espaços

  • cotação colocando > no início da linha

  • aspas invertidas `tipo_isso`

  • criar cercas de código com aspas invertidas `

    ```
    código aqui
    ```
pagination: prev: Anterior next: Próximo page_title: question: Pergunta questions: Perguntas tag: Marcador tags: Marcadores tag_wiki: marcador wiki create_tag: Criar Marcador edit_tag: Editar Marcador ask_a_question: Create Question edit_question: Editar Pergunta edit_answer: Editar Resposta search: Busca posts_containing: Postagens contendo settings: Configurações notifications: Notificações login: Entrar sign_up: Registar-se account_recovery: Recuperação de conta account_activation: Ativação de conta confirm_email: Confirmar E-mail account_suspended: Conta suspensa admin: Administrador change_email: Modificar e-mail install: Instalação do Answer upgrade: Atualização do Answer maintenance: Manutenção do website users: Usuários oauth_callback: Processando http_404: HTTP Erro 404 http_50X: HTTP Erro 500 http_403: HTTP Erro 403 logout: Encerrar Sessão posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notificações inbox: Caixa de entrada achievement: Conquistas new_alerts: Novos alertas all_read: Marcar todos como lida show_more: Mostrar mais someone: Alguém inbox_type: all: Tudo posts: Postagens invites: Convites votes: Votos answer: Resposta question: Questão badge_award: Emblema suspended: title: A sua conta foi suspensa until_time: "Sua conta está suspensa até {{ time }}." forever: Este usuário foi suspenso permanentemente. end: Você não atende a uma diretriz da comunidade. contact_us: Entre em contato conosco editor: blockquote: text: Bloco de citação bold: text: Negrito chart: text: Gráfico flow_chart: Gráfico de fluxo sequence_diagram: Diagrama de sequência class_diagram: Diagrama de classe state_diagram: Diagrama de estado entity_relationship_diagram: Diagrama de relacionamento de entidade user_defined_diagram: Diagrama definido pelo usuário gantt_chart: Gráfico de Gantt pie_chart: Gráfico de pizza code: text: Exemplo de código add_code: Adicionar exemplo de código form: fields: code: label: Código msg: empty: Código não pode ser vazio. language: label: Idioma placeholder: Deteção automática btn_cancel: Cancelar btn_confirm: Adicionar formula: text: Fórmula options: inline: Fórmula na linha block: Bloco de fórmula heading: text: Cabeçalho options: h1: Cabeçalho 1 h2: Cabeçalho 2 h3: Cabeçalho 3 h4: Cabeçalho 4 h5: Cabeçalho 5 h6: Cabeçalho 6 help: text: Ajuda hr: text: Régua horizontal image: text: Imagem add_image: Adicionar imagem tab_image: Enviar image, form_image: fields: file: label: Arquivo de imagem btn: Selecione imagem msg: empty: Arquivo não pode ser vazio. only_image: Somente um arquivo de imagem é permitido. max_size: O tamanho do arquivo não pode exceder {{size}} MB. desc: label: Descrição (opcional) tab_url: URL da imagem form_url: fields: url: label: URL da imagem msg: empty: URL da imagem não pode ser vazia. name: label: Descrição (opcional) btn_cancel: Cancelar btn_confirm: Adicionar uploading: Enviando indent: text: Identação outdent: text: Não identado italic: text: Emphase link: text: Superlink (Hyperlink) add_link: Adicionar superlink (hyperlink) form: fields: url: label: URL msg: empty: URL não pode ser vazia. name: label: Descrição (opcional) btn_cancel: Cancelar btn_confirm: Adicionar ordered_list: text: Lista numerada unordered_list: text: Lista com marcadores table: text: Tabela heading: heading cell: Célula file: text: Anexar arquivos not_supported: "Não suporta esse tipo de arquivo. Tente novamente com {{file_type}}." max_size: "Anexar arquivos não pode exceder {{size}} MB." close_modal: title: Estou fechando este post como... btn_cancel: Cancelar btn_submit: Enviar remark: empty: Não pode ser vazio. msg: empty: Por favor selecione um motivo. report_modal: flag_title: Estou marcando para denunciar este post como... close_title: Estou fechando este post como... review_question_title: Revisar pergunta review_answer_title: Revisar resposta review_comment_title: Revisar comentário btn_cancel: Cancelar btn_submit: Enviar remark: empty: Não pode ser vazio. msg: empty: Por favor selecione um motivo. not_a_url: Formato da URL incorreto. url_not_match: A origem da URL não corresponde ao site atual. tag_modal: title: Criar novo marcador form: fields: display_name: label: Nome de exibição msg: empty: Nome de exibição não pode ser vazio. range: Nome de exibição tem que ter até 35 caracteres. slug_name: label: Slug de URL desc: 'Deve usar o conjunto de caracteres "a-z", "0-9", "+ # - ."' msg: empty: URL slug não pode ser vazio. range: URL slug até 35 caracteres. character: URL slug contém conjunto de caracteres não permitido. desc: label: Descrição (opcional) revision: label: Revisão edit_summary: label: Editar descrição placeholder: >- Explique resumidamente as suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) btn_cancel: Cancelar btn_submit: Enviar btn_post: Postar novo marcador tag_info: created_at: Criado edited_at: Editado history: Histórico synonyms: title: Sinônimos text: Os marcadores a seguir serão re-mapeados para empty: Sinônimos não encotrados. btn_add: Adicionar um sinônimo btn_edit: Editar btn_save: Salvar synonyms_text: Os marcadores a seguir serão re-mapeados para delete: title: Remover este marcador tip_with_posts: >-

Nós não permitimos remover marcadores com postagens.

Por favor, remova este marcador a partir da postagem.

tip_with_synonyms: >-

Nós não permitimos remover marcadores com postagens.

Por favor, remova este marcador a partir da postagem.

tip: Você tem certeza que deseja remover? close: Fechar merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Editar marcador default_reason: Editar marcador default_first_reason: Adicionar marcador btn_save_edits: Salvar edições btn_cancel: Cancelar dates: long_date: D MMM long_date_with_year: "D MMM, YYYY" long_date_with_time: "D MMM, YYYY [at] HH:mm" now: agora x_seconds_ago: "{{count}}s atrás" x_minutes_ago: "{{count}}m atrás" x_hours_ago: "{{count}}h atrás" hour: hora day: dia hours: horas days: dias month: month months: months year: year reaction: heart: coração smile: sorrir frown: cara feia btn_label: adicionar ou remover reações undo_emoji: desfazer reação {{ emoji }} react_emoji: reagir com {{ emoji }} unreact_emoji: remover reação {{ emoji }} comment: btn_add_comment: Adicionar comentário reply_to: Responder a btn_reply: Responder btn_edit: Editar btn_delete: Excluir btn_flag: Marcador btn_save_edits: Salvar edições btn_cancel: Cancelar show_more: "Mais {{count}} comentários" tip_question: >- Use os comentários para pedir mais informações ou sugerir melhorias. Evite responder perguntas nos comentários. tip_answer: >- Use comentários para responder a outros usuários ou notificá-los sobre alterações. Se você estiver adicionando novas informações, edite sua postagem em vez de comentar. tip_vote: Isto adiciona alguma utilidade à postagem edit_answer: title: Editar Resposta default_reason: Editar Resposta default_first_reason: Adicionar resposta form: fields: revision: label: Revisão answer: label: Resposta feedback: characters: conteúdo deve ser pelo menos 6 characters em comprimento. edit_summary: label: Resumo da edição placeholder: >- Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) btn_save_edits: Salvar edições btn_cancel: Cancelar tags: title: Marcadores sort_buttons: popular: Popular name: Nome newest: mais recente button_follow: Seguir button_following: Seguindo tag_label: perguntas search_placeholder: Filtrar por nome de marcador no_desc: O marcador não possui descrição. more: Mais wiki: Wiki ask: title: Create Question edit_title: Editar Pergunta default_reason: Editar pergunta default_first_reason: Create question similar_questions: Similar perguntas form: fields: revision: label: Revisão title: label: Título placeholder: What's your topic? Be specific. msg: empty: Título não pode ser vazio. range: Título até 150 caracteres body: label: Corpo msg: empty: Corpo da mensagem não pode ser vazio. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Marcadores msg: empty: Marcadores não podes ser vazios. answer: label: Resposta msg: empty: Resposta não pode ser vazia. edit_summary: label: Resumo da edição placeholder: >- Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) btn_post_question: Publicação a sua pergunta btn_save_edits: Salvar edições answer_question: Responda a sua própria pergunta post_question&answer: Publicação a sua pergunta e resposta tag_selector: add_btn: Adicionar marcador create_btn: Criar novo marcador search_tag: Procurar marcador hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Nenhum marcador correspondente tag_required_text: Marcador obrigatório (ao menos um) header: nav: question: Perguntas tag: Marcadores user: Usuários badges: Emblemas profile: Perfil setting: Configurações logout: Sair admin: Administrador review: Revisar bookmark: Favoritos moderation: Moderação search: placeholder: Procurar footer: build_on: Powered by <1> Apache Answer upload_img: name: Mudar loading: carregando... pic_auth_code: title: Captcha placeholder: Escreva o texto acima msg: empty: Captcha não pode ser vazio. inactive: first: >- Você está quase pronto! Enviamos um e-mail de ativação para {{mail}}. Por favor, siga as instruções no e-mail para ativar uma conta sua. info: "Se não chegar, verifique sua pasta de spam." another: >- Enviamos outro e-mail de ativação para você em {{mail}}. Pode levar alguns minutos para chegar; certifique-se de verificar sua pasta de spam. btn_name: Reenviar e-mail de ativação change_btn_name: Mudar email msg: empty: Não pode ser vazio. resend_email: url_label: Tem certeza de que deseja reenviar o e-mail de ativação? url_text: Você também pode fornecer o link de ativação acima para o usuário. login: login_to_continue: Entre para continue info_sign: Não possui uma conta? <1>Cadastrar-se info_login: Já possui uma conta? <1>Entre agreements: Ao se registrar, você concorda com as <1>políticas de privacidades e os <3>termos de serviços. forgot_pass: Esqueceu a sua senha? name: label: Nome msg: empty: Nome não pode ser vazio. range: O nome deve ter entre 2 e 30 caracteres. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: E-mail msg: empty: E-mail não pode ser vazio. password: label: Senha msg: empty: Senha não pode ser vazia. different: As senhas inseridas em ambos os campos são inconsistentes account_forgot: page_title: Esqueceu a sua senha btn_name: Enviar e-mail de recuperação de senha send_success: >- Se uma conta corresponder {{mail}}, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve. email: label: E-mail msg: empty: E-mail não pode ser vazio. change_email: btn_cancel: Cancelar btn_update: Atualiza email address send_success: >- Se uma conta corresponder {{mail}}, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve. email: label: Novo E-mail msg: empty: E-mail não pode ser vazio. oauth: connect: Conecte com {{ auth_name }} remove: Remover {{ auth_name }} oauth_bind_email: subtitle: Adicione um e-mail para recuperação da conta. btn_update: Atualizar endereço de e-mail email: label: E-mail msg: empty: E-mail não pode ser vazio. modal_title: E-mail já existe. modal_content: Este e-mail já está cadastrado. Você tem certeza que deseja conectar à conta existente? modal_cancel: Alterar E-mail modal_confirm: Conectar a uma conta existente password_reset: page_title: Redefinir senha btn_name: Redefinir minha senha reset_success: >- Você alterou com sucesso uma senha sua; você será redirecionado para a página de login. link_invalid: >- Desculpe, este link de redefinição de senha não é mais válido. Talvez uma senha sua já tenha sido redefinida? to_login: Continuar para a tela de login password: label: Senha msg: empty: Senha não pode ser vazio. length: O comprimento deve estar entre 8 e 32 different: As senhas inseridas em ambos os campos são inconsistentes password_confirm: label: Confirmar Nova senha settings: page_title: Configurações goto_modify: Ir para modificar nav: profile: Perfil notification: Notificações account: Conta interface: Interface profile: heading: Perfil btn_name: Salvar display_name: label: Nome de exibição msg: Nome de exibição não pode ser vazio. msg_range: Display name must be 2-30 characters in length. username: label: Nome de usuário caption: As pessoas poderão mensionar você com "@usuário". msg: Nome de usuário não pode ser vazio. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Perfil Imagem gravatar: Gravatar gravatar_text: Você pode mudar a imagem em <1>gravatar.com custom: Customizado custom_text: Você pode enviar a sua image. default: Padrão do Sistema msg: Por favor envie um avatar bio: label: Sobre mim (opcional) website: label: Website (opcional) placeholder: "https://exemplo.com.br" msg: Formato incorreto de endereço de Website location: label: Localização (opcional) placeholder: "Cidade, País" notification: heading: Notificações por e-mail turn_on: Ativar inbox: label: Notificações na caixa de entrada description: Responda suas próprias perguntas, comentários, convites e muito mais. all_new_question: label: Todas as perguntas novas description: Seja notificado de todas as novas perguntas. Até 50 perguntas por semana. all_new_question_for_following_tags: label: Novas perguntas para os seguintes marcadores description: Seja notificado de novas perguntas para os seguintes marcadores. account: heading: Conta change_email_btn: Mudar e-mail change_pass_btn: Mudar senha change_email_info: >- Enviamos um e-mail para esse endereço. Siga as instruções de confirmação. email: label: Correio eletrónico new_email: label: Novo correio eletrónico msg: Novo correio eletrónico não pode ser vazio. pass: label: Senha atual msg: Senha não pode ser vazio. password_title: Senha current_pass: label: Senha atual msg: empty: A Senha não pode ser vazia. length: O comprimento deve estar entre 8 and 32. different: As duas senhas inseridas não correspondem. new_pass: label: Nova Senha pass_confirm: label: Confirmar nova Senha interface: heading: Interface lang: label: Idioma da Interface text: Idioma da interface do usuário. A interface mudará quando você atualizar a página. my_logins: title: Meus logins label: Entre ou cadastre-se neste site utilizando estas contas. modal_title: Remover login modal_content: Você tem certeza que deseja remover este login da sua conta? modal_confirm_btn: Remover remove_success: Removido com sucesso toast: update: atualização realizada com sucesso update_password: Senha alterada com sucesso. flag_success: Obrigado por marcar. forbidden_operate_self: Proibido para operar por você mesmo review: A sua resposta irá aparecer após a revisão. sent_success: Enviado com sucesso related_question: title: Related answers: respostas linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Pessoas Perguntaram desc: Select people who you think might know the answer. invite: Convidar para responder add: Adicionar pessoas search: Procurar pessoas question_detail: action: Acção created: Created Asked: Perguntado asked: perguntado update: Modificado Edited: Edited edit: modificado commented: comentado Views: Visualizado Follow: Seguir Following: Seguindo follow_tip: Siga esta pergunta para receber notificações answered: respondido closed_in: Fechado em show_exist: Mostrar pergunta existente. useful: Útil question_useful: Isso é útil e claro question_un_useful: Isso não está claro ou não é útil question_bookmark: Favoritar esta pergunta answer_useful: Isso é útil answer_un_useful: Isso não é útil answers: title: Respostas score: Pontuação newest: Mais recente oldest: Mais Antigos btn_accept: Aceito btn_accepted: Aceito write_answer: title: A sua Resposta edit_answer: Editar a minha resposta existente btn_name: Publicação a sua resposta add_another_answer: Adicionar outra resposta confirm_title: Continuar a responder continue: Continuar confirm_info: >-

Tem certeza de que deseja adicionar outra resposta?

Você pode usar o link de edição para refinar e melhorar uma resposta existente.

empty: Resposta não pode ser vazio. characters: conteúdo deve ser pelo menos 6 caracteres em comprimento. tips: header_1: Obrigado pela sua resposta li1_1: Por favor, não esqueça de responder a pergunta. Providencie detalhes e compartilhe a sua pesquisa. li1_2: Faça backup de quaisquer declarações que você fizer com referências ou experiência pessoal. header_2: Mas evite ... li2_1: Pedir ajuda, buscar esclarecimentos ou responder a outras respostas. reopen: confirm_btn: Reabrir title: Reabrir esta postagem content: Você tem certeza que deseja reabrir? list: confirm_btn: Lista title: Liste esta postagem content: Você tem certeza que deseja listar? unlist: confirm_btn: Remover da lista title: Remover da lista de postagens content: Tem certeza de que deseja remover da lista? pin: title: Fixe esta postagem content: Tem certeza de que deseja fixar globalmente? Esta postagem aparecerá no topo de todas as listas de postagens. confirm_btn: Fixar delete: title: Excluir esta postagem question: >- Nós não recomendamos excluindo perguntas com respostas porque isso priva os futuros leitores desse conhecimento.

Repeated deletion of answered questions can result in a sua account being blocked from asking. Você tem certeza que deseja deletar? answer_accepted: >-

Nós não recomendamos excluir perguntas com respostas porque isso priva os futuros leitores desse conhecimento.

A exclusão repetida de respostas aceitas pode resultar no bloqueio de respostas de sua conta.. Você tem certeza que deseja deletar? other: Você tem certeza que deseja deletar? tip_answer_deleted: Esta resposta foi deletada undelete_title: Recuperar esta publicação undelete_desc: Você tem certeza que deseja recuperar? btns: confirm: Confirmar cancel: Cancelar edit: Editar save: Salvar delete: Excluir undelete: Recuperar list: Lista unlist: Não listar unlisted: Não listado login: Entrar signup: Cadastrar-se logout: Sair verify: Verificar create: Create approve: Aprovar reject: Rejetar skip: Pular discard_draft: Descartar rascunho pinned: Fixado all: Todos question: Pergunta answer: Resposta comment: Comentário refresh: Atualizar resend: Reenviar deactivate: Desativar active: Ativar suspend: Suspender unsuspend: Suspensão cancelada close: Fechar reopen: Reabrir ok: OK light: Claro dark: Escuro system_setting: Definições de sistema default: Padrão reset: Reset tag: Marcador post_lowercase: publicação filter: Filtro ignore: Ignorar submit: Submeter normal: Normal closed: Fechado deleted: Removido deleted_permanently: Deleted permanently pending: Pendente more: Mais view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Procurar Resultados keywords: Palavras-chave options: Opções follow: Seguir following: Seguindo counts: "{{count}} Resultados" counts_loading: "... Results" more: Mais sort_btns: relevance: Relevância newest: Mais recente active: Ativar score: Pontuação more: Mais tips: title: Dicas de Pesquisa Avançada tag: "<1>[tag] pesquisar com um marcador" user: "<1>user:username buscar por autor" answer: "<1>answers:0 perguntas não respondidas" score: "<1>score:3 postagens com mais de 3+ placares" question: "<1>is:question buscar perguntas" is_answer: "<1>is:answer buscar respostas" empty: Não conseguimos encontrar nada.
Tente palavras-chave diferentes ou menos específicas. share: name: Compartilhar copy: Copiar link via: Compartilhar postagem via... copied: Copiado facebook: Compartilhar no Facebook twitter: Share to X cannot_vote_for_self: Você não pode votar na sua própria postagem modal_confirm: title: Erro... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: A sua nova conta está confirmada; você será redirecionado para a página inicial. link: Continuar para a página inicial. oops: Oops! invalid: O link utilizado não funciona mais. confirm_new_email: O seu e-mail foi atualizado. confirm_new_email_invalid: >- Desculpe, este link de confirmação não é mais válido. Talvez o seu e-mail já tenha sido alterado. unsubscribe: page_title: Cancelar subscrição success_title: Cancelamento de inscrição bem-sucedido success_desc: Você foi removido com sucesso desta lista de assinantes e não receberá mais nenhum e-mail nosso. link: Mudar configurações question: following_tags: Seguindo Marcadores edit: Editar save: Salvar follow_tag_tip: Seguir tags to curate a sua lista de perguntas. hot_questions: Perguntas quentes all_questions: Todas Perguntas x_questions: "{{ count }} perguntas" x_answers: "{{ count }} respostas" x_posts: "{{ count }} Posts" questions: Perguntas answers: Respostas newest: Mais recente active: Ativo hot: Popular frequent: Frequente recommend: Recomendado score: Pontuação unanswered: Não Respondido modified: modificado answered: respondido asked: perguntado closed: fechado follow_a_tag: Seguir o marcador more: Mais personal: overview: Visão geral answers: Respostas answer: resposta questions: Perguntas question: pergunta bookmarks: Favoritas reputation: Reputação comments: Comentários votes: Votos badges: Emblemas newest: Mais recente score: Pontuação edit_profile: Editar Perfil visited_x_days: "Visitado {{ count }} dias" viewed: Visualizado joined: Ingressou comma: "," last_login: Visto about_me: Sobre mim about_me_empty: "// Olá, Mundo !" top_answers: Melhores Respostas top_questions: Melhores Perguntas stats: Estatísticas list_empty: Postagens não encontradas.
Talvez você queira selecionar uma guia diferente? content_empty: Nenhum post encontrado. accepted: Aceito answered: respondido asked: perguntado downvoted: voto negativo mod_short: Moderador mod_long: Moderadores x_reputation: reputação x_votes: votos recebidos x_answers: respostas x_questions: perguntas recent_badges: Emblemas recentes install: title: Instalação next: Proximo done: Completo config_yaml_error: Não é possível criar o arquivo config.yaml. lang: label: Por favor Escolha um Idioma db_type: label: Motor do Banco de dados db_username: label: Nome de usuário placeholder: raiz msg: Nome de usuário não pode ser vazio. db_password: label: Senha placeholder: raiz msg: Senha não pode ser vazio. db_host: label: Database Host placeholder: "db:3306" msg: Database Host não pode ser vazio. db_name: label: Database Nome placeholder: resposta msg: Database Nome não pode ser vazio. db_file: label: Database File placeholder: /data/answer.db msg: Database File não pode ser vazio. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Criar config.yaml label: Arquivo config.yaml criado. desc: >- Você pode criar o arquivo <1>config.yaml manualmente no diretório <1>/var/ww/xxx/ e colar o seguinte texto nele. info: Qlique no botão "Próximo" após finalizar. site_information: Informação do Site admin_account: Administrador Conta site_name: label: Site Nome msg: Site Nome não pode ser vazio. msg_max_length: O nome do site deve ter no máximo 30 caracteres. site_url: label: URL do Site text: O endereço do seu site. msg: empty: Site URL não pode ser vazio. incorrect: URL do site está incorreto. max_length: O nome do site deve ter no máximo 512 caracteres. contact_email: label: E-mail par contato text: O endereço de e-mail do contato principal deste site. msg: empty: E-mail par contato não pode ser vazio. incorrect: E-mail par contato incorrect format. login_required: label: Privado switch: É necessário fazer login text: Somente usuários conectados podem acessar esta comunidade. admin_name: label: Nome msg: Nome não pode ser vazio. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Senha text: >- You will need this password to log in. Por favor store it in a secure location. msg: Senha não pode ser vazio. msg_min_length: A senha deve ser ter pelo menos 8 caracteres. msg_max_length: A senha deve ter no máximo 32 caracteres. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: E-mail text: Você precisará deste e-mail para efetuar o login. msg: empty: Email não pode ser vazio. incorrect: O formato do e-mail está incorreto. ready_title: Seu site está pronto ready_desc: >- Se você quiser alterar mais configurações, visite <1>seção de administrador; encontre-o no menu do site. good_luck: "Divirta-se, e boa sorte!" warn_title: Atenção warn_desc: >- O arquivo <1>config.yaml já existe. Se você precisa redefinir algum dos itens de configuração deste arquivo, apague-o primeiro. install_now: Você pode tentar <1>instalando agora. installed: Já instalado installed_desc: >- You appear to have already installed. To reinstall please clear a sua old database tables first. db_failed: Falha ao conectar-se ao banco de dados db_failed_desc: >- This either means that the database information in a sua <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean a sua host's database server is down. counts: views: visualizações votes: votos answers: respostas accepted: Aceito page_error: http_error: Erro HTTP {{ code }} desc_403: Você não possui permissão para acessar esta página. desc_404: Infelizmente esta página não existe. desc_50X: Houve um erro no servidor e não foi possível completar a sua requisição. back_home: Voltar para a página inicial page_maintenance: desc: "Estamos em manutenção, voltaremos em breve." nav_menus: dashboard: Painel contents: Conteúdos questions: Perguntas answers: Respostas users: Usuários badges: Emblemas flags: Marcadores settings: Configurações general: Geral interface: Interface smtp: SMTP branding: Marca legal: Informação legal write: Escrever terms: Terms tos: Termos de Serviços privacy: Privacidade seo: SEO customize: Personalização themes: Temas login: Entrar privileges: Privilégios plugins: Extensões installed_plugins: Plugins instalados apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Bem vindo(a) ao {{site_name}} user_center: login: Entrar qrcode_login_tip: Por favor, utilize {{ agentName }} para escanear o QR code para entrar. login_failed_email_tip: Falha ao entrar, por favor, permita que este aplicativo acesse a informação do seu e-mail antes de tentar novamente. badges: modal: title: Parabéns content: Você ganhou um novo emblema. close: Fechar confirm: Ver emblemas title: Emblemas awarded: Premiado earned_×: Ganhou ×{{ number }} ×_awarded: "{{ number }} premiado" can_earn_multiple: Você pode ganhar isto várias vezes. earned: Ganhou admin: admin_header: title: Administrador dashboard: title: Painel welcome: Bem-vindo ao Admin! site_statistics: Estatísticas do site questions: "Perguntas:" resolved: "Resolvido:" unanswered: "Não Respondido:" answers: "Respostas:" comments: "Comentários:" votes: "Votos:" users: "Usuários:" flags: "Marcadores:" reviews: "Revisão:" site_health: Saúde do site version: "Versão:" https: "HTTPS:" upload_folder: "Upload da pasta:" run_mode: "Mode de execução:" private: Privado public: Público smtp: "SMTP:" timezone: "Fuso horário:" system_info: Informação do sistema go_version: "Versão do Go:" database: "Banco de dados:" database_size: "Tamanho do banco de dados:" storage_used: "Armazenamento usado:" uptime: "Tempo de atividade:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Contato forum: Fórum documents: Documentos feedback: Opinião support: Supporte review: Revisar config: Configurações update_to: Atualizar ao latest: Ultimo check_failed: Falha na verificação "yes": "Sim" "no": "Não" not_allowed: Não permitido allowed: Permitido enabled: Ativo disabled: Disponível writable: Possível escrever not_writable: Não é possível escrever flags: title: Marcadores pending: Pendente completed: Completo flagged: Marcado flagged_type: '{{ type }} sinalizado' created: Criado action: Ação review: Revisar user_role_modal: title: Altere a função do usuário para... btn_cancel: Cancelar btn_submit: Enviar new_password_modal: title: Criar nova senha form: fields: password: label: Senha text: O usuário será desconectado e precisar entrar novamente. msg: A senha precisa ter no mínimo 8-32 caracteres. btn_cancel: Cancelar btn_submit: Enviar edit_profile_modal: title: Editar profile form: fields: display_name: label: Nome no display msg_range: Display name must be 2-30 characters in length. username: label: Nome do usuário msg_range: Username must be 2-30 characters in length. email: label: Correio eletrônico msg_invalid: Correio eletrônico invalido. edit_success: Editado com sucesso btn_cancel: Cancelar btn_submit: Submeter user_modal: title: Adicionar novo usuário form: fields: users: label: Adicionar usuários em massa placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separe "nome, e-mail, senha" com vírgulas. Um usuário por linha. msg: "Por favor insira o e-mail do usuário, um por linha." display_name: label: Nome de exibição msg: O nome de exibição deve ter entre 2 e 30 caracteres. email: label: E-mail msg: E-mail inválido. password: label: Senha msg: A senha precisa ter no mínimo 8-32 caracteres. btn_cancel: Cancelar btn_submit: Enviar users: title: Usuários name: Nome email: E-mail reputation: Reputação created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Estado role: Função action: Ação change: Mudar all: Todos staff: Funcionários more: Mais inactive: Inativo suspended: Suspenso deleted: Removido normal: Normal Moderator: Moderador Admin: Administrador User: Usuário filter: placeholder: "Filtrar por nome, user:id" set_new_password: Configurar nova senha edit_profile: Editar profile change_status: Mudar status change_role: Mudar função show_logs: Mostrar registros add_user: Adicionar usuário deactivate_user: title: Desativar usuários content: Um usuário inativo deve revalidar seu e-mail. delete_user: title: Remover este usuário content: Tem certeza de que deseja excluir este usuário? Isso é permanente! remove: Remover o conteúdo dele label: Remover todas as perguntas, respostas, comentários etc. text: Não marque isso se deseja excluir apenas a conta do usuário. suspend_user: title: Suspender este usuário content: Um usuário suspenso não pode fazer login. label: How long will the user be suspended for? forever: Forever questions: page_title: Perguntas unlisted: Não-listado post: Publicação votes: Votos answers: Respostas created: Criado status: Estado action: Ação change: Mudar pending: Pendente filter: placeholder: "Filtrar por título, question:id" answers: page_title: Respostas post: Publicação votes: Votos created: Criado status: Estado action: Ação change: Mudar filter: placeholder: "Filtrar por título, answer:id" general: page_title: Geral name: label: Site Nome msg: Site name não pode ser vazio. text: "O nome deste site, conforme usado na tag de título." site_url: label: URL do Site msg: Site url não pode ser vazio. validate: Por favor digite uma URL válida. text: O endereço do seu site. short_desc: label: Breve Descrição do site (opcional) msg: Breve Descrição do site não pode ser vazio. text: "Breve descrição, conforme usado na tag de título na página inicial." desc: label: Site Descrição (opcional) msg: Descrição do site não pode ser vazio. text: "Descreva este site em uma única sentença, conforme usado na meta tag de descrição." contact_email: label: E-mail para contato msg: E-mail par contato não pode ser vazio. validate: E-mail par contato não é válido. text: Endereço de e-mail do principal contato responsável por este site. check_update: label: Atualizações de software text: Verificar se há atualizações automaticamente interface: page_title: Interface language: label: Idioma da interface msg: Idioma da Interface não pode ser vazio. text: Idioma da interface do Usuário. Ele mudará quando você atualizar a página. time_zone: label: Fuso horário msg: Fuso horário não pode ser vazio. text: Escolha a cidade no mesmo fuso horário que você. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: E-mail de origem msg: E-mail de origem não pode ser vazio. text: O endereço de e-mail de onde os e-mails são enviados. from_name: label: Nome de origem msg: Nome de origem não pode ser vazio. text: O nome de onde os e-mails são enviados. smtp_host: label: SMTP Host msg: SMTP host não pode ser vazio. text: O seu servidor de e-mails. encryption: label: Criptografia msg: Criptografia não pode ser vazio. text: Para a maioria dos servidores SSL é a opção recomendada. ssl: SSL tls: TLS none: Nenhum smtp_port: label: SMTP Port msg: Porta SMTP deve ser o número 1 ~ 65535. text: The port to a sua mail server. smtp_username: label: SMTP Nome de usuário msg: SMTP username não pode ser vazio. smtp_password: label: SMTP Senha msg: SMTP password não pode ser vazio. test_email_recipient: label: Test Email Recipients text: Forneça o endereço de e-mail que irá receber envios de teste. msg: O e-mail de teste é inválido smtp_authentication: label: Habilitar autenticação title: Autenticação SMTP msg: Autenticação SMTP não pode ser vazio. "yes": "Sim" "no": "Não" branding: page_title: Marca logo: label: Logo (opcional) msg: Logo não pode ser vazio. text: The logo image at the top left of a sua site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (opcional) text: The logo used on mobile version of a sua site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (opcional) msg: Square icon não pode ser vazio. text: Imagem used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (opcional) text: A favicon for a sua site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Informação legal terms_of_service: label: Terms of Service text: "Você pode adicionar termos de conteúdo de serviço aqui. Se você já tem um documento hospedado em outro lugar, forneça a URL completa aqui." privacy_policy: label: Privacy Policy text: "Você pode adicionar termos de conteúdo de serviço aqui. Se você já tem um documento hospedado em outro lugar, forneça a URL completa aqui." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Escrever resposta label: Each user can only write one answer for each question text: "Desative para permitir que os usuários escrevam várias respostas para a mesma pergunta, o que pode fazer com que as respostas fiquem menos focadas." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend Marcadores text: "Os marcadores recomendados serão exibidos na lista dropdown por padrão." msg: contain_reserved: "tags recomendadas não podem conter tags reservadas" required_tag: title: Definir tags necessárias label: Definir "Tags recomendadas" como tags necessárias text: "Every new question must have ao menos one recommend tag." reserved_tags: label: Reserved Marcadores text: "Tags reservadas só podem ser usadas pelo moderador." image_size: label: Tamanho máximo da imagem (MB) text: "O tamanho máximo para upload de imagem." attachment_size: label: Tamanho máximo do anexo (MB) text: "O tamanho máximo para o carregamento de arquivos anexados." image_megapixels: label: Máximo megapíxels da imagem text: "Número máximo de megapixels permitido para uma imagem." image_extensions: label: Extensões de imagens autorizadas text: "Uma lista de extensões de arquivo permitidas para exibição de imagens, separadas por vírgula." attachment_extensions: label: Extensões autorizadas para anexos text: "Uma lista de extensões de arquivo permitidas para carregamento, separar por vírgula. AVISO: permitir o carregamento pode causar problemas de segurança." seo: page_title: SEO permalink: label: Link permanente text: Custom URL structures can improve the usability, and forward-compatibility of a sua links. robots: label: robos.txt text: Isto irá substituir permanentemente quaisquer configurações do site relacionadas. themes: page_title: Temas themes: label: Temas text: Selecionar um tema existente. color_scheme: label: Esquema de cores navbar_style: label: Navbar background style primary_color: label: Cor primária text: Modifica as cores usadas por seus temas layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS e HTML custom_css: label: CSS Personalizado text: > head: label: Cabeçalho text: > header: label: Cabeçalho text: > footer: label: Rodapé text: Isto será inserido antes de </body>. sidebar: label: Barra lateral text: Isto irá inserir na barra lateral. login: page_title: Entrar membership: title: Afiliação label: Permitir novas inscrições text: Desligue para impedir que alguém crie uma nova conta. email_registration: title: Registrar e-mail label: Permitir registro utilizando e-mail text: Desative para impedir que qualquer pessoa crie uma nova conta por e-mail. allowed_email_domains: title: Domínios de e-mail permitidos text: Domínios de e-mail com os quais os usuários devem registrar contas. Um domínio por linha. Ignorado quando vazio. private: title: Privado label: Login requirido text: Somente usuários conectados podem acessar esta comunidade. password_login: title: Login com senha label: Permitir login por e-mail e senha text: "AVISO: Se desativar, você pode ser incapaz de efetuar login se você não tiver configurado anteriormente outro método de login." installed_plugins: title: Extensões instaladas plugin_link: Plugins ampliam e expandem a funcionalidade. Você pode encontrar plugins no <1>Repositório de Plugins. filter: all: Todos active: Ativo inactive: Inativo outdated: Desactualizado plugins: label: Extensões text: Selecionar uma extensão existente. name: Nome version: Versão status: Estado action: Ação deactivate: Desativar activate: Ativado settings: Configurações settings_users: title: Usuários avatar: label: Avatar padrão text: Para usuários sem um avatar personalizado próprio. gravatar_base_url: label: Gravatar Base URL text: URL da API do provedor Gravatar ignorado quando vazio. profile_editable: title: Perfil editável allow_update_display_name: label: Permitir que os usuários mudem seus nomes de exibição allow_update_username: label: Permitem que os usuário mudem seus nomes de usuário allow_update_avatar: label: Permitem que os usuário mudem a sua imagem de perfil allow_update_bio: label: Permitir que os usuários mudem suas descrições allow_update_website: label: Permitir que os usuários mudem seus web-sites allow_update_location: label: Permitir que usuários alterem suas localizações privilege: title: Privilégios level: label: Nível de reputação necessário text: Escolha a reputação necessária para os privilégios msg: should_be_number: o valor de entrada deve ser número number_larger_1: número deve ser igual ou maior que 1 badges: action: Ação active: Ativo activate: Ativado all: Todos awards: Prêmios deactivate: Desativar filter: placeholder: Filtrar por nome, badge:id group: Grupo inactive: Inativo name: Nome show_logs: Mostrar registros status: Status title: Emblemas apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (opcional) empty: não pode ser vazio invalid: é inválido btn_submit: Salvar not_found_props: "Propriedade requerida {{ key }} não encontrada." select: Selecionar page_review: review: Revisar proposed: proposto question_edit: Editar pergunta answer_edit: Editar resposta tag_edit: Editar marcador edit_summary: Editar descrição edit_question: Editar pergunta edit_answer: Editar resposta edit_tag: Editar marcador empty: Nenhuma tarefa de revisão restante. approve_revision_tip: Você aprova esta revisão? approve_flag_tip: Você aprova esta sinalização? approve_post_tip: Você aprova esta publicação? approve_user_tip: Você aprova este usuário? suggest_edits: Edições sugeridas flag_post: Post sinalizado flag_user: Sinalizar usuário queued_post: Publicação na fila queued_user: Usuário na fila filter_label: Tipo reputation: reputação flag_post_type: Sinalizou esta publicação como {{ type }}. flag_user_type: Sinalizou este usuário como {{ type }}. edit_post: Editar publicação list_post: Listar postagem unlist_post: Remover postagem da lista timeline: undeleted: não removido deleted: removido downvote: voto negativo upvote: voto positivo accept: aceito cancelled: cancelado commented: comentado rollback: reversão edited: editado answered: respondido asked: perguntado closed: fechado reopened: reaberto created: criado pin: fixado unpin: desafixado show: listadas hide: não listado title: "Histórico para" tag_title: "Título para" show_votes: "Mostrar Votos" n_or_a: Não aplicável title_for_question: "Título para" title_for_answer: "Título para resposta {{ title }} por {{ author }}" title_for_tag: "Título para marcador" datetime: Data e hora type: Tipo by: Por comment: Comentário no_data: "Não conseguimos encontrar nada." users: title: Usuários users_with_the_most_reputation: Usuários com maior pontuação users_with_the_most_vote: Usuários que mais votaram staffs: Nossos colaboradores reputation: reputação votes: votos prompt: leave_page: Tem a certeza que quer sair desta página? changes_not_save: Suas alterações não podem ser salvas. draft: discard_confirm: Tem certeza que deseja descartar o rascunho? messages: post_deleted: Esta publicação foi removida. post_cancel_deleted: Esta postagem foi restaurada. post_pin: Esta publicação foi fixada. post_unpin: Esta postagem foi desafixada. post_hide_list: Esta postagem foi ocultada da lista. post_show_list: Esta postagem foi exibida à lista. post_reopen: Esta publicação foi re-aberta. post_list: Esta postagem foi listada. post_unlist: Esta publicação foi removida da lista. post_pending: A sua postagem está aguardando revisão. Ela ficará visível depois que for aprovada. post_closed: Esta postagem foi fechada. answer_deleted: Esta resposta foi excluída. answer_cancel_deleted: Esta resposta foi restaurada. change_user_role: O papel deste usuário foi alterado. user_inactive: Este usuário já está inativo. user_normal: Este usuário já está normal. user_suspended: Este usuário foi suspenso. user_deleted: Este usuário foi removido. user_added: User has been added successfully. badge_activated: Este emblema foi ativado. badge_inactivated: Este emblema foi desativado. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/ro_RO.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Succes. unknown: other: Eroare necunoscută. request_format_error: other: Formatul cererii nu este valid. unauthorized_error: other: Neautorizat. database_error: other: Eroare la serverul de date. forbidden_error: other: Interzis. duplicate_request_error: other: Trimitere dublă. action: report: other: Steag edit: other: Editează delete: other: Ștergere close: other: Închide reopen: other: Redeschidere forbidden_error: other: Interzis. pin: other: Fixează hide: other: Dezlistare unpin: other: Anulați fixarea show: other: Listă invite_someone_to_answer: other: Editează undelete: other: Restabilește merge: other: Îmbinare role: name: user: other: Utilizator admin: other: Administrator moderator: other: Moderator description: user: other: Implicit fără acces special. admin: other: Ai puterea deplină de a accesa site-ul. moderator: other: Are acces la toate postările cu excepţia setărilor administratorului. privilege: level_1: description: other: Nivel 1 (mai puțină reputație pentru echipa privată, grup) level_2: description: other: Nivelul 2 (reputație scăzută necesară pentru comunitatea de pornire) level_3: description: other: Nivelul 3 (reputație ridicată necesară pentru comunitatea mature) level_custom: description: other: Nivel personalizat rank_question_add_label: other: Întreabă ceva rank_answer_add_label: other: Scrie răspunsul rank_comment_add_label: other: Scrie comentariu rank_report_add_label: other: Steag rank_comment_vote_up_label: other: Votează comentariul rank_link_url_limit_label: other: Postează mai mult de 2 link-uri simultan rank_question_vote_up_label: other: Votează întrebarea rank_answer_vote_up_label: other: Votează răspunsul rank_question_vote_down_label: other: Votează întrebarea ca negativa rank_answer_vote_down_label: other: Voteaza răspunsul ca negativ rank_invite_someone_to_answer_label: other: Invită pe cineva să răspundă rank_tag_add_label: other: Creează o etichetă nouă rank_tag_edit_label: other: Editați descrierea etichetei (este nevoie de revizuire) rank_question_edit_label: other: Editați altă întrebare (este nevoie de revizuire) rank_answer_edit_label: other: Editați altă întrebare (este nevoie de revizuire) rank_question_edit_without_review_label: other: Editează întrebarea celuilalt fără revizuire rank_answer_edit_without_review_label: other: Editează întrebarea celuilalt fără revizuire rank_question_audit_label: other: Revizuiește editarea întrebărilor rank_answer_audit_label: other: Revizuiește editările răspunsurilor rank_tag_audit_label: other: Revizuiește editarea etichetelor rank_tag_edit_without_review_label: other: Editează descrierea etichetei fără revizuire rank_tag_synonym_label: other: Gestionează sinonimele etichetelor email: other: E-mail e_mail: other: E-mail password: other: Parolă pass: other: Parolă old_pass: other: Parolă actuală original_text: other: Acest articol email_or_password_wrong_error: other: E-mailul și parola nu se potrivesc. error: common: invalid_url: other: URL invalid. status_invalid: other: Stare nevalidă. password: space_invalid: other: Parola nu poate conține spații. admin: cannot_update_their_password: other: Nu vă puteți modifica parola. cannot_edit_their_profile: other: Nu vă puteți modifica profilul. cannot_modify_self_status: other: Nu vă puteți modifica starea. email_or_password_wrong: other: E-mailul și parola nu se potrivesc. answer: not_found: other: Răspunsul nu a fost găsit. cannot_deleted: other: Nu există permisiunea de ștergere. cannot_update: other: Nu există permisiunea de ștergere. question_closed_cannot_add: other: Întrebările sunt închise şi nu pot fi adăugate. content_cannot_empty: other: Conținutul răspunsului nu poate fi gol. comment: edit_without_permission: other: Comentariul nu poate fi editat. not_found: other: Comentariul nu a fost găsit. cannot_edit_after_deadline: other: Comentariul a durat prea mult pentru a fi modificat. content_cannot_empty: other: Conținutul comentariului nu poate fi gol. email: duplicate: other: Email-ul există deja. need_to_be_verified: other: E-mailul trebuie verificat. verify_url_expired: other: Adresa de e-mail verificată a expirat, vă rugăm să retrimiteți e-mailul. illegal_email_domain_error: other: E-mailul nu este permis din acel domeniu de e-mail. Vă rugăm să folosiți altul. lang: not_found: other: Fișierul de limbă nu a fost găsit. object: captcha_verification_failed: other: Captcha este greșit. disallow_follow: other: Nu vă este permis să urmăriți. disallow_vote: other: Nu ai permisiunea de a vota. disallow_vote_your_self: other: Nu poți vota pentru propria ta postare. not_found: other: Obiectul nu a fost găsit. verification_failed: other: Verificarea a eșuat. email_or_password_incorrect: other: E-mailul și parola nu se potrivesc. old_password_verification_failed: other: Verificarea parolei vechi a eșuat new_password_same_as_previous_setting: other: Noua parolă este identică cu cea anterioară. already_deleted: other: Acest articol a fost șters. meta: object_not_found: other: Nu s-a găsit obiectul Meta question: already_deleted: other: Această postare a fost ștearsă. under_review: other: Articolul tău este în așteptare. Acesta va fi vizibil după ce a fost aprobat. not_found: other: Întrebarea nu a fost găsită. cannot_deleted: other: Nu există permisiunea de ștergere. cannot_close: other: Nu există permisiunea de a închide. cannot_update: other: Nu aveți permisiunea de a actualiza. content_cannot_empty: other: Conținutul nu poate fi gol. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Rangul de reputaţie nu îndeplineşte condiţia. vote_fail_to_meet_the_condition: other: Mulțumim pentru feedback. Aveți nevoie cel puțin de reputația {{.Rank}} pentru a vota. no_enough_rank_to_operate: other: Aveți nevoie cel puțin de reputația {{.Rank}} pentru a face asta. report: handle_failed: other: Procesarea raportării a eșuat. not_found: other: Raportul nu a fost găsit. tag: already_exist: other: Eticheta există deja. not_found: other: Eticheta nu a fost găsită. recommend_tag_not_found: other: Eticheta recomandată nu există. recommend_tag_enter: other: Te rugăm să introduci cel puțin o etichetă necesară. not_contain_synonym_tags: other: Nu trebuie să conțină etichete sinonime. cannot_update: other: Nu aveți permisiunea de a actualiza. is_used_cannot_delete: other: Nu puteți șterge o etichetă care este în uz. cannot_set_synonym_as_itself: other: Nu se poate seta sinonimul etichetei curente ca atare. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: Numele nu poate fi o adresă de e-mail. theme: not_found: other: Tema nu a fost găsită. revision: review_underway: other: Nu se poate edita momentan, există o versiune în coada de revizuire. no_permission: other: Nu ai permisiunea de a revizui. user: external_login_missing_user_id: other: Platforma terță nu oferă un Id de utilizator unic, deci nu vă puteți autentifica, contactați administratorul site-ului. external_login_unbinding_forbidden: other: Vă rugăm să setaţi o parolă de conectare pentru contul dumneavoastră înainte de a elimina această autentificare. email_or_password_wrong: other: other: E-mailul și parola nu se potrivesc. not_found: other: Utilizatorul nu a fost găsit. suspended: other: Utilizatorul a fost suspendat. username_invalid: other: Numele de utilizator nu este valid. username_duplicate: other: Numele de utilizator este deja luat. set_avatar: other: Setarea avatarului a eșuat. cannot_update_your_role: other: Nu vă puteți modifica rolul. not_allowed_registration: other: În prezent, site-ul nu este deschis pentru înregistrare. not_allowed_login_via_password: other: În prezent, site-ul nu este permis să se autentifice prin parolă. access_denied: other: Acces Blocat page_access_denied: other: Nu aveți acces la această pauză. add_bulk_users_format_error: other: "{{.Field}} format lângă '{{.Content}}' la linia {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Numărul de utilizatori pe care îi adăugați odată trebuie să fie în intervalul 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Citirea configurației a eșuat database: connection_failed: other: Conexiunea la baza de date a eșuat create_table_failed: other: Crearea tabelului a eșuat install: create_config_failed: other: Nu se poate crea fișierul config.yaml. upload: unsupported_file_format: other: Format de fișier incompatibil. site_info: config_not_found: other: Configurarea site-ului nu a fost găsită. badge: object_not_found: other: Nu s-a găsit obiectul Insignă reason: spam: name: other: nedorite desc: other: Acest post este o reclamă sau un vandalism. Nu este util sau relevant pentru subiectul actual. rude_or_abusive: name: other: nepoliticos sau abuziv desc: other: "O persoană rezonabilă ar considera acest conținut nepotrivit pentru un discurs respectuos." a_duplicate: name: other: un duplicat desc: other: Această întrebare a fost adresată înainte şi are deja un răspuns. placeholder: other: Introduceți link-ul de întrebare existent not_a_answer: name: other: nu este un răspuns desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." no_longer_needed: name: other: nu mai este necesar desc: other: Acest comentariu este învechit, conversaţional sau nu are relevanţă pentru această postare. something: name: other: altceva desc: other: Acest post necesită atenție din partea personalului, din alt motiv nemenționat mai sus. placeholder: other: Spune-ne ce anume vă îngrijorează community_specific: name: other: un motiv specific comunității desc: other: Această întrebare nu corespunde cu ghidul comunității. not_clarity: name: other: are nevoie de detalii sau de claritate desc: other: Această întrebare include în prezent mai multe întrebări. Ar trebui să se concentreze asupra unei singure probleme. looks_ok: name: other: arată OK desc: other: Această postare este bună și nu este de slabă calitate. needs_edit: name: other: are nevoie de editare și am făcut-o desc: other: Îmbunătățește și corectează problemele cu această postare. needs_close: name: other: necesită închidere desc: other: La o întrebare închisă nu poți răspunde, dar poți totuși să o editezi, să o votezi și să o comentezi. needs_delete: name: other: necesită ștergere desc: other: Această postare va fi ștearsă. question: close: duplicate: name: other: nedorite desc: other: Această întrebare a fost adresată înainte şi are deja un răspuns. guideline: name: other: un motiv specific comunității desc: other: Această întrebare nu corespunde cu ghidul comunității. multiple: name: other: necesită detalii sau claritate desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: altceva desc: other: Acest post necesită un alt motiv care nu este listat mai sus. operation_type: asked: other: întrebat answered: other: răspunse modified: other: modificat deleted_title: other: Întrebare ștearsă questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: întrebarea actualizată answer_the_question: other: întrebare răspunsă update_answer: other: răspuns actualizat accept_answer: other: răspuns acceptat comment_question: other: întrebare comentată comment_answer: other: răspuns comentat reply_to_you: other: ți-a răspuns mention_you: other: te-a menționat your_question_is_closed: other: Întrebarea dumneavoastră a fost închisă your_question_was_deleted: other: Întâlnirea dumneavoastră a fost ştearsă your_answer_was_deleted: other: Răspunsul dumneavoastră a fost șters your_comment_was_deleted: other: Contul dumneavoastră a fost șters up_voted_question: other: votează întrebarea down_voted_question: other: votează întrebarea negativ up_voted_answer: other: votează răspunsul down_voted_answer: other: răspuns negativ up_voted_comment: other: votează comentariul invited_you_to_answer: other: te-a invitat să răspunzi earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "[{{.SiteName}}] Confirmați noua dvs. adresă de e-mail" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} a răspuns la întrebarea dvs" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} vă invită să răspundeți" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} a răspuns la întrebarea dvs" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] Întrebare nouă: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Resetare parolă" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Confirmă noul tău cont" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Test de e-mail" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: votat upvoted: other: vot pozitiv downvote: other: vot negativ downvoted: other: vot negativ accept: other: acceptat accepted: other: acceptat edit: other: editează review: queued_post: other: Posturi în așteptare flagged_post: other: Postare marcată suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: Editor desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Contribuții restante în prima lor lună. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Distribuire grozavă desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Cum se formatează desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Înapoi next: Înainte page_title: question: Întrebare questions: Întrebări tag: Etichetă tags: Etichete tag_wiki: etichetă wiki create_tag: Creați etichetă edit_tag: Modificați eticheta ask_a_question: Create Question edit_question: Editați întrebarea edit_answer: Editaţi răspunsul search: Caută posts_containing: Posturi care conțin settings: Setări notifications: Notificări login: Conectează-te sign_up: Înregistrează-te account_recovery: Recuperarea contului account_activation: Activare cont confirm_email: Confirmare e-mail account_suspended: Cont suspendat admin: Administrator change_email: Modifică E-mail install: Instalează Answer upgrade: Actualizare Answer maintenance: Mentenanță website users: Utilizatori oauth_callback: Se procesează http_404: Eroare HTTP 404 http_50X: Eroare HTTP 500 http_403: Eroare HTTP 403 logout: Deconectare posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notificări inbox: Mesaje primite achievement: Realizări new_alerts: Alerte noi all_read: Marchează totul ca fiind citit show_more: Arată mai mult someone: Cineva inbox_type: all: Toate posts: Postări invites: Invitați votes: Voturi answer: Answer question: Question badge_award: Badge suspended: title: Contul dumneavoastră a fost suspendat until_time: "Contul dumneavoastră a fost suspendat până la {{ time }}." forever: Acest utilizator a fost suspendat pentru totdeauna. end: Această întrebare nu corespunde cu ghidul comunității. contact_us: Contactați-ne editor: blockquote: text: Citat bold: text: Bolt chart: text: Diagramă flow_chart: Diagrama fluxului sequence_diagram: Diagrama secvenței class_diagram: Diagrama clasei state_diagram: Diagrama stării entity_relationship_diagram: Diagrama relației entității user_defined_diagram: Diagramă definită de utilizator gantt_chart: Grafic Gantt pie_chart: Grafic circular code: text: Exemplu de cod add_code: Adaugă exemplu de cod form: fields: code: label: Cod msg: empty: Corpul mesajului trebuie să conțină text. language: label: Limbă placeholder: Detectare automată btn_cancel: Anulați btn_confirm: Adaugă formula: text: Formulă options: inline: Formula inline block: Formula blocului heading: text: Titlu options: h1: Titlu 1 h2: Titlu 2 h3: Titlu 3 h4: Titlu 4 h5: Titlu 5 h6: Titlu 6 help: text: Ajutor hr: text: Linie orizontală image: text: Imagine add_image: Adaugă imagine tab_image: Incarca poza form_image: fields: file: label: Fișier imagine btn: Selectați imaginea msg: empty: Fișierul nu poate fi gol. only_image: Sunt permise doar fișierele imagine. max_size: File size cannot exceed {{size}} MB. desc: label: Descriere tab_url: URL-ul imaginii form_url: fields: url: label: URL-ul imaginii msg: empty: URL-ul imaginii nu poate fi gol. name: label: Descriere btn_cancel: Anulați btn_confirm: Adaugă uploading: Se încarcă indent: text: Indentare outdent: text: Outdent italic: text: Accentuare link: text: Hyperlink add_link: Adaugă hiperlink form: fields: url: label: URL msg: empty: URL-ul nu poate fi gol. name: label: Descriere btn_cancel: Anulează btn_confirm: Adaugă ordered_list: text: Listă numerotată unordered_list: text: Listă cu marcatori table: text: Tabelă heading: Titlu cell: Celulă file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: Închid această postare ca... btn_cancel: Anulează btn_submit: Trimiteți remark: empty: Nu poate fi lăsat necompletat. msg: empty: Te rugăm să selectezi un motiv. report_modal: flag_title: Fac un semnal de alarmă pentru a raporta acest post ca... close_title: Închid această postare ca... review_question_title: Revizuiește întrebarea review_answer_title: Revizuiește răspunsul review_comment_title: Revizuiește comentariul btn_cancel: Anulează btn_submit: Trimiteți remark: empty: Nu poate fi lăsat necompletat. msg: empty: Te rugăm să selectezi un motiv. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: Creează o etichetă nouă form: fields: display_name: label: Nume afișat msg: empty: Numele afișat nu poate fi gol. range: Nume afișat până la 35 de caractere. slug_name: label: Slug URL desc: Slug-ul URL pana la 35 de caractere. msg: empty: Slug-ul URL nu poate fi gol. range: Slug-ul URL pana la 35 de caractere. character: URL-ul slug conţine un set de caractere nepermis. desc: label: Descriere revision: label: Versiunea edit_summary: label: Editează sumarul placeholder: >- Explicați pe scurt modificările (ortografie corectată, gramatică fixată, formatare îmbunătățită) btn_cancel: Anulează btn_submit: Trimiteți btn_post: Postează o nouă etichetă tag_info: created_at: Creat edited_at: Editat history: Istoric synonyms: title: Sinonime text: Următoarele etichete vor fi păstrate la empty: Nu s-au găsit sinonime. btn_add: Adaugă un sinonim btn_edit: Editează btn_save: Salvează synonyms_text: Următoarele etichete vor rămâne la delete: title: Șterge această etichetă tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Sunteţi sigur că doriţi să ştergeţi? close: Închide merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Editează eticheta default_reason: Editare etichetă default_first_reason: Adaugă etichetă btn_save_edits: Salvați modificările btn_cancel: Anulați dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, AAAA [at] HH:mm" now: acum x_seconds_ago: "acum {{count}} sec" x_minutes_ago: "acum {{count}} min" x_hours_ago: "acum {{count}} ore" hour: oră day: zi hours: ore days: zile month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Adaugă comentariu reply_to: Raspunde la btn_reply: Răspunde btn_edit: Editează btn_delete: Ștergeți btn_flag: Marcaj btn_save_edits: Salvați modificările btn_cancel: Anulați show_more: "{{count}} alte comentarii" tip_question: >- Utilizați comentariile pentru a solicita mai multe informații sau pentru a sugera îmbunătățiri. Evitați răspunsul la întrebări în comentarii. tip_answer: >- Utilizați comentarii pentru a răspunde la alți utilizatori sau pentru a le notifica modificările. Dacă adăugați informații noi, editați postarea în loc să comentați. tip_vote: Adaugă ceva util postării edit_answer: title: Editaţi răspunsul default_reason: Editați răspunsul default_first_reason: Adăugare răspuns form: fields: revision: label: Revizuire answer: label: Răspuns feedback: characters: conţinutul trebuie să aibă cel puţin 6 caractere. edit_summary: label: Editează sumarul placeholder: >- Explicați pe scurt modificările (ortografie corectată, gramatică fixă, formatare îmbunătățită) btn_save_edits: Salvați modificările btn_cancel: Anulează tags: title: Etichete sort_buttons: popular: Popular name: Nume newest: Cele mai noi button_follow: Urmărește button_following: Urmăriți tag_label: întrebări search_placeholder: Filtrare după numele etichetei no_desc: Această echipă nu are o descriere. more: Mai multe wiki: Wiki ask: title: Create Question edit_title: Editați întrebarea default_reason: Editați întrebarea default_first_reason: Create question similar_questions: Întrebări similare form: fields: revision: label: Revizuire title: label: Titlu placeholder: What's your topic? Be specific. msg: empty: Titlul nu poate fi gol. range: Titlu de până la 150 de caractere body: label: Corp msg: empty: Corpul mesajului trebuie să conțină text. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Etichete msg: empty: Etichetele nu pot fi goale. answer: label: Răspuns msg: empty: Răspunsul nu poate fi gol. edit_summary: label: Editează sumarul placeholder: >- Explicați pe scurt modificările (ortografie corectată, gramatică fixă, formatare îmbunătățită) btn_post_question: Postează întrebarea ta btn_save_edits: Salvați modificările answer_question: Răspundeți la propria întrebare post_question&answer: Postează-ți întrebarea și răspunsul tag_selector: add_btn: Adaugă etichetă create_btn: Creează o etichetă nouă search_tag: Căutare etichetă hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Nicio etichetă potrivită tag_required_text: Etichetă necesară (cel puțin una) header: nav: question: Întrebări tag: Etichete user: Utilizatori badges: Badges profile: Profil setting: Setări logout: Deconectaţi-vă admin: Administrator review: Recenzie bookmark: Semne de carte moderation: Moderare search: placeholder: Caută footer: build_on: Powered by <1> Apache Answer upload_img: name: Schimbare loading: încarcare... pic_auth_code: title: Captcha placeholder: Introdu textul de mai sus msg: empty: Captcha nu poate fi gol. inactive: first: >- Ești aproape gata! Am trimis un e-mail de activare la {{mail}}. Te rugăm să urmezi instrucțiunile din e-mail pentru a-ți activa contul. info: "Dacă nu ajunge, verifică folderul Spam." another: >- Ți-am trimis un alt e-mail de activare la {{mail}}. Poate dura câteva minute până ajuns; asiguraţi-vă că verificaţi folderul Spam. btn_name: Retrimitere link de activare change_btn_name: Schimbați e-mailul msg: empty: Nu poate fi lăsat necompletat. resend_email: url_label: Sunteţi sigur că doriţi să retrimiteţi e-mailul de activare? url_text: De asemenea, puteți da link-ul de activare de mai sus utilizatorului. login: login_to_continue: Conectează-te pentru a continua info_sign: Nu ai un cont? Înregistrează-te info_login: Ai deja un cont? <1>Autentifică-te agreements: Prin înregistrare, ești de acord cu <1>politica de confidențialitate și <3>termenii și condițiile de utilizare. forgot_pass: Ai uitat parola? name: label: Nume msg: empty: Câmpul Nume trebuie completat. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: E-mail msg: empty: Câmpul e-mail nu poate fi gol. password: label: Parolă msg: empty: Parola nu poate fi goală. different: Parolele introduse pe ambele părți sunt incompatibile account_forgot: page_title: Ati uitat parola btn_name: Trimite-mi e-mail de recuperare send_success: >- Dacă un cont corespunde cu {{mail}}, ar trebui să primiți un e-mail cu instrucțiuni despre cum să resetați parola în scurt timp. email: label: E-mail msg: empty: Câmpul e-mail nu poate fi gol. change_email: btn_cancel: Anulați btn_update: Actualizare adresă de e-mail send_success: >- Dacă un cont corespunde cu {{mail}}, ar trebui să primiți un e-mail cu instrucțiuni despre cum să resetați parola în scurt timp. email: label: E-mail nou msg: empty: Câmpul e-mail nu poate fi gol. oauth: connect: Conectează-te cu {{ auth_name }} remove: Elimină {{ auth_name }} oauth_bind_email: subtitle: Adăugați un e-mail de recuperare la contul dvs. btn_update: Actualizare adresă de e-mail email: label: E-mail msg: empty: E-mail-ul nu poate fi gol. modal_title: E-mail deja existent. modal_content: Această adresă de e-mail este deja înregistrată. Sigur doriți să vă conectați la contul existent? modal_cancel: Schimbați e-mailul modal_confirm: Conectează-te la contul existent password_reset: page_title: Resetează parola btn_name: Resetează-mi parola reset_success: >- Ați schimbat cu succes parola; veți fi redirecționat către pagina de conectare. link_invalid: >- Ne pare rău, acest link de resetare a parolei nu mai este valabil. Poate că parola este deja resetată? to_login: Continuă autentificarea în pagină password: label: Parolă msg: empty: Parola nu poate fi goală. length: Lungimea trebuie să fie între 8 și 32 different: Parolele introduse pe ambele părți sunt incompatibile password_confirm: label: Confirmă parola nouă settings: page_title: Setări goto_modify: Du-te pentru a modifica nav: profile: Profil notification: Notificări account: Cont interface: Interfață profile: heading: Profil btn_name: Salvează display_name: label: Nume afișat msg: Numele afișat nu poate fi gol. msg_range: Display name must be 2-30 characters in length. username: label: Nume de utilizator caption: Oamenii te pot menționa ca "@utilizator". msg: Numele de utilizator nu poate fi gol. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Imaginea de profil gravatar: Gravatar gravatar_text: Poți schimba imaginea pe custom: Personalizat custom_text: Poți să încarci imaginea. default: Sistem msg: Te rugăm să încarci un avatar bio: label: Despre mine website: label: Website placeholder: "https://exemplu.com" msg: Format incorect pentru website location: label: Locație placeholder: "Oraş, Ţară" notification: heading: Notificări prin e-mail turn_on: Pornire inbox: label: Notificări primite description: Răspunde la întrebări, comentarii, invitații și multe altele. all_new_question: label: Adauga o intrebare noua description: Primiți notificări despre toate întrebările noi. Până la 50 de întrebări pe săptămână. all_new_question_for_following_tags: label: Toate întrebările noi pentru etichetele următoare description: Primiți notificări despre întrebări noi pentru următoarele etichete. account: heading: Cont change_email_btn: Schimbați e-mailul change_pass_btn: Schimbați parola change_email_info: >- Am trimis un e-mail la acea adresă. Vă rugăm să urmați instrucțiunile de confirmare. email: label: Email new_email: label: New email msg: New email cannot be empty. pass: label: Parolă actuală msg: Parola nu poate fi goală. password_title: Parolă current_pass: label: Parola curentă msg: empty: Parola curentă nu poate fi goală. length: Lungimea trebuie să fie între 8 și 32. different: Cele două parole introduse nu se potrivesc. new_pass: label: Parola nouă pass_confirm: label: Confirmă parola nouă interface: heading: Interfață lang: label: Limba interfeței text: Limba interfeței utilizatorului. Se va schimba atunci când se reîmprospătează pagina. my_logins: title: Autentificările mele label: Autentifică-te sau înregistrează-te pe acest site folosind aceste conturi. modal_title: Elimină autentificarea modal_content: Sunteţi sigur că doriţi să eliminaţi această autentificare din contul dumneavoastră? modal_confirm_btn: Eliminare remove_success: Eliminată cu succes toast: update: actualizare reușită update_password: Parola schimbata cu succes. flag_success: Mulțumim pentru marcare. forbidden_operate_self: Interzis să operezi singur review: Revizuirea ta va arăta după recenzie. sent_success: Trimis cu succes related_question: title: Related answers: răspunsuri linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Persoane întrebate desc: Invită persoane care crezi că știu răspunsul. invite: Invită să răspundă add: Adaugă persoane search: Caută persoane question_detail: action: Acţiune created: Created Asked: Întrebat asked: întrebat update: Modificat Edited: Edited edit: editat commented: commented Views: Văzute Follow: Urmărește Following: Urmăriți follow_tip: Urmărește această întrebare pentru a primi notificări answered: răspunse closed_in: Închis în show_exist: Arată întrebarea existentă. useful: Utilă question_useful: Acest lucru este util și clar question_un_useful: Nu este clar sau nu este util question_bookmark: Marchează această întrebare answer_useful: Este util answer_un_useful: Nu este util answers: title: Răspunsuri score: Scor newest: Cele mai noi oldest: Cel mai vechi btn_accept: Acceptă btn_accepted: Acceptă write_answer: title: Răspunsul tău edit_answer: Editează răspunsul meu existent btn_name: Postează răspunsul tău add_another_answer: Adaugă un alt răspuns confirm_title: Continuă să răspunzi continue: Continuare confirm_info: >-

Sunteţi sigur că doriţi să adăugaţi un alt răspuns?

Puteţi folosi link-ul de editare pentru a perfecţiona şi îmbunătăţi răspunsul existent, în schimb.

empty: Răspunsul nu poate fi gol. characters: conţinutul trebuie să aibă cel puţin 6 caractere. tips: header_1: Îți mulțumim pentru răspuns li1_1: Asigurați-vă că răspundeți la întrebarea. Furnizați detalii și împărtășiți cercetările dvs. li1_2: Faceți o copie de rezervă cu referințe sau experiență personală. header_2: Dar evită... li2_1: Solicită ajutor, caută clarificări sau răspunsuri la alte răspunsuri. reopen: confirm_btn: Redeschide title: Redeschide această postare content: Sunteţi sigur că doriţi să redeschideţi? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Fixează această postare content: Sunteţi sigur că doriţi să fixaţi la nivel global? Acest post va apărea în partea de sus a tuturor listelor de postări. confirm_btn: Fixează delete: title: Șterge această postare question: >- Nu recomandăm ștergerea întrebărilor cu răspunsuri deoarece acest lucru privează viitorii cititori de aceste cunoștințe.

Ștergerea repetată a întrebărilor cu răspuns poate duce la blocarea contului dvs. de a întreba. Sigur doriți să ștergeți? answer_accepted: >-

Nu recomandăm ștergerea răspunsului acceptat deoarece acest lucru privează viitorii cititori de aceste cunoștințe.

Ștergerea repetată a răspunsurilor acceptate poate duce la blocarea contului dvs. de a răspunde. Sigur doriți să ștergeți? other: Sunteţi sigur că doriţi să ştergeţi? tip_answer_deleted: Aceasta postare a fost stearsa undelete_title: Anulează ștergerea acestei postări undelete_desc: Sunteți sigur că doriți să adulați ștergerea? btns: confirm: Confirmați cancel: Anulați edit: Editează save: Salvează delete: Ștergeți undelete: Restabilește list: List unlist: Unlist unlisted: Unlisted login: Autentifică-te signup: Înscrieți-vă logout: Deconectaţi-vă verify: Verificare create: Create approve: Aprobă reject: Respins skip: Treci peste discard_draft: Respingeți draftul pinned: Fixat all: Toate question: Întrebare answer: Răspuns comment: Comentariu refresh: Actualizare resend: Retrimite deactivate: Dezactivare active: Activați suspend: Suspendați unsuspend: Anulează suspendare close: Închide reopen: Redeschide ok: OK light: Luminoasă dark: Întunecată system_setting: Setări de sistem default: Default reset: Resetează tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Rezultatele căutării keywords: Cuvinte cheie options: Opţiuni follow: Urmărește following: Urmăriți counts: "{{count}} rezultatele" counts_loading: "... Results" more: Mai mult sort_btns: relevance: Relevanță newest: Cele mai noi active: Activ score: Scor more: Mai mult tips: title: Sfaturi de căutare avansate tag: "<1>[tag] search with a tag" user: "<1>utilizator:username căutare de către autor" answer: "<1>răspunsuri:0 întrebări fără răspuns" score: "<1>scor:3 postări cu un scor de 3+" question: "<1>este:question întrebări de căutare" is_answer: "<1>este:răspuner răspunsuri la căutare" empty: Nu am putut găsi nimic.
Încearcă cuvinte cheie diferite sau mai puţin specifice. share: name: Distribuiți copy: Copiază linkul via: Distribuie postarea prin... copied: Copiat facebook: Partajează pe Facebook twitter: Share to X cannot_vote_for_self: Nu poți vota pentru propria ta postare. modal_confirm: title: Eroare... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Noul tău cont este confirmat; vei fi redirecționat către pagina de pornire. link: Continuă la pagina principală oops: Oops! invalid: The link you used no longer works. confirm_new_email: E-mailul dvs. a fost actualizat. confirm_new_email_invalid: >- Ne pare rău, acest link de confirmare nu mai este valabil. Poate că e-mailul dvs. a fost deja modificat? unsubscribe: page_title: Dezabonează-te success_title: Dezabonare cu succes success_desc: Ați fost eliminat cu succes din această listă de abonați și nu veți mai primi alte e-mailuri de la noi. link: Modificați setările question: following_tags: Etichete urmărite edit: Editează save: Salvează follow_tag_tip: Urmărește etichetele pentru a curăța lista ta de întrebări. hot_questions: Întrebări importante all_questions: Toate întrebările x_questions: "{{ count }} Întrebări" x_answers: "{{ count }} răspunsuri" x_posts: "{{ count }} Posts" questions: Întrebări answers: Răspunsuri newest: Cele mai noi active: Activ hot: Hot frequent: Frequent recommend: Recommend score: Scor unanswered: Fără răspuns modified: modificat answered: răspunse asked: întrebat closed: închise follow_a_tag: Urmărește o etichetă more: Mai multe personal: overview: Privire de ansamblu answers: Răspunsuri answer: răspuns questions: Întrebări question: întrebare bookmarks: Semne de carte reputation: Reputație comments: Comentarii votes: Voturi badges: Badges newest: Cele mai noi score: Scor edit_profile: Editare profil visited_x_days: "{{ count }} zile vizitate" viewed: Văzute joined: Înscris comma: "," last_login: Văzut about_me: Despre mine about_me_empty: "// Salut, Lumea !" top_answers: Top răspunsuri top_questions: Top Intrebari stats: Statistici list_empty: Nici o postare găsită.
Poate doriţi să selectaţi o filă diferită? content_empty: No posts found. accepted: Acceptat answered: răspunse asked: întrebat downvoted: vot negativ mod_short: MOD mod_long: Moderatori x_reputation: reputație x_votes: voturi primite x_answers: răspunsuri x_questions: întrebări recent_badges: Recent Badges install: title: Installation next: Înainte done: Finalizat config_yaml_error: Nu se poate crea fișierul config.yaml. lang: label: Vă rugăm să selectați limba db_type: label: Motorul Bazei de Date db_username: label: Nume de utilizator placeholder: root msg: Numele de utilizator nu poate fi gol. db_password: label: Parolă placeholder: root msg: Parola nu poate fi goală. db_host: label: Numele serverului de baze de date placeholder: "db:3306" msg: Adresa bazei de date nu poate fi goală. db_name: label: Numele bazei de date placeholder: răspuns msg: Numele bazei de date nu poate fi gol. db_file: label: Fișierul bazei de date placeholder: /data/answer.db msg: Fişierul bazei de date nu poate fi gol. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Crează config.yaml label: Fișierul config.yaml a fost creat. desc: >- Puteți crea fișierul <1>config.yaml manual în directorul <1>/var/wwww/xxx/ și inserați următorul text în el. info: După ce ai făcut asta, apasă butonul "Înainte". site_information: Informatii site admin_account: Contul de admin site_name: label: Numele site-ului msg: Numele site-ului nu poate fi gol. msg_max_length: Numele site-ului trebuie să aibă maximum 30 de caractere lungime. site_url: label: URL-ul site-ului text: Adresa site-ului dvs. msg: empty: URL-ul site-ului nu poate fi gol. incorrect: Format incorect pentru URL-ul site-ului. max_length: URL-ul site-ului trebuie să aibă maximum 512 caractere lungime. contact_email: label: E-mail de contact text: Adresa de e-mail a persoanei cheie responsabile pentru acest site. msg: empty: E-mailul de contact nu poate fi gol. incorrect: E-mail de contact are un format incorect. login_required: label: Privat switch: Autentificare necesară text: Numai utilizatorii autentificați pot accesa această comunitate. admin_name: label: Nume msg: Câmpul Nume trebuie completat. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Parolă text: >- Veți avea nevoie de această parolă pentru autentificare. Vă rugăm să o păstrați într-o locație sigură. msg: Parola nu poate fi goală. msg_min_length: Parola trebuie să aibă cel puțin 8 caractere. msg_max_length: Parola trebuie să aibă cel puțin 32 caractere. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: E-mail text: Veţi avea nevoie de acest e-mail pentru a vă autentifica. msg: empty: Câmpul e-mail nu poate fi gol. incorrect: Campul e-mail are formatul incorect. ready_title: Your site is ready ready_desc: >- Dacă te simți vreodată ca și cum ai schimba mai multe setări, vizitează <1>secțiunea de administrare ; găsește-o în meniul site-ului. good_luck: "Distracție plăcută și noroc!" warn_title: Atenţie warn_desc: >- Fișierul <1>config.yaml există deja. Dacă trebuie să resetați oricare dintre elementele de configurare din acest fișier, vă rugăm să îl ștergeți mai întâi. install_now: Puteți încerca <1>să instalați acum. installed: Deja instalat installed_desc: >- Se pare că ai instalat deja. Pentru a reinstala vă rugăm să ștergeți mai întâi vechile tabele ale bazei de date. db_failed: Conexiunea la baza de date a eșuat db_failed_desc: >- Acest lucru înseamnă fie că baza de date este în configurația ta <1>. config yaml este incorect sau contactul cu serverul bazei de date nu a putut fi stabilit. Acest lucru ar putea însemna că serverul gazdei nu este în funcțiune. counts: views: vizualizări votes: voturi answers: răspunsuri accepted: Acceptat page_error: http_error: HTTP - {{ code }} desc_403: Nu ai permisiunea să accesezi pagina. desc_404: Din păcate, această pagină nu există. desc_50X: Serverul a întâmpinat o eroare și nu a putut finaliza cererea. back_home: Înapoi la pagina principală page_maintenance: desc: "Suntem în mentenanță, ne vom întoarce în curând." nav_menus: dashboard: Panou de control contents: Conţinut questions: Întrebări answers: Răspunsuri users: Utilizatori badges: Badges flags: Marcaj settings: Setări general: General interface: Interfață smtp: SMTP branding: Marcă legal: Juridic write: Scrie terms: Terms tos: Condiții de utilizare privacy: Confidențialitate seo: SEO customize: Personalizează themes: Teme login: Autentifică-te privileges: Privilegii plugins: Extensii installed_plugins: Extensii instalate apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Bun venit la {{site_name}} user_center: login: Autentifică-te qrcode_login_tip: Vă rugăm să folosiți {{ agentName }} pentru a scana codul QR și a vă autentifica. login_failed_email_tip: Autentificare eșuată. Vă rugăm să permiteți acestei aplicații să vă acceseze informațiile de e-mail înainte de a încerca din nou. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Administrator dashboard: title: Panou de control welcome: Welcome to Admin! site_statistics: Statisticile site-ului questions: "Întrebări:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Răspunsuri:" comments: "Comentarii:" votes: "Voturi:" users: "Utilizatori:" flags: "Marcaje:" reviews: "Reviews:" site_health: Site health version: "Versiune:" https: "HTTPS:" upload_folder: "Director încărcare:" run_mode: "Modul de rulare:" private: Privat public: Public smtp: "SMTP:" timezone: "Fusul orar:" system_info: Informaţii despre sistem go_version: "Versiune Go:" database: "Baza de date:" database_size: "Dimensiune bază de date:" storage_used: "Spațiu utilizat:" uptime: "Timpul de funcționare:" links: Links plugins: Pluginuri github: GitHub blog: Blog contact: Contact forum: Forum documents: Ducumente feedback: Feedback support: Suport review: Recenzie config: Configurație update_to: Actualizare la latest: Recente check_failed: Verificarea eșuată "yes": "Da" "no": "Nu" not_allowed: Nu este permis allowed: Permis enabled: Activat disabled: Dezactivat writable: Inscriptibil not_writable: Neinscriptibil flags: title: Steaguri pending: În așteptare completed: Finalizată flagged: Semnalizat flagged_type: Marcat {{ type }} created: Creată action: Actiune review: Recenzie user_role_modal: title: Schimbă rolul utilizatorului la... btn_cancel: Anulați btn_submit: Trimiteți new_password_modal: title: Setați parola nouă form: fields: password: label: Parolă text: Utilizatorul va fi deconectat și trebuie să se conecteze din nou. msg: Parola trebuie să aibă o lungime de 8-32 caractere. btn_cancel: Anulați btn_submit: Trimiteți edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Adaugă un nou utilizator form: fields: users: label: Adăugare utilizator în bloc placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separați “nume, email, parola” cu virgulă. Un utilizator pe linie. msg: "Te rugăm să introduci e-mailul utilizatorului, câte unul pe linie." display_name: label: Nume afișat msg: Display name must be 2-30 characters in length. email: label: E-mail msg: E-mail nu este validă. password: label: Parolă msg: Parola trebuie să aibă o lungime de 8-32 caractere. btn_cancel: Anulați btn_submit: Trimiteți users: title: Utilizatori name: Nume email: E-mail reputation: Reputație created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Stare role: Rol action: Acţiune change: Schimbare all: Toate staff: Personal more: Mai mult inactive: Inactiv suspended: Suspendat deleted: Şters normal: Normal Moderator: Moderator Admin: Administrator User: Utilizator filter: placeholder: "Filtrare după nume, utilizator:id" set_new_password: Setați parola nouă edit_profile: Edit profile change_status: Schimba starea change_role: Schimbare rol show_logs: Arată jurnalele add_user: Adaugă utilizator deactivate_user: title: Dezactivare utilizator content: Un utilizator inactiv trebuie să își revalideze e-mailul. delete_user: title: Ștergeți acest utilizator content: Sunteţi sigur că doriţi să ştergeţi acest utilizator? Acest lucru este permanent! remove: Eliminaţi acest conţinut label: Elimină toate întrebările, răspunsurile, comentariile etc. text: Nu bifați acest lucru dacă doriți să ștergeți doar contul utilizatorului. suspend_user: title: Suspendă acest utilizator content: Un utilizator suspendat nu se poate autentifica. label: How long will the user be suspended for? forever: Forever questions: page_title: Întrebări unlisted: Unlisted post: Postare votes: Voturi answers: Răspunsuri created: Creată status: Stare action: Actiune change: Schimbare pending: În așteptare filter: placeholder: "Filtrează după titlu, întrebare: id" answers: page_title: Răspunsuri post: Postare votes: Voturi created: Creată status: Stare action: Acţiune change: Schimbare filter: placeholder: "Filtrează după titlu, întrebare: id" general: page_title: General name: label: Numele site-ului msg: Numele site-ului nu poate fi gol. text: "Numele acestui site, așa cum este folosit în eticheta de titlu." site_url: label: URL-ul site-ului msg: Url-ul site-ului nu poate fi gol. validate: Introduceți un URL valid. text: Adresa site-ului dvs. short_desc: label: Scurtă descriere a site-ului msg: Scurtă descriere a site-ului nu poate fi goală. text: "Scurtă descriere, așa cum este folosit în eticheta titlu pe website." desc: label: Descriere site msg: Descrierea site-ului nu poate fi goală. text: "Descrie acest site într-o propoziție, așa cum este folosit în tag-ul meta descriere." contact_email: label: E-mail de contact msg: E-mailul de contact nu poate fi gol. validate: E-mailul de contact nu este valid. text: Adresa de e-mail a persoanei cheie responsabile pentru acest site. check_update: label: Software updates text: Automatically check for updates interface: page_title: Interfață language: label: Limba interfeței msg: Limba interfata nu poate fi goala. text: Limba interfeței utilizatorului. Se va schimba atunci când se reîmprospătează pagina. time_zone: label: Fusul orar msg: Fusul orar nu poate fi gol. text: Alege un oraș în același fus orar cu tine. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: De la e-mail msg: Câmpul e-mail nu poate fi gol. text: Adresa de e-mail de la care e-mailurile sunt trimise. from_name: label: Din numele msg: Numele nu poate fi gol. text: Numele de la care sunt trimise e-mailurile. smtp_host: label: Gazda SMTP msg: Gazda SMTP nu poate fi goală. text: Serverul tau de mail. encryption: label: Criptare msg: Câmpul decriptare nu poate fi gol. text: Pentru majoritatea serverelor SSL este opțiunea recomandată. ssl: SSL tls: TLS none: Niciuna smtp_port: label: Portul SMTP msg: Portul SMTP trebuie să fie numărul 1 ~ 65535. text: Portul către serverul de mail. smtp_username: label: Utilizatorul SMTP msg: Numele de utilizator SMTP nu poate fi gol. smtp_password: label: Parola SMTP msg: Parola SMTP nu poate fi goală. test_email_recipient: label: Destinatari de e-mail test text: Furnizați adresa de e-mail care va primi trimiterile de teste. msg: Destinatarii de e-mail de test sunt invalizi smtp_authentication: label: Activare Authenticator title: Autentificare SMTP msg: Autentificarea SMTP nu poate fi goală. "yes": "Da" "no": "Nu" branding: page_title: Marcă logo: label: Logo msg: Logo-ul nu poate fi gol. text: Imaginea logo-ului din stânga sus a site-ului dvs. Utilizaţi o imagine dreptunghiulară largă cu o înălţime de 56 şi un raport de aspect mai mare de 3:1. Dacă nu se completează, textul pentru titlul site-ului va fi afișat. mobile_logo: label: Logo mobil text: Logo-ul folosit pe versiunea mobila a site-ului dvs. Utilizați o imagine dreptunghiulară largă cu o înălțime de 56. Dacă nu o completați, va fi utilizată imaginea din setarea "logo". square_icon: label: Pictogramă pătrată msg: Pictograma pătrată nu poate fi goală. text: Imaginea folosită ca bază pentru pictogramele de metadate. Ar trebui să fie, în mod ideal, mai mare de 512x512. favicon: label: Favicon text: O pictogramă favorită pentru site-ul dvs. Pentru a lucra corect peste un CDN trebuie să fie un png. Va fi redimensionată la 32x32. Dacă este lăsat necompletat, va fi folosită "iconiță pătrată". legal: page_title: Juridic terms_of_service: label: Termeni și condiții text: "Puteți adăuga aici termeni de conținut pentru serviciu. Dacă aveți deja un document găzduit în altă parte, furnizați URL-ul complet aici." privacy_policy: label: Politică de confidențialitate text: "Puteți adăuga aici termeni politicii de confidențialitate. Dacă aveți deja un document găzduit în altă parte, furnizați URL-ul complet aici." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Fiecare utilizator poate scrie doar câte un răspuns pentru fiecare întrebare text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Etichete recomandate text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Fiecare întrebare nouă trebuie să aibă cel puțin o etichetă recomandată." reserved_tags: label: Etichete rezervate text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Legătură permanenta text: Structurile URL personalizate pot îmbunătăți capacitatea de utilizare și compatibilitatea link-urilor tale. robots: label: robots.txt text: Acest lucru va suprascrie permanent orice setări ale site-ului. themes: page_title: Teme themes: label: Teme text: Selectaţi o temă existentă. color_scheme: label: Paletă de culori navbar_style: label: Navbar background style primary_color: label: Culoare primară text: Modifică culorile folosite de temele tale layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS și HTML custom_css: label: CSS personalizat text: > head: label: Cap text: > header: label: Antet text: > footer: label: Subsol text: Se va insera înainte de </body>. sidebar: label: Bară laterală text: Acesta va fi inserat în bara laterală. login: page_title: Autentifică-te membership: title: Calitatea de membru label: Permite înregistrări noi text: Dezactivați pentru a împiedica pe oricine să creeze un cont nou. email_registration: title: Înregistrare e-mail label: Permite înregistrări noi text: Dezactivați pentru a preveni crearea unui cont nou prin e-mail. allowed_email_domains: title: Domenii de e-mail permise text: Domeniile de e-mail cu care utilizatorii trebuie să înregistreze conturi. Un domeniu pe linie. Se ignoră atunci când este gol. private: title: Privat label: Autentificare necesară text: Numai utilizatorii autentificați pot accesa această comunitate. password_login: title: Parola de login label: Permiteți autentificarea prin e-mail și parolă text: "AVERTISMENT: Dacă opriți, este posibil să nu vă puteți conecta dacă nu ați configurat anterior o altă metodă de autentificare." installed_plugins: title: Extensii instalate plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: Toate active: Activ inactive: Inactiv outdated: Învechit plugins: label: Extensii text: Selectați un plugin existent. name: Nume version: Versiune status: Stare action: Acţiune deactivate: Dezactivare activate: Activare settings: Setări settings_users: title: Utilizatori avatar: label: Avatarul implicit text: Pentru utilizatorii fără un avatar personalizat propriu. gravatar_base_url: label: URL de bază Gravatar text: URL-ul bazei API a furnizorului de Gravatar. Ignorat când este gol. profile_editable: title: Profil editabil allow_update_display_name: label: Permite utilizatorilor să își schimbe numele afișat allow_update_username: label: Permite utilizatorilor să își schimbe numele de utilizator allow_update_avatar: label: Permite utilizatorilor să își schimbe imaginea de profil allow_update_bio: label: Permite utilizatorilor să își schimbe propriul lor despre mine allow_update_website: label: Permite utilizatorilor să îşi schimbe website-ul allow_update_location: label: Permite utilizatorilor să își schimbe locația privilege: title: Privilegii level: label: Nivel necesar de reputație text: Alegeți reputația necesară pentru privilegii msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (opțional) empty: nu poate fi lăsat necompletat invalid: nu este valid btn_submit: Salvează not_found_props: "Proprietatea solicitată {{ key }} nu a fost găsită." select: Selectează page_review: review: Recenzie proposed: propus question_edit: Editare întrebări answer_edit: Editările răspunsului tag_edit: Editare etichetă edit_summary: Editează sumarul edit_question: Editați întrebarea edit_answer: Editați răspunsul edit_tag: Editare etichetă empty: Nu au mai rămas sarcini de evaluare. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Tip reputation: reputation flag_post_type: Acest post a fost marcat de {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Editează postarea list_post: Listează postarea unlist_post: Unlist post timeline: undeleted: restabilește deleted: şterse downvote: vot negativ upvote: vot pozitiv accept: acceptat cancelled: anulat commented: comentat rollback: revenire edited: editat answered: răspunse asked: întrebat closed: închise reopened: redeschise created: creat pin: fixat unpin: nefixat show: listă hide: nelistat title: "Istoric pentru" tag_title: "Cronologie pentru" show_votes: "Afișare voturi" n_or_a: N/A title_for_question: "Cronologie pentru" title_for_answer: "Calendarul răspunsului la {{ title }} cu {{ author }}" title_for_tag: "Cronologie pentru etichetă" datetime: Dată și oră type: Tip by: De către comment: Comentariu no_data: "Nu a fost găsit nimic." users: title: Utilizatori users_with_the_most_reputation: Utilizatori cu cele mai mari scoruri ale reputaţiei în această săptămână users_with_the_most_vote: Utilizatorii care au votat cel mai mult săptămâna aceasta staffs: Personalul acestei comunități reputation: reputație votes: voturi prompt: leave_page: Sunteți sigur că doriți să ieșiți din pagina asta? changes_not_save: Modificările nu pot fi salvate. draft: discard_confirm: Ești sigur că vrei să renunți la ciornă? messages: post_deleted: Această postare a fost ștearsă. post_cancel_deleted: This post has been undeleted. post_pin: Această postare a fost fixată. post_unpin: Această postare nu a fost fixată. post_hide_list: Această postare a fost ascunsă din listă. post_show_list: Această postare a fost afișată în listă. post_reopen: Această postare a fost redeschisă. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/ru_RU.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Выполнено. unknown: other: Неизвестная ошибка. request_format_error: other: Формат файла не корректен. unauthorized_error: other: Авторизация не выполнена. database_error: other: Ошибка сервера данных. forbidden_error: other: Доступ запрещен. duplicate_request_error: other: Дублирующая отправка. action: report: other: Пожаловаться edit: other: Редактировать delete: other: Удалить close: other: Закрыть reopen: other: Открыть forbidden_error: other: Доступ запрещен. pin: other: Закрепить hide: other: Убрать unpin: other: Открепить show: other: Список invite_someone_to_answer: other: Редактировать undelete: other: Отменить удаление merge: other: Объединить role: name: user: other: Пользователь admin: other: Администратор moderator: other: Модератор description: user: other: По умолчанию, без специального доступа. admin: other: Имейте все полномочия для доступа к сайту. moderator: other: Имеет доступ ко всем сообщениям, кроме настроек администратора. privilege: level_1: description: other: Уровень 1 (для приватной команды, группы требуется наименьшая репутация) level_2: description: other: Уровень 2 (для стартапа достаточен низкий уровень репутации) level_3: description: other: Уровень 3 (для зрелого сообщества требуется высокая репутация) level_custom: description: other: Настраиваемый уровень rank_question_add_label: other: Задать вопрос rank_answer_add_label: other: Написать ответ rank_comment_add_label: other: Написать комментарий rank_report_add_label: other: Пожаловаться rank_comment_vote_up_label: other: Полезный комментарий rank_link_url_limit_label: other: Опубликовать более 2 ссылок за раз rank_question_vote_up_label: other: Полезный вопрос rank_answer_vote_up_label: other: Полезный ответ rank_question_vote_down_label: other: Бесполезный вопрос rank_answer_vote_down_label: other: Бесполезный ответ rank_invite_someone_to_answer_label: other: Пригласить кого-нибудь ответить rank_tag_add_label: other: Новый тег rank_tag_edit_label: other: Редактировать описание тега (требуется проверка) rank_question_edit_label: other: Редактировать вопрос другого пользователя (требуется проверка) rank_answer_edit_label: other: Редактировать ответ другого пользователя (требуется проверка) rank_question_edit_without_review_label: other: Редактировать вопрос другого пользователя без проверки rank_answer_edit_without_review_label: other: Редактировать ответ другого пользователя без проверки rank_question_audit_label: other: Проверить изменения вопроса rank_answer_audit_label: other: Проверить изменения ответа rank_tag_audit_label: other: Проверить изменения тегов rank_tag_edit_without_review_label: other: Редактировать описание тега без проверки rank_tag_synonym_label: other: Управлять синонимами тегов email: other: Эл. почта e_mail: other: Почта password: other: Пароль pass: other: Пароль old_pass: other: Current password original_text: other: Это сообщение email_or_password_wrong_error: other: Неверное имя пользователя или пароль. error: common: invalid_url: other: Неверная URL. status_invalid: other: Неверный статус. password: space_invalid: other: Пароль не должен содержать пробелы. admin: cannot_update_their_password: other: Вы не можете изменить свой пароль. cannot_edit_their_profile: other: Вы не можете изменять свой профиль. cannot_modify_self_status: other: Вы не можете изменить свой статус. email_or_password_wrong: other: Неверное имя пользователя или пароль. answer: not_found: other: Ответ не найден. cannot_deleted: other: Недостаточно прав для удаления. cannot_update: other: Нет прав для обновления. question_closed_cannot_add: other: Вопросы закрыты и не могут быть добавлены. content_cannot_empty: other: Содержимое ответа не может быть пустым. comment: edit_without_permission: other: Комментарий не может редактироваться. not_found: other: Комментарий не найден. cannot_edit_after_deadline: other: Невозможно редактировать комментарий из-за того, что он был создан слишком давно. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: Адрес электронной почты уже существует. need_to_be_verified: other: Адрес электронной почты должен быть подтвержден. verify_url_expired: other: Срок действия подтверждённого адреса электронной почты истек, пожалуйста, отправьте письмо повторно. illegal_email_domain_error: other: Невозможно использовать email с этим доменом. Пожалуйста, используйте другой. lang: not_found: other: Языковой файл не найден. object: captcha_verification_failed: other: Captcha введена неверно. disallow_follow: other: Вы не можете подписаться. disallow_vote: other: Вы не можете голосовать. disallow_vote_your_self: other: Вы не можете голосовать за собственный отзыв. not_found: other: Объект не найден. verification_failed: other: Проверка не удалась. email_or_password_incorrect: other: Email или пароль не совпадают. old_password_verification_failed: other: Не удалось подтвердить старый пароль new_password_same_as_previous_setting: other: Пароль не может быть таким же как прежний. already_deleted: other: Этот пост был удален. meta: object_not_found: other: Объект мета не найден question: already_deleted: other: Этот пост был удалён. under_review: other: Ваш пост ожидает проверки. Он станет видимым после одобрения. not_found: other: Вопрос не найден. cannot_deleted: other: Недостаточно прав для удаления. cannot_close: other: Нет разрешения на закрытие. cannot_update: other: Нет разрешения на обновление. content_cannot_empty: other: . content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Ранг репутации не соответствует условию. vote_fail_to_meet_the_condition: other: Спасибо за отзыв. Вам нужно как минимум {{.Rank}} репутация для голосования. no_enough_rank_to_operate: other: Для этого вам нужна репутация {{.Rank}}. report: handle_failed: other: Не удалось обработать отчет. not_found: other: Отчет не найден. tag: already_exist: other: Тег уже существует. not_found: other: Тег не найден. recommend_tag_not_found: other: Рекомендуемый тег не существует. recommend_tag_enter: other: Пожалуйста, введите хотя бы один тег. not_contain_synonym_tags: other: Не должно содержать теги синонимы. cannot_update: other: Нет прав для обновления. is_used_cannot_delete: other: Вы не можете удалить метку, которая используется. cannot_set_synonym_as_itself: other: Вы не можете установить синоним текущего тега. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: Поле отправителя не может содержать email адрес. theme: not_found: other: Тема не найдена. revision: review_underway: other: В настоящее время не удается редактировать версию, в очереди на проверку. no_permission: other: Разрешения на пересмотр нет. user: external_login_missing_user_id: other: Сторонняя платформа не предоставляет уникальный идентификатор пользователя, поэтому вы не можете войти в систему, пожалуйста, свяжитесь с администратором веб-сайта. external_login_unbinding_forbidden: other: Пожалуйста, установите пароль для входа в свою учетную запись, прежде чем удалять этот логин. email_or_password_wrong: other: other: Почта и пароль введены неправильно. not_found: other: Пользователь не найден. suspended: other: Пользователь был заблокирован. username_invalid: other: Недопустимое имя пользователя. username_duplicate: other: Имя пользователя уже используется. set_avatar: other: Не удалось установить аватар. cannot_update_your_role: other: Вы не можете изменить свою роль. not_allowed_registration: other: В данный момент регистрация на сайте выключена. not_allowed_login_via_password: other: В настоящее время вход на сайт по паролю отключен. access_denied: other: Доступ запрещен page_access_denied: other: У вас нет доступа к этой странице. add_bulk_users_format_error: other: "Ошибка формата {{.Field}} рядом с '{{.Content}}' в строке {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Количество пользователей, которое Вы добавляете, должно быть в промежутке от 1 до {{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Не удалось прочитать конфигурацию database: connection_failed: other: Ошибка подключения к базе данных create_table_failed: other: Не удалось создать таблицу install: create_config_failed: other: Не удалось создать файл config.yaml. upload: unsupported_file_format: other: Неподдерживаемый формат файла. site_info: config_not_found: other: Конфигурация сайта не найдена. badge: object_not_found: other: Объект бейджа не найден reason: spam: name: other: Спам desc: other: Этот пост является рекламой или вандализмом. Он не полезен и не имеет отношения к текущей теме. rude_or_abusive: name: other: Грубость или оскорбления desc: other: "Человек может посчитать такое содержимое неподходящим для уважительной беседы." a_duplicate: name: other: дубликат desc: other: Этот вопрос уже был задан, и на него уже был получен ответ. placeholder: other: Введите существующую ссылку на вопрос not_a_answer: name: other: это не ответ desc: other: "Это сообщение было опубликовано в качестве ответа, но оно не пытается ответить на вопрос. Возможно, оно должно быть отредактировано, дополнено, быть другим вопросом или удалено навсегда." no_longer_needed: name: other: Не актуально desc: other: Этот комментарий устарел, носит разговорный характер или не имеет отношения к данному сообщению. something: name: other: Прочее desc: other: Этот пост требует внимания администрации по другой причине, не перечисленной выше. placeholder: other: Уточните, что именно Вас беспокоит community_specific: name: other: специфическая для сообщества причина desc: other: Этот вопрос не соответствует рекомендациям сообщества. not_clarity: name: other: нуждается в деталях или ясности desc: other: В настоящее время этот вопрос включает в себя несколько вопросов в одном. Он должен быть сосредоточен только на одной проблеме. looks_ok: name: other: выглядит нормально desc: other: Этот пост хороший и достойного качества. needs_edit: name: other: нуждается в редактировании, и я сделал это desc: other: Устраните проблемы с этим сообщением самостоятельно. needs_close: name: other: требует закрытия desc: other: На закрытый вопрос нельзя ответить, но все равно можно редактировать, голосовать и комментировать. needs_delete: name: other: требует удаления desc: other: Этот пост будет удален. question: close: duplicate: name: other: спам desc: other: Этот вопрос был задан ранее и уже имеет ответ. guideline: name: other: специфическая для сообщества причина desc: other: Этот вопрос не соответствует рекомендациям сообщества. multiple: name: other: нуждается в деталях или ясности desc: other: В настоящее время этот вопрос включает в себя несколько вопросов в одном. Он должен быть сосредоточен только на одной проблеме. other: name: other: прочее desc: other: Для этого поста требуется другая причина, не указанная выше. operation_type: asked: other: вопросы answered: other: отвеченные modified: other: измененные deleted_title: other: Удаленные вопросы questions_title: other: Вопросы tag: tags_title: other: Теги no_description: other: Тег не имеет описания. notification: action: update_question: other: обновленные вопросы answer_the_question: other: отвеченные вопросы update_answer: other: обновленные ответы accept_answer: other: принятые ответы comment_question: other: Прокомментированные ответы comment_answer: other: прокоментированные ответы reply_to_you: other: отвеченные вам mention_you: other: с упоминанием вас your_question_is_closed: other: Ваш вопрос был закрыт your_question_was_deleted: other: Ваш вопрос был удален your_answer_was_deleted: other: Ваш ответ был удален your_comment_was_deleted: other: Ваш комментарий был удален up_voted_question: other: поддержанный вопрос down_voted_question: other: неподдержанный вопрос up_voted_answer: other: ответ "за" down_voted_answer: other: ответ "против" up_voted_comment: other: поддержанный комментарий invited_you_to_answer: other: пригласил вас ответить earned_badge: other: Вы заработали значок "{{.BadgeName}}" email_tpl: change_email: title: other: "[{{.SiteName}}] Подтвердите новый адрес электронной почты" body: other: "Подтвердите свой новый адрес электронной почты для {{.SiteName}}, перейдя по следующей ссылке:
{{.ChangeEmailUrl}}

Если вы не запрашивали это изменение, пожалуйста, проигнорируйте это электронное письмо.

-
Примечание: Данное сообщение является автоматическим, отвечать на него не нужно." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} ответил на ваш вопрос" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nОткрыть {{.SiteName}}

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.

\n\nОтписаться." invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} приглашает вас в Answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Я думаю, что вы можете знать ответ.

\nОткрыть {{.SiteName}}

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.

\n\nОтписаться" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} прокомментировал под вашей публикацией" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nОткрыть {{.SiteName}}

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.

\n\nОтписаться" new_question: title: other: "[{{.SiteName}}] Новый вопрос: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Пароль сброшен" body: other: "Кто-то попросил сбросить ваш пароль на {{.SiteName}}.

\n\nЕсли это не вы, вы можете проигнорировать это письмо.

\n\nПерейдите по следующей ссылке, чтобы выбрать новый пароль:
\n{{.PassResetUrl}}\n

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно." register: title: other: "[{{.SiteName}}] Подтвердите Ваш новый аккаунт" body: other: "Добро пожаловать в {{.SiteName}}!

\n\nПерейдите по следующей ссылке для подтверждения и активации вашей новой учетной записи:
\n{{.RegisterUrl}}

\n\nЕсли ссылка выше не нажата, попробуйте скопировать и вставить её в адресную строку вашего браузера.\n

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно." test: title: other: "[{{.SiteName}}] Проверочное электронное письмо" body: other: "Это тестовое сообщение.\n

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно." action_activity_type: upvote: other: проголосовать за upvoted: other: проголосовано за downvote: other: бесполезный downvoted: other: проголосовано против accept: other: принять accepted: other: принято edit: other: редактировать review: queued_post: other: Задан вопрос flagged_post: other: Отмеченный пост suggested_post_edit: other: Предложенные исправления reaction: tooltip: other: "{{ .Names }} и {{ .Count }} еще..." badge: default_badges: autobiographer: name: other: Автобиограф desc: other: Заполнена информация об профиле. certified: name: other: Сертифицированный desc: other: Завершил наше новое руководство пользователя. editor: name: other: Редактор desc: other: Впервые отредактировать сообщение first_flag: name: other: Первый флаг desc: other: Впервые проставить флаг в сообщения first_upvote: name: other: Первый голос desc: other: Впервые добавить голос в сообщении. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Оставить 5 комментариев. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Прочтите [правила сообщества]. reader: name: other: Читатель desc: other: Прочитать каждый ответ в разделе с более чем 10 ответами. welcome: name: other: Добро пожаловать desc: other: Получен голос «за». nice_share: name: other: Неплохо поделился desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Спасибо desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Активный участник на год, опубликовал по крайней мере один раз. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: 'Форматирование:' desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Назад next: Следующий page_title: question: Вопрос questions: Вопросы tag: Тэг tags: Теги tag_wiki: wiki тэг create_tag: Создать тег edit_tag: Изменить тег ask_a_question: Create Question edit_question: Редактировать вопрос edit_answer: Редактировать ответ search: Поиск posts_containing: Посты содержащие settings: Настройки notifications: Уведомления login: Вход sign_up: Регистрация account_recovery: Восстановление аккаунта account_activation: Активация учётной записи confirm_email: Подтвердить адрес электронной почты account_suspended: Аккаунт заблокирован admin: Управление change_email: Изменить Email install: Установка ответа upgrade: Обновить ответ maintenance: Обслуживание сайта users: Пользователи oauth_callback: Идет обработка http_404: Ошибка HTTP 404 http_50X: Ошибка HTTP 500 http_403: Ошибка HTTP 403 logout: Выйти posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Уведомления inbox: Входящие achievement: Достижения new_alerts: Новые оповещения all_read: Отметить всё как прочитанное show_more: Показать еще someone: Кто-то inbox_type: all: Все posts: Посты invites: Приглашения votes: Голоса answer: Ответ question: Вопрос badge_award: Значок suspended: title: Ваш аккаунт заблокирован until_time: "Ваша учетная запись была заблокирована до {{ time }}." forever: Этот пользователь был навсегда заблокирован. end: Вы не соответствуете правилам сообщества. contact_us: Связаться с нами editor: blockquote: text: Цитата bold: text: Сильный chart: text: Диаграмма flow_chart: Блок-схема sequence_diagram: Диаграмма последовательности class_diagram: Диаграмма классов state_diagram: Диаграмма состояний entity_relationship_diagram: Диаграмма связей сущностей user_defined_diagram: Пользовательская диаграмма gantt_chart: Диаграмма Гантта pie_chart: Круговая диаграмма code: text: Фрагмент кода add_code: Добавить пример кода form: fields: code: label: Код msg: empty: Код не может быть пустым. language: label: Язык placeholder: Автоматический выбор btn_cancel: Отменить btn_confirm: Добавить formula: text: Формула options: inline: Встроенная формула block: Блочная формула heading: text: Заголовок options: h1: Заголовок 1 h2: Заголовок 2 h3: Заголовок 3 h4: Заголовок 4 h5: Заголовок 5 h6: Заголовок 6 help: text: Помощь hr: text: Горизонтальная линия image: text: Изображение add_image: Добавить изображение tab_image: Загрузить изображение form_image: fields: file: label: Файл изображения btn: Выбрать изображение msg: empty: Файл не может быть пустым. only_image: Разрешены только изображения. max_size: Размер файла не может превышать {{size}} МБ. desc: label: Описание tab_url: URL изображения form_url: fields: url: label: URL изображения msg: empty: URL изображения не может быть пустым. name: label: Описание btn_cancel: Отменить btn_confirm: Добавь uploading: Загрузка indent: text: Абзац outdent: text: Уменьшить отступ italic: text: Курсив link: text: Гиперссылка add_link: Вставить гиперссылку form: fields: url: label: URL-адрес msg: empty: URL не может быть пустым. name: label: Описание btn_cancel: Отменить btn_confirm: Добавить ordered_list: text: Нумерованный список unordered_list: text: Маркированный список table: text: Таблица heading: Заголовок cell: Ячейка file: text: Прикрепить файлы not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: Я закрываю этот пост как... btn_cancel: Отменить btn_submit: Сохранить remark: empty: Не может быть пустым. msg: empty: Пожалуйста, выбери причину. report_modal: flag_title: 'Причина жалобы:' close_title: Я закрываю этот пост как... review_question_title: Проверить вопрос review_answer_title: Проверить ответ review_comment_title: Просмотр комментариев btn_cancel: Отмена btn_submit: Сохранить remark: empty: Не может быть пустым. msg: empty: Пожалуйста, выбери причину. not_a_url: Недопустимый формат URL. url_not_match: URL адрес не соответствует текущему веб-сайту. tag_modal: title: Новый тег form: fields: display_name: label: Отображаемое имя msg: empty: Отображаемое название не может быть пустым. range: Отображаемое имя до 35 символов. slug_name: label: URL-адрес тега desc: URL-адрес тега длиной до 35 символов. msg: empty: URL slug не может быть пустым. range: URL slug до 35 символов. character: URL slug содержит недопустимый набор символов. desc: label: Описание revision: label: Версия edit_summary: label: Отредактировать сводку placeholder: >- Коротко опишите изменения (орфография, грамматики, улучшение формата) btn_cancel: Отмена btn_submit: Сохрнаить btn_post: Создать новый тег tag_info: created_at: Создано edited_at: Отредактировано history: История synonyms: title: Синонимы text: Следующие теги будут переназначены на empty: Синонимы не найдены. btn_add: Добавить синоним btn_edit: Редактировать btn_save: Сохранить synonyms_text: Следующие теги будут переназначены на delete: title: Удалить этот тег tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Вы уверены, что хотите удалить? close: Закрыть merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Изменить тег default_reason: Правка тега default_first_reason: Добавить метку btn_save_edits: Сохранить изменения btn_cancel: Отмена dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: сейчас x_seconds_ago: "{{count}}с назад" x_minutes_ago: "{{count}}м назад" x_hours_ago: "{{count}}ч назад" hour: часы day: дней hours: часов days: дней month: month months: months year: year reaction: heart: сердечко smile: smile frown: frown btn_label: добавить или удалить реакции undo_emoji: отменить реакцию {{ emoji }} react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Добавить комментарий reply_to: Ответить на btn_reply: Ответить btn_edit: Редактирование btn_delete: Удалить btn_flag: Пожаловаться btn_save_edits: Сохранить изменения btn_cancel: Отменить show_more: "Еще {{count}} комментарий" tip_question: >- Воспользуйтесь комментариями, чтобы запросить больше информации или предложить улучшения. Не отвечайте на вопросы в комментариях. tip_answer: >- Используйте комментарии для ответа другим пользователям или уведомления об изменениях. Если вы добавляете новую информацию, редактируйте ваше сообщение вместо комментариев. tip_vote: Это добавляет кое-что полезное к сообщению edit_answer: title: Редактировать ответ default_reason: Редактировать ответ default_first_reason: Добавить ответ form: fields: revision: label: Пересмотр answer: label: Ответ feedback: characters: длина пароля должна составлять не менее 6 символов. edit_summary: label: Изменить краткое описание placeholder: >- Кратко опишите вносимые изменения (исправлена орфография, исправлена грамматика, улучшено форматирование) btn_save_edits: Сохранить изменения btn_cancel: Отменить tags: title: Теги sort_buttons: popular: Популярное name: Имя newest: Последние button_follow: Подписаться button_following: Подписки tag_label: вопросы search_placeholder: Фильтр по названию тега no_desc: Тег не имеет описания. more: Подробнее wiki: Wiki ask: title: Create Question edit_title: Редактировать вопрос default_reason: Редактировать вопрос default_first_reason: Create question similar_questions: Похожие вопросы form: fields: revision: label: Версия title: label: Заголовок placeholder: What's your topic? Be specific. msg: empty: Заголовок не может быть пустым. range: Заголовок должен быть меньше 150 символов body: label: 'Вопрос:' msg: empty: Вопрос не может быть пустым. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Теги msg: empty: Теги не могут быть пустыми. answer: label: Ответ msg: empty: Ответ не может быть пустым. edit_summary: label: Изменить краткое описание placeholder: >- Кратко опишите вносимые изменения (исправлена орфография, исправлена грамматика, улучшено форматирование) btn_post_question: Задать вопрос btn_save_edits: Сохранить изменения answer_question: Ответить на свой собственный вопрос post_question&answer: Опубликуйте свой вопрос и ответ tag_selector: add_btn: Тег create_btn: новый тег search_tag: Поиск тега hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Нет соответствующих тэгов tag_required_text: Обязательный тег (хотя бы один) header: nav: question: Вопросы tag: Теги user: Пользователи badges: Значки profile: Профиль setting: Настройки logout: Выйти admin: Управление review: Рецензия bookmark: Закладки moderation: Модерирование search: placeholder: Поиск footer: build_on: Powered by <1> Apache Answer upload_img: name: Изменить loading: загрузка... pic_auth_code: title: Капча placeholder: Введите текст выше msg: empty: Капча не может быть пустой. inactive: first: >- Вы почти закончили! Мы отправили письмо с активацией на адрес {{mail}}. Пожалуйста, следуйте инструкциям в письме, чтобы активировать свою учетную запись. info: "Если оно не пришло, проверьте свою папку со спамом." another: >- Мы отправили вам еще одно электронное письмо с активацией по адресу {{mail}}. Его получение может занять несколько минут; обязательно проверьте папку со спамом. btn_name: Повторно отправить письмо с активацией change_btn_name: Изменить email msg: empty: Не может быть пустым. resend_email: url_label: Вы уверены, что хотите повторно отправить письмо с активацией? url_text: Вы также можете предоставить пользователю ссылку для активации выше. login: login_to_continue: Войдите, чтобы продолжить info_sign: У вас нет аккаунта? <1>Зарегистрируйтесь info_login: Уже есть аккаунт? <1>Войти agreements: Регистрируясь, вы соглашаетесь с <1>политикой конфиденциальности и <3>условиями обслуживания. forgot_pass: Забыли пароль? name: label: Имя пользователя msg: empty: Имя пользователя не должно быть пустым. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Email адрес msg: empty: Адрес электронной почты не может быть пустым. password: label: Пароль msg: empty: Пароль не может быть пустым. different: Введенные пароли не совпадают account_forgot: page_title: Забыли свой пароль btn_name: Отправить мне письмо для восстановления пароля send_success: >- Если учетная запись соответствует {{mail}}, вы должны в ближайшее время получить электронное письмо с инструкциями о том, как сбросить пароль. email: label: Email адрес msg: empty: Адрес электронной почты не может быть пустым. change_email: btn_cancel: Отмена btn_update: Сменить адрес email send_success: >- Если учетная запись соответствует {{mail}}, вы должны в ближайшее время получить электронное письмо с инструкциями о том, как сбросить пароль. email: label: Новый email msg: empty: Email не может быть пустым. oauth: connect: Связаться с {{ auth_name }} remove: Удалить {{ auth_name }} oauth_bind_email: subtitle: Добавьте адрес электронной почты для восстановления учетной записи. btn_update: Сменить адрес email email: label: Электронная почта msg: empty: Адрес электронной почты не может быть пустым. modal_title: Электронная почта уже существует. modal_content: Этот адрес электронной почты уже зарегистрирован. Вы уверены, что хотите подключиться к существующей учетной записи? modal_cancel: Изменить адрес электронной почты modal_confirm: Подключение к существующей учетной записи password_reset: page_title: Сброс пароля btn_name: Сбросить мой пароль reset_success: >- Вы успешно сменили свой пароль; вы будете перенаправлены на страницу входа в систему. link_invalid: >- Извините, эта ссылка для сброса пароля больше недействительна. Возможно, ваш пароль уже сброшен? to_login: Перейдите на страницу входа в систему password: label: Пароль msg: empty: Пароль не может быть пустым. length: Длина должна быть от 8 до 32 different: Введенные пароли не совпадают password_confirm: label: Подтвердите новый пароль settings: page_title: Настройки goto_modify: Перейдите к изменению nav: profile: Профиль notification: Уведомления account: Учетная запись interface: Интерфейс profile: heading: Профиль btn_name: Сохранить display_name: label: Отображаемое имя msg: Отображаемое имя не может быть пустым. msg_range: Display name must be 2-30 characters in length. username: label: Имя пользователя caption: Люди могут упоминать вас как "@username". msg: Имя пользователя не может быть пустым. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Изображение профиля gravatar: Gravatar gravatar_text: Вы можете изменить изображение на custom: Другой custom_text: Вы можете загрузить свое изображение. default: Системные msg: Пожалуйста, загрузите аватар bio: label: Обо мне website: label: Сайт placeholder: "https://example.com" msg: Неправильный формат веб-сайта location: label: Местоположение placeholder: "Город, страна" notification: heading: Уведомления по эл. почте turn_on: Вкл. inbox: label: Email уведомления description: Ответы на ваши вопросы, комментарии, приглашения и многое другое. all_new_question: label: Все новые вопросы description: Получайте уведомления обо всех новых вопросах. До 50 вопросов в неделю. all_new_question_for_following_tags: label: Все новые вопросы для тегов из подписок description: Получайте уведомления о новых вопросах по следующим тегам. account: heading: Учетная запись change_email_btn: Изменить e-mail change_pass_btn: Изменить пароль change_email_info: >- Мы отправили электронное письмо на этот адрес. Пожалуйста, следуйте инструкциям из письма. email: label: Email new_email: label: Новый email msg: Новый email не может быть пустым. pass: label: Текущий пароль msg: Пароль не может быть пустым. password_title: Пароль current_pass: label: Текущий пароль msg: empty: Текущий пароль не может быть пустым. length: Длина должна быть от 8 до 32. different: Введенные пароли не совпадают. new_pass: label: Новый пароль pass_confirm: label: Подтвердите новый пароль interface: heading: Интерфейс lang: label: Язык интерфейса text: Язык пользовательского интерфейса. Он изменится при обновлении страницы. my_logins: title: Мои логины label: Войдите в систему или зарегистрируйтесь на этом сайте, используя эти учетные записи. modal_title: Удаление логина modal_content: Вы уверены, что хотите удалить этот логин из своей учетной записи? modal_confirm_btn: Удалить remove_success: Успешно удалено toast: update: успешное обновление update_password: Пароль успешно изменен. flag_success: Благодарим за отметку. forbidden_operate_self: Запрещено работать с собой review: Ваша версия будет отображаться после проверки. sent_success: Отправлено успешно related_question: title: Related answers: ответы linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Позвать на помощь desc: Выберите людей, которые, по вашему мнению, могут знать ответ. invite: Пригласил вас ответить add: Добавить пользователей search: Поиск людей question_detail: action: Действия created: Created Asked: Спросил(а) asked: спросил(а) update: Изменён Edited: Edited edit: отредактировал commented: commented Views: Просмотрен Follow: Подписаться Following: Подписки follow_tip: Подпишитесь на этот вопрос для получения уведомлений answered: отвеченные closed_in: Закрыто в show_exist: Показать существующий вопрос. useful: Полезный question_useful: Это полезно и понятно question_un_useful: Это непонятно или не полезно question_bookmark: Добавьте этот вопрос в закладки answer_useful: Это полезно answer_un_useful: Это бесполезно answers: title: Ответы score: Оценка newest: Последние oldest: Oldest btn_accept: Принять btn_accepted: Принято write_answer: title: Ваш ответ edit_answer: Редактировать мой существующий ответ btn_name: Ответить add_another_answer: Добавить другой ответ confirm_title: Перейти к ответу continue: Продолжить confirm_info: >-

Вы уверены, что хотите добавить другой ответ?

Вы можете использовать ссылку редактирования для уточнения и улучшения существующего ответа.

empty: Ответ не может быть пустым. characters: длина содержимого должна составлять не менее 6 символов. tips: header_1: Спасибо за ответ li1_1: Пожалуйста, обязательно отвечайте на вопрос. Предоставьте подробности и поделитесь результатами своих исследований. li1_2: Поддерживайте свои высказывания ссылками или личным опытом. header_2: Но избегайте ... li2_1: Просить о помощи, запрашивать уточнения или отвечать на другие ответы. reopen: confirm_btn: Снова открыть title: Открыть повторно этот пост content: Вы уверены, что хотите открыть заново? list: confirm_btn: Список title: List this post content: Are you sure you want to list? unlist: confirm_btn: Убрать из списка title: Unlist this post content: Are you sure you want to unlist? pin: title: Закрепить сообщение content: Вы уверены, что хотите закрепить глобально? Это сообщение появится вверху всех списков сообщений. confirm_btn: Закрепить delete: title: Удалить сообщение question: >- Мы не рекомендуем удалять вопросы с ответами, поскольку это лишает будущих читателей этих знаний.

Повторное удаление вопросов с ответами может привести к блокировке вашей учетной записи. Вы уверены, что хотите удалить? answer_accepted: >- Мы не рекомендуем удалять вопросы с ответами, поскольку это лишает будущих читателей этих знаний.

Повторное удаление вопросов с ответами может привести к блокировке вашей учетной записи. Вы уверены, что хотите удалить? other: Вы уверены, что хотите удалить? tip_answer_deleted: Этот ответ был удален undelete_title: Восстановить сообщение undelete_desc: Вы уверены, что хотите отменить удаление? btns: confirm: Подтвердить cancel: Отменить edit: Редактировать save: Сохранить delete: Удалить undelete: Отменить удаление list: List unlist: Unlist unlisted: Unlisted login: Авторизоваться signup: Регистрация logout: Выйти verify: Подтвердить create: Create approve: Одобрить reject: Отклонить skip: Пропустить discard_draft: Удалить черновик pinned: Закрепленный all: Все question: Вопрос answer: Ответ comment: Комментарий refresh: Обновить resend: Отправить повторно deactivate: Отключить active: Активные suspend: Заблокировать unsuspend: Разблокировать close: Закрыть reopen: Открыть повторно ok: ОК light: Светлая тема dark: Темная тема system_setting: Настройки системы default: По умолчанию reset: Сбросить tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Результаты поиска keywords: Ключевые слова options: Настройки follow: Подписаться following: Подписка counts: "Результатов: {{count}}" counts_loading: "... Results" more: Ещё sort_btns: relevance: По релевантности newest: Последние active: Активные score: Оценки more: Больше tips: title: Советы по расширенному поиску tag: "<1>[tag] search with a tag" user: "<1>user:username поиск по автору" answer: "<1>ответов:0 вопросы без ответов" score: "<1>score:3 записи с рейтингом 3+" question: "<1>is:question поиск по вопросам" is_answer: "<1>ответ поиск ответов" empty: Мы ничего не смогли найти.
Попробуйте другие или менее специфичные ключевые слова. share: name: Поделиться copy: Скопировать ссылку via: Поделитесь постом через... copied: Скопировано facebook: Поделиться на Facebook twitter: Share to X cannot_vote_for_self: Вы не можете проголосовать за свой собственный пост. modal_confirm: title: Ошибка... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Ваша новая учетная запись подтверждена; вы будете перенаправлены на главную страницу. link: Перейти на главную oops: Oops! invalid: The link you used no longer works. confirm_new_email: Ваш адрес электронной почты был обновлен. confirm_new_email_invalid: >- Извините, эта ссылка для подтверждения больше недействительна. Возможно, ваш адрес электронной почты уже был изменен? unsubscribe: page_title: Отписаться success_title: Вы успешно отписались от рассылки success_desc: Вы были успешно удалены из этого списка подписчиков и больше не будете получать от нас никаких электронных писем. link: Изменить настройки question: following_tags: Подписка на теги edit: Редактировать save: Сохранить follow_tag_tip: Подпишитесь на теги, чтобы следить за интересующими темами. hot_questions: Популярные вопросы all_questions: Все вопросы x_questions: "{{ count }} вопросов" x_answers: "{{ count }} ответов" x_posts: "{{ count }} Posts" questions: Вопросы answers: Ответы newest: Последние active: Активные hot: Hot frequent: Frequent recommend: Recommend score: Оценка unanswered: Без ответа modified: изменён answered: отвеченные asked: спросил(а) closed: закрытый follow_a_tag: Следить за тегом more: Подробнее personal: overview: Обзор answers: Ответы answer: ответ questions: Вопросы question: вопрос bookmarks: Закладки reputation: Репутация comments: Комментарии votes: Голоса badges: Badges newest: Последние score: Оценки edit_profile: Редактировать профиль visited_x_days: "Посещено {{ count }} дней" viewed: Просмотрен joined: Присоединился comma: "," last_login: Просмотрен(-а) about_me: О себе about_me_empty: "// Привет, Мир!" top_answers: Лучшие ответы top_questions: Топ вопросов stats: Статистика list_empty: Сообщений не найдено.
Возможно, вы хотели бы выбрать другую вкладку? content_empty: No posts found. accepted: Принято answered: отвеченные asked: спросил downvoted: проголосовано против mod_short: MOD mod_long: Модераторы x_reputation: репутация x_votes: полученные голоса x_answers: ответы x_questions: вопросы recent_badges: Recent Badges install: title: Installation next: Следующий done: Готово config_yaml_error: Не удается создать файл config.yaml. lang: label: Пожалуйста, выберите язык db_type: label: База данных db_username: label: Имя пользователя placeholder: root msg: Имя пользователя не может быть пустым. db_password: label: Пароль placeholder: root msg: Пароль не может быть пустым. db_host: label: Сервер базы данных placeholder: "db:3306" msg: Сервер базы данных не может быть пустым. db_name: label: Название базы данных placeholder: ответ msg: Имя базы данных не может быть пустым. db_file: label: Файл базы данных placeholder: /data/answer.db msg: Файл базы данных не может быть пустым. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Создайте файл config.yaml label: Файл config.yaml создан. desc: >- Вы можете создать файл <1>config.yaml вручную в каталоге <1>/var/wwww/xxx/ и вставить в него следующий текст. info: После этого нажмите на кнопку "Далее". site_information: Информация о сайте admin_account: Администратор site_name: label: Название сайта msg: Название сайта не может быть пустым. msg_max_length: Длина названия сайта должна составлять не более 30 символов. site_url: label: Адрес сайта text: Адрес вашего сайта. msg: empty: URL-адрес сайта не может быть пустым. incorrect: Неверный формат URL-адреса сайта. max_length: Длина URL-адреса сайта должна составлять не более 512 символов. contact_email: label: Контактный адрес электронной почты text: Адрес электронной почты контактного лица, ответственного за этот сайт. msg: empty: Контактный адрес электронной почты не может быть пустым. incorrect: Некорректный формат контактного адреса электронной почты. login_required: label: Приватный switch: Требуется авторизация text: Только зарегистрированные пользователи могут получить доступ к этому сообществу. admin_name: label: Имя msg: Имя не может быть пустым. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Пароль text: >- Этот пароль понадобится вам для входа в систему. Пожалуйста, сохраните его в надежном месте. msg: Пароль не может быть пустым. msg_min_length: Длина пароля должна составлять не менее 8 символов. msg_max_length: Длина пароля должна составлять не более 32 символов. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: Вам понадобится этот адрес электронной почты для входа в систему. msg: empty: Адрес электронной почты не может быть пустым. incorrect: Недопустимый формат e-mail адреса. ready_title: Your site is ready ready_desc: >- Если вам когда-нибудь захочется изменить дополнительные настройки, посетите <1>раздел администратора; найдите его в меню сайта. good_luck: "Получайте удовольствие и удачи!" warn_title: Предупреждение warn_desc: >- Файл <1>config.yaml уже существует. Если вам нужно сбросить любой из элементов конфигурации в этом файле, пожалуйста, удалите его. install_now: Вы можете попробовать <1>установить сейчас. installed: Уже установлено installed_desc: >- Похоже, вы уже установили. Для переустановки, пожалуйста, сначала очистите ваши старые таблицы базы данных. db_failed: Ошибка подключения к базе данных db_failed_desc: >- Это означает, что информация о базе данных в вашем файле <1>config.yaml неверна, либо не удалось установить контакт с сервером базы данных. Это может означать, что сервер базы данных вашего хоста недоступен. counts: views: просмотры votes: голоса answers: ответы accepted: Принято page_error: http_error: Ошибка HTTP {{ code }} desc_403: Нет прав доступа для просмотра этой страницы. desc_404: К сожалению, эта страница не существует. desc_50X: Сервер обнаружил ошибку и не смог выполнить ваш запрос. back_home: Вернуться на главную страницу page_maintenance: desc: "Мы выполняем техническое обслуживание, скоро вернемся." nav_menus: dashboard: Панель управления contents: Содержимое questions: Вопросы answers: Ответы users: Пользователи badges: Badges flags: Отметить settings: Настройки general: Основные interface: Интерфейс smtp: SMTP branding: Фирменное оформление legal: Правовая информация write: Написать terms: Terms tos: Пользовательское Соглашение privacy: Конфиденциальность seo: SEO customize: Настройки интерфейса themes: Темы login: Вход privileges: Привилегии plugins: Плагины installed_plugins: Установленные плагины apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Добро пожаловать на {{site_name}} user_center: login: Вход qrcode_login_tip: Пожалуйста, используйте {{ agentName }} для сканирования QR-кода и входа в систему. login_failed_email_tip: Не удалось войти в систему, пожалуйста, разрешите этому приложению получить доступ к вашей электронной почте, прежде чем повторять попытку. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Администратор dashboard: title: Панель управления welcome: Welcome to Admin! site_statistics: Статистика сайта questions: "Вопросы:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Ответы:" comments: "Комментарии:" votes: "Голоса:" users: "Пользователи:" flags: "Жалобы:" reviews: "Reviews:" site_health: Здоровье сайта version: "Версия:" https: "HTTPS:" upload_folder: "Каталог загрузки:" run_mode: "Режим приватности:" private: Приватный public: Публичные smtp: "SMTP:" timezone: "Часовой пояс:" system_info: Информация о системе go_version: "Версия GO:" database: "База данных:" database_size: "Размер базы данных:" storage_used: "Использовано хранилища: " uptime: "Время работы:" links: Ссылки plugins: Плагины github: GitHub blog: Блог contact: Контакты forum: Форум documents: Документы feedback: Обратная связь support: Поддержка review: Обзор config: Конфигурация update_to: Обновление до latest: Последние check_failed: Проверка не удалась "yes": "Да" "no": "Нет" not_allowed: Запрещено allowed: Разрешено enabled: Включено disabled: Отключено writable: Доступен для записи not_writable: Не доступен для записи flags: title: Жалобы pending: Ожидают completed: Рассмотрены flagged: Жалобы flagged_type: Жалоба {{ type }} created: Создано action: Действие review: На проверку user_role_modal: title: Изменить роль пользователя на... btn_cancel: Отмена btn_submit: Отправить new_password_modal: title: Задать новый пароль form: fields: password: label: Пароль text: Сессия пользователя будет завершена и ему придется повторить вход. msg: Длина пароля должна составлять от 8 до 32 символов. btn_cancel: Отменить btn_submit: Отправить edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Создание новых пользователей form: fields: users: label: Массовое добавление пользователей placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Разделите “name, email, password” запятыми. По одному пользователю в строке. msg: "Пожалуйста, введите адрес электронной почты пользователя, по одному на строку." display_name: label: Отображаемое имя msg: Display name must be 2-30 characters in length. email: label: Email msg: Некорректный email. password: label: Пароль msg: Длина пароля должна составлять от 8 до 32 символов. btn_cancel: Отменить btn_submit: Отправить users: title: Пользователи name: Имя email: Email reputation: Репутация created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Статус role: Роль action: Действия change: Изменить all: Все staff: Сотрудники more: Ещё inactive: Неактивные suspended: Заблокированные deleted: Удаленные normal: Обычный Moderator: Модератор Admin: Администратор User: Пользователь filter: placeholder: "Фильтровать по имени, user:id" set_new_password: Задать новый пароль edit_profile: Edit profile change_status: Изменить статус change_role: Изменить роль show_logs: Показать логи add_user: Добавить пользователя deactivate_user: title: Деактивировать пользователя content: Неактивный пользователь должен будет повторно подтвердить свою электронную почту. delete_user: title: Удалить этого пользователя content: Вы уверены, что хотите удалить этого пользователя? Это действие необратимо! remove: Удалить контент пользователя (опционально) label: Удалить все вопросы, ответы, комментарии и т.д. text: Не устанавливайте этот флажок, если вы хотите удалить только учетную запись пользователя. suspend_user: title: Заблокировать этого пользователя content: Заблокированный пользователь не сможет войти. label: How long will the user be suspended for? forever: Forever questions: page_title: Вопросы unlisted: Unlisted post: Публикация votes: Голоса answers: Ответы created: Создан status: Статус action: Действие change: Изменить pending: Ожидают filter: placeholder: "Фильтровать по заголовку, question:id" answers: page_title: Ответы post: Публикация votes: Голоса created: Создан status: Статус action: Действие change: Изменить filter: placeholder: "Фильтровать по заголовку, answer:id" general: page_title: Основные name: label: Название сайта msg: Название сайта не может быть пустым. text: "Название сайта, используемое в теге title." site_url: label: URL-адрес сайта msg: URL-адрес сайта не может быть пустым. validate: Пожалуйста, введите корректный URL. text: Адрес вашего сайта. short_desc: label: Краткое описание msg: Краткое описание сайта не может быть пустым. text: "Краткое описание, используемое в теге заголовка на домашней странице." desc: label: Описание сайта msg: Описание сайта не может быть пустым. text: "Опишите этот сайт одним предложением, как используется в теге meta description" contact_email: label: Контактный адрес электронной почты msg: Контактный адрес электронной почты не может быть пустым. validate: Контактный адрес электронной почты не может быть пустым. text: Адрес электронной почты контактного лица, ответственного за данный сайт. check_update: label: Обновления программного обеспечения text: Автоматически проверять наличие обновлений interface: page_title: Интерфейс language: label: Язык интерфейса msg: Язык интерфейса не может быть пустым. text: Язык пользовательского интерфейса. Он изменится при обновлении страницы. time_zone: label: Часовой пояс msg: Часовой пояс не может быть пустым. text: Выберите город в том же часовом поясе, что и вы. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: С эл. почты msg: Адрес электронной почты отправителя не может быть пустым. text: Адрес электронной почты, с которого отправляются письма. from_name: label: Имя отправителя msg: Имя пользователя не может быть пустым. text: Имя, с которого отправляются электронные письма. smtp_host: label: Сервер SMTP msg: Сервер SMTP не может быть пустым. text: Ваш почтовый сервер. encryption: label: Шифрование msg: Шифрование не может быть пустым. text: Для большинства серверов рекомендуется использовать протокол SSL. ssl: SSL tls: TLS none: Нет smtp_port: label: Порт SMTP msg: Порт SMTP должен быть числом 1 ~ 65535. text: Порт для вашего почтового сервера. smtp_username: label: Имя пользователя SMTP msg: Имя пользователя SMTP не может быть пустым. smtp_password: label: Пароль SMTP msg: Пароль SMTP не может быть пустым. test_email_recipient: label: Тестовые получатели электронной почты text: Укажите адрес электронной почты, на который будут отправляться тестовые сообщения. msg: Некорректный тестовый адрес электронной почты smtp_authentication: label: Включить авторизацию title: Аутентификация SMTP msg: Аутентификационные данные для SMTP не могут быть пустыми. "yes": "Да" "no": "Нет" branding: page_title: Фирменное оформление logo: label: Логотип msg: Логотип не может быть пустым. text: Изображение логотипа в левом верхнем углу вашего сайта. Используйте широкое прямоугольное изображение высотой 56 см с соотношением сторон более 3:1. Если оставить поле пустым, будет показан текст заголовка сайта. mobile_logo: label: Мобильный логотип text: Логотип, используемый в мобильной версии вашего сайта. Используйте широкое прямоугольное изображение высотой 56. Если оставить пустым, будет использоваться изображение из настройки "Логотип". square_icon: label: Квадратный значок msg: Square icon не может быть пустым. text: Изображение, используемое в качестве основы для значков метаданных. В идеале должно быть больше 512x512. favicon: label: Иконка text: Значок для вашего сайта. Для корректной работы через CDN он должен быть в формате png. Размер будет изменен до 32x32. Если оставить пустым, будет использоваться "square icon". legal: page_title: Правовая информация terms_of_service: label: Условия использования text: "Вы можете добавить содержимое условий предоставления услуг здесь. Если у вас уже есть документ, размещенный в другом месте, укажите полный URL-адрес здесь." privacy_policy: label: Условия конфиденциальности text: "Вы можете добавить содержание политики конфиденциальности здесь. Если у вас уже есть документ, размещенный в другом месте, укажите полный URL-адрес здесь." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Каждый пользователь может написать только один ответ на каждый вопрос text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Рекомендованные теги text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Каждый новый вопрос должен иметь хотя бы один рекомендуемый тег." reserved_tags: label: Зарезервированные теги text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Постоянная ссылка text: Пользовательские структуры URL-адресов могут улучшить удобство использования и обратную совместимость ваших ссылок. robots: label: robots.txt text: Это приведет к необратимому переопределению всех связанных настроек сайта. themes: page_title: Темы themes: label: Темы text: Выберите существующую тему. color_scheme: label: Цветовая схема navbar_style: label: Navbar background style primary_color: label: Основной цвет text: Измените цвета, используемые в ваших темах layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS и HTML custom_css: label: Пользовательский CSS text: > head: label: Head text: > header: label: Header text: > footer: label: Нижняя панель text: Это будет вставлено перед </body>. sidebar: label: Боковая панель text: Это будет вставлено в боковую панель. login: page_title: Авторизоваться membership: title: Участие в сообществах label: Разрешить новые регистрации text: Отключите, чтобы никто не мог создать новую учетную запись. email_registration: title: Регистрация по электронной почте label: Разрешить регистрацию по электронной почте text: Отключите, чтобы предотвратить создание новой учетной записи через электронную почту. allowed_email_domains: title: Разрешенные домены электронной почты text: Домены электронной почты, с которыми пользователи должны регистрировать аккаунты. Один домен на каждой строке. Игнорируется, если пусто. private: title: Приватный label: Требуется авторизация text: Только зарегистрированные пользователи могут получить доступ к этому сообществу. password_login: title: Вход в пароль label: Разрешить вход по паролю text: "Предупреждение: При отключении, вы не сможете войти, если ранее не настроили другой способ входа." installed_plugins: title: Установленные плагины plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: Все active: Активные inactive: Неактивные outdated: Устаревшие plugins: label: Плагины text: Выберите существующий плагин. name: Название version: Версия status: Статус action: Действие deactivate: Деактивировать activate: Активировать settings: Настройки settings_users: title: Пользователи avatar: label: Аватар по умолчанию text: Для пользователей, у которых нет собственного пользовательского аватара. gravatar_base_url: label: Базовый URL Gravatar text: URL базы API провайдера Gravatar. Игнорируется, если пусто. profile_editable: title: Настройки профилей allow_update_display_name: label: Разрешить пользователям изменять отображаемое имя allow_update_username: label: Разрешить пользователям изменять свой username allow_update_avatar: label: Разрешить пользователям изменять изображение своего профиля allow_update_bio: label: Разрешить пользователям изменять свои сведения в поле "обо мне" allow_update_website: label: Разрешить пользователям изменять свой веб-сайт allow_update_location: label: Разрешить пользователям изменять свое местоположение privilege: title: Привилегии level: label: Необходимый уровень репутации text: Выберите количество репутации, необходимое для получения привилегий msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (опционально) empty: не может быть пустым invalid: недействителен btn_submit: Сохранить not_found_props: "Требуемое свойство {{ key }} не найдено." select: Select page_review: review: На проверку proposed: предложенный question_edit: Редактировать вопрос answer_edit: Редактирование ответа tag_edit: Редактирование тега edit_summary: Редактирование краткого описания edit_question: Редактирование вопроса edit_answer: Редактирование ответа edit_tag: Редактирование тега empty: Нет задач для проверки. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Предложенные исправления flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: репутация flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: Восстановлен deleted: Удаленные downvote: бесполезный upvote: оценить accept: принять cancelled: отменен commented: прокомментированный rollback: откатить edited: отредактированный answered: отвеченные asked: asked closed: закрытый reopened: Открыт повторно created: созданный pin: закрепленный unpin: незакреплённые show: listed hide: unlisted title: "History for" tag_title: "Хронология" show_votes: "Show votes" n_or_a: Недоступно title_for_question: "Хронология" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Дата и время type: Тип by: Автор comment: Комментарий no_data: "Ничего не найдено." users: title: Пользователи users_with_the_most_reputation: Пользователи с самой высокой репутацией на этой неделе users_with_the_most_vote: Пользователи, которые больше всего проголосовали на этой неделе staffs: Сотрудники нашего сообщества reputation: репутация votes: голоса prompt: leave_page: Вы уверены, что хотите покинуть страницу? changes_not_save: Ваши изменения могут не быть сохранены. draft: discard_confirm: Вы уверены, что хотите отказаться от своего черновика? messages: post_deleted: Этот пост был удалён. post_cancel_deleted: This post has been undeleted. post_pin: Этот пост был закреплен. post_unpin: Этот пост был откреплен. post_hide_list: Это сообщение было скрыто из списка. post_show_list: Этот пост был показан в списке. post_reopen: Этот пост был вновь открыт. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/sk_SK.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Úspech. unknown: other: Neznáma chyba. request_format_error: other: Formát žiadosti nie je platný. unauthorized_error: other: Neoprávnené. database_error: other: Chyba dátového servera. forbidden_error: other: Forbidden. duplicate_request_error: other: Duplicate submission. action: report: other: Flag edit: other: Edit delete: other: Delete close: other: Close reopen: other: Reopen forbidden_error: other: Forbidden. pin: other: Pin hide: other: Unlist unpin: other: Unpin show: other: List invite_someone_to_answer: other: Edit undelete: other: Undelete merge: other: Merge role: name: user: other: Užívateľ admin: other: Správca moderator: other: Moderátor description: user: other: Predvolené bez špeciálneho prístupu. admin: other: Má plnú moc a prístup ku stránke. moderator: other: Má prístup ku všetkým príspevkom okrem nastavenia správcu. privilege: level_1: description: other: Level 1 (less reputation required for private team, group) level_2: description: other: Level 2 (low reputation required for startup community) level_3: description: other: Level 3 (high reputation required for mature community) level_custom: description: other: Custom Level rank_question_add_label: other: Ask question rank_answer_add_label: other: Write answer rank_comment_add_label: other: Write comment rank_report_add_label: other: Flag rank_comment_vote_up_label: other: Upvote comment rank_link_url_limit_label: other: Post more than 2 links at a time rank_question_vote_up_label: other: Upvote question rank_answer_vote_up_label: other: Upvote answer rank_question_vote_down_label: other: Downvote question rank_answer_vote_down_label: other: Downvote answer rank_invite_someone_to_answer_label: other: Invite someone to answer rank_tag_add_label: other: Create new tag rank_tag_edit_label: other: Edit tag description (need to review) rank_question_edit_label: other: Edit other's question (need to review) rank_answer_edit_label: other: Edit other's answer (need to review) rank_question_edit_without_review_label: other: Edit other's question without review rank_answer_edit_without_review_label: other: Edit other's answer without review rank_question_audit_label: other: Review question edits rank_answer_audit_label: other: Review answer edits rank_tag_audit_label: other: Review tag edits rank_tag_edit_without_review_label: other: Edit tag description without review rank_tag_synonym_label: other: Manage tag synonyms email: other: E-mail e_mail: other: Email password: other: Heslo pass: other: Password old_pass: other: Current password original_text: other: This post email_or_password_wrong_error: other: E-mail a heslo sa nezhodujú. error: common: invalid_url: other: Invalid URL. status_invalid: other: Invalid status. password: space_invalid: other: Password cannot contain spaces. admin: cannot_update_their_password: other: Svoje heslo upraviť. cannot_edit_their_profile: other: You cannot modify your profile. cannot_modify_self_status: other: Nemôžete upraviť svoj stav. email_or_password_wrong: other: E-mail a heslo sa nezhodujú. answer: not_found: other: Odpoveď sa nenašla. cannot_deleted: other: Žiadne povolenie na odstránenie. cannot_update: other: Žiadne povolenie na aktualizáciu. question_closed_cannot_add: other: Questions are closed and cannot be added. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Komentár nie je dovolené upravovať. not_found: other: Komentár sa nenašiel. cannot_edit_after_deadline: other: Čas na úpravu komentára bol príliš dlhý. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: E-mail už existuje. need_to_be_verified: other: E-mail by sa mal overiť. verify_url_expired: other: Platnosť overenej adresy URL e-mailu vypršala, pošlite e-mail znova. illegal_email_domain_error: other: Email is not allowed from that email domain. Please use another one. lang: not_found: other: Jazykový súbor sa nenašiel. object: captcha_verification_failed: other: Captcha zle. disallow_follow: other: Nemáte dovolené sledovať. disallow_vote: other: Nemáte povolené hlasovať. disallow_vote_your_self: other: Nemôžete hlasovať za svoj vlastný príspevok. not_found: other: Objekt sa nenašiel. verification_failed: other: Overenie zlyhalo. email_or_password_incorrect: other: E-mail a heslo sa nezhodujú. old_password_verification_failed: other: Overenie starého hesla zlyhalo new_password_same_as_previous_setting: other: Nové heslo je rovnaké ako predchádzajúce. already_deleted: other: This post has been deleted. meta: object_not_found: other: Meta object not found question: already_deleted: other: Tento príspevok bol odstránený. under_review: other: Your post is awaiting review. It will be visible after it has been approved. not_found: other: Otázka sa nenašla. cannot_deleted: other: Žiadne povolenie na odstránenie. cannot_close: other: Žiadne povolenie na uzavretie. cannot_update: other: Žiadne povolenie na aktualizáciu. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. vote_fail_to_meet_the_condition: other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. no_enough_rank_to_operate: other: You need at least {{.Rank}} reputation to do this. report: handle_failed: other: Spracovanie prehľadu zlyhalo. not_found: other: Hlásenie sa nenašlo. tag: already_exist: other: Značka už existuje. not_found: other: Značka sa nenašla. recommend_tag_not_found: other: Recommend tag is not exist. recommend_tag_enter: other: Zadajte aspoň jednu požadovanú značku. not_contain_synonym_tags: other: Nemal by obsahovať synonymické značky. cannot_update: other: Žiadne povolenie na aktualizáciu. is_used_cannot_delete: other: You cannot delete a tag that is in use. cannot_set_synonym_as_itself: other: Synonymum aktuálnej značky nemôžete nastaviť ako samotnú. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: The from name cannot be a email address. theme: not_found: other: Téma sa nenašla. revision: review_underway: other: Momentálne nie je možné upravovať, vo fronte na kontrolu je verzia. no_permission: other: No permission to revise. user: external_login_missing_user_id: other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. external_login_unbinding_forbidden: other: Please set a login password for your account before you remove this login. email_or_password_wrong: other: other: E-mail a heslo sa nezhodujú. not_found: other: Používateľ nenájdený. suspended: other: Používateľ bol pozastavený. username_invalid: other: Používateľské meno je neplatné. username_duplicate: other: Používateľské meno sa už používa. set_avatar: other: Nastavenie avatara zlyhalo. cannot_update_your_role: other: Svoju rolu nemôžete zmeniť. not_allowed_registration: other: Currently the site is not open for registration. not_allowed_login_via_password: other: Currently the site is not allowed to login via password. access_denied: other: Access denied page_access_denied: other: You do not have access to this page. add_bulk_users_format_error: other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Read Config zlyhal database: connection_failed: other: Databázové pripojenie zlyhalo create_table_failed: other: Vytvorenie tabuľky zlyhalo install: create_config_failed: other: Nie je možné vytvoriť súbor config.yaml. upload: unsupported_file_format: other: Nepodporovaný formát súboru. site_info: config_not_found: other: Site config not found. badge: object_not_found: other: Badge object not found reason: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude_or_abusive: name: other: rude or abusive desc: other: "A reasonable person would find this content inappropriate for respectful discourse." a_duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. placeholder: other: Enter the existing question link not_a_answer: name: other: not an answer desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." no_longer_needed: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. something: name: other: something else desc: other: This post requires staff attention for another reason not listed above. placeholder: other: Let us know specifically what you are concerned about community_specific: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. not_clarity: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. looks_ok: name: other: looks OK desc: other: This post is good as-is and not low quality. needs_edit: name: other: needs edit, and I did it desc: other: Improve and correct problems with this post yourself. needs_close: name: other: needs close desc: other: A closed question can't answer, but still can edit, vote and comment. needs_delete: name: other: needs delete desc: other: This post will be deleted. question: close: duplicate: name: other: nevyžiadaná pošta desc: other: Táto otázka už bola položená a už má odpoveď. guideline: name: other: dôvod špecifický pre komunitu desc: other: Táto otázka nespĺňa pokyny pre komunitu. multiple: name: other: potrebuje podrobnosti alebo jasnosť desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: niečo iné desc: other: Tento príspevok vyžaduje iný dôvod, ktorý nie je uvedený vyššie. operation_type: asked: other: požiadaný answered: other: zodpovedaný modified: other: upravený deleted_title: other: Deleted question questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: aktualizovaná otázka answer_the_question: other: zodpovedaná otázka update_answer: other: aktualizovaná odpoveď accept_answer: other: prijatá odpoveď comment_question: other: komentovaná otázka comment_answer: other: komentovaná odpoveď reply_to_you: other: odpovedal vám mention_you: other: spomenul vás your_question_is_closed: other: Vaša otázka bola uzavretá your_question_was_deleted: other: Vaša otázka bola odstránená your_answer_was_deleted: other: Vaša odpoveď bola odstránená your_comment_was_deleted: other: Váš komentár bol odstránený up_voted_question: other: upvoted question down_voted_question: other: downvoted question up_voted_answer: other: upvoted answer down_voted_answer: other: downvoted answer up_voted_comment: other: upvoted comment invited_you_to_answer: other: invited you to answer earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "[{{.SiteName}}] Confirm your new email address" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n

{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Password reset" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Confirm your new account" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Test Email" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: upvote upvoted: other: upvoted downvote: other: downvote downvoted: other: downvoted accept: other: accept accepted: other: accepted edit: other: edit review: queued_post: other: Queued post flagged_post: other: Flagged post suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: Editor desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Ako formátovať desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Predch next: Ďalšie page_title: question: Otázka questions: Otázky tag: Značka tags: Značky tag_wiki: značka wiki create_tag: Vytvoriť štítok edit_tag: Upraviť značku ask_a_question: Create Question edit_question: Úpraviť otázku edit_answer: Úpraviť odpoveť search: Vyhľadávanie posts_containing: Príspevky obsahujúce settings: Nastavenie notifications: Oznámenia login: Prihlásiť sa sign_up: Prihlásiť Se account_recovery: Obnovenie účtu account_activation: Aktivácia účtu confirm_email: Potvrď e-mail account_suspended: Účet pozastavený admin: Administrátor change_email: Upraviť e-mail install: Odpoveď Inštalácia upgrade: Answer Upgrade maintenance: Údržba webových stránok users: Užívatelia oauth_callback: Processing http_404: HTTP chyba 404 http_50X: HTTP chyba 403 http_403: HTTP Error 403 logout: Log Out posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Oznámenia inbox: Doručená pošta achievement: Úspechy new_alerts: New alerts all_read: Označiť všetko ako prečítané show_more: Zobraziť viac someone: Someone inbox_type: all: All posts: Posts invites: Invites votes: Votes answer: Answer question: Question badge_award: Badge suspended: title: Váš účet bol pozastavený until_time: "Váš účet bol pozastavený do {{ time }}." forever: Tento používateľ bol navždy pozastavený. end: Nespĺňate pokyny pre komunitu. contact_us: Contact us editor: blockquote: text: Blockquote bold: text: Silný chart: text: Rebríček flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Ganttov diagram pie_chart: Koláčový graf code: text: Code Sample add_code: Add code sample form: fields: code: label: Kód msg: empty: Code cannot be empty. language: label: Jazyk placeholder: Automatic detection btn_cancel: Zrušiť btn_confirm: Pridať formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Pomoc hr: text: Horizontal rule image: text: Obrázok add_image: Pridať obrázok tab_image: Nahrať obrázok form_image: fields: file: label: Image file btn: Vyberte obrázok msg: empty: Názov súboru nemôže byť prázdny. only_image: Povolené sú iba obrázkové súbory. max_size: File size cannot exceed {{size}} MB. desc: label: Popis tab_url: URL obrázka form_url: fields: url: label: URL obrázka msg: empty: URL obrázka nemôže byť prázdna. name: label: Description btn_cancel: Zrušiť btn_confirm: Pridať uploading: Nahráva sa indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hypertextový odkaz add_link: Pridať hypertextový odkaz form: fields: url: label: URL msg: empty: URL adresa nemôže byť prázdna. name: label: Popis btn_cancel: Zrušiť btn_confirm: Pridať ordered_list: text: Numbered list unordered_list: text: Bulleted list table: text: Table heading: Heading cell: Bunka file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: Tento príspevok uzatváram ako... btn_cancel: Zrušiť btn_submit: Potvrdiť remark: empty: Nemôže byť prázdny. msg: empty: Vyberte dôvod. report_modal: flag_title: Nahlasujem nahlásenie tohto príspevku ako... close_title: Tento príspevok zatváram ako ... review_question_title: Kontrola otázky review_answer_title: Kontrola odpovede review_comment_title: Kontrola komentára btn_cancel: Zrušiť btn_submit: Potvrdiť remark: empty: Nemôže byť prázdny. msg: empty: Vyberte dôvod. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: Vytvorte novú značku form: fields: display_name: label: Display name msg: empty: Zobrazovaný názov nemôže byť prázdny. range: Zobrazovaný názov do 35 znakov. slug_name: label: URL slug desc: URL slug do 35 znakov. msg: empty: URL slug nemôže byť prázdny. range: URL slug do 35 znakov. character: URL slug obsahuje nepovolenú znakovú sadu. desc: label: Opis revision: label: Revision edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_cancel: Zrušiť btn_submit: Potvrdiť btn_post: Post new tag tag_info: created_at: Vytvorená edited_at: Upravená history: História synonyms: title: Synonymá text: Nasledujúce značky budú premapované na empty: Nenašli sa žiadne synonymá. btn_add: Pridajte synonymum btn_edit: Upraviť btn_save: Uložiť synonyms_text: Nasledujúce značky budú premapované na delete: title: Odstrániť túto značku tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Naozaj chcete odstrániť? close: Zavrieť merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Upraviť značku default_reason: Upraviť značku default_first_reason: Add tag btn_save_edits: Uložiť úpravy btn_cancel: Zrušiť dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [o] HH:mm" now: teraz x_seconds_ago: "pred {{count}}s" x_minutes_ago: "pred {{count}}m" x_hours_ago: "pred {{count}}h" hour: hodina day: deň hours: hours days: days month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Pridať komentár reply_to: Odpovedať btn_reply: Odpovedať btn_edit: Upraviť btn_delete: Zmazať btn_flag: Vlajka btn_save_edits: Uložiť zmeny btn_cancel: Zrušiť show_more: "{{count}} more comments" tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. tip_vote: It adds something useful to the post edit_answer: title: Uprav odpoveď default_reason: Uprav odpoveď default_first_reason: Add answer form: fields: revision: label: Revízia answer: label: Odpoveď feedback: characters: Obsah musí mať dĺžku najmenej 6 znakov. edit_summary: label: Edit summary placeholder: >- Stručne vysvetlite svoje zmeny (opravený pravopis, opravená gramatika, vylepšené formátovanie) btn_save_edits: Uložiť úpravy btn_cancel: Zrušiť tags: title: Značky sort_buttons: popular: Populárne name: názov newest: Newest button_follow: Sledovať button_following: Sledované tag_label: otázky search_placeholder: Filtrujte podľa názvu značky no_desc: Značka nemá popis. more: Viac wiki: Wiki ask: title: Create Question edit_title: Upraviť otázku default_reason: Upraviť otázku default_first_reason: Create question similar_questions: Podobné otázky form: fields: revision: label: Revízia title: label: Názov placeholder: What's your topic? Be specific. msg: empty: Názov nemôže byť prázdny. range: Názov do 150 znakov body: label: Telo msg: empty: Telo nemôže byť prázdne. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Značky -- msg: empty: Štítky nemôžu byť prázdne. answer: label: Odpoveď msg: empty: Odpoveď nemôže byť prázdna. edit_summary: label: Edit summary placeholder: >- Stručne vysvetlite svoje zmeny (opravený pravopis, opravená gramatika, vylepšené formátovanie) btn_post_question: Uverejnite svoju otázku btn_save_edits: Uložiť úpravy answer_question: Odpovedzte na svoju vlastnú otázku post_question&answer: Uverejnite svoju otázku a odpoveď tag_selector: add_btn: Pridať značku create_btn: Vytvoriť novú značku search_tag: Vyhľadať značku -- hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Nezodpovedajú žiadne značky tag_required_text: Povinný štítok (aspoň jeden) header: nav: question: Otázky tag: Značky user: Užívatelia badges: Badges profile: Profil setting: Nastavenia logout: Odhlásiť sa admin: Správca review: Preskúmanie bookmark: Bookmarks moderation: Moderation search: placeholder: Vyhľadávanie footer: build_on: Powered by <1> Apache Answer upload_img: name: Zmena loading: načítavanie... pic_auth_code: title: captcha placeholder: Zadajte vyššie uvedený text msg: empty: Captcha nemôže byť prázdna. inactive: first: >- Ste takmer na konci! Poslali sme Vám aktivačný mail na adresu {{mail}}. K aktivácií účtu postupujte prosím podľa pokynov v e-maily. info: "Ak neprichádza, skontrolujte priečinok spamu." another: >- Poslali sme vám ďalší aktivačný e-mail na adresu {{mail}}. Môže to trvať niekoľko minút; Nezabudnite skontrolovať priečinok spamu. btn_name: Opätovne odoslať aktivačný e-mail change_btn_name: Zmeniť e-mail msg: empty: Nemôže byť prázdny. resend_email: url_label: Are you sure you want to resend the activation email? url_text: You can also give the activation link above to the user. login: login_to_continue: Pre pokračovanie sa prihláste info_sign: Nemáte účet? <1>Sign up info_login: Máte už účet? <1>Log in agreements: Registráciou súhlasíte s <1>zásadami ochrany osobných údajov a <3>podmienkami služby. forgot_pass: Zabudli ste heslo? name: label: Prihlasovacie meno msg: empty: Prihlasovacie meno nemôže byť prázdne. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: E-mail msg: empty: E-mail nemôže byť prázdny. password: label: Heslo msg: empty: Heslo nemôže byť prázdne. different: Heslá zadané na oboch stranách sú nekonzistentné account_forgot: page_title: Zabudli ste heslo btn_name: Pošlite mi e-mail na obnovenie send_success: >- Ak sa účet zhoduje s {{mail}}, tak by ste mali čoskoro dostať e-mail s pokynmi, ako resetovať svoje heslo. email: label: E-mail msg: empty: E-mail nemôže byť prázdny. change_email: btn_cancel: Zrušiť btn_update: Aktualizovať e-mailovú adresu send_success: >- Ak sa účet zhoduje s {{mail}}, tak by ste mali čoskoro dostať e-mail s pokynmi, ako resetovať svoje heslo. email: label: New email msg: empty: E-mail nemôže byť prázdny. oauth: connect: Connect with {{ auth_name }} remove: Remove {{ auth_name }} oauth_bind_email: subtitle: Add a recovery email to your account. btn_update: Update email address email: label: Email msg: empty: Email cannot be empty. modal_title: Email already existes. modal_content: This email address already registered. Are you sure you want to connect to the existing account? modal_cancel: Change email modal_confirm: Connect to the existing account password_reset: page_title: Resetovanie hesla btn_name: Obnoviť heslo reset_success: >- Úspešne ste zmenili svoje heslo; Budete presmerovaný na prihlásenie. link_invalid: >- Ospravedlňujeme sa, tento odkaz na obnovenie hesla už nie je platný. Možno už došlo k resetovaniu vašho hesla? to_login: Continue to log in page password: label: Heslo msg: empty: Heslo nemôže byť prázdne. length: Dĺžka musí byť medzi 8 a 32 different: Heslá zadané na oboch stranách sú nekonzistentné password_confirm: label: Confirm new password settings: page_title: Nastavenia goto_modify: Go to modify nav: profile: Profil notification: Oznámenia account: Účet interface: Rozhranie profile: heading: Profil btn_name: Uložiť display_name: label: Display name msg: Zobrazované meno nemôže byť prázdne. msg_range: Display name must be 2-30 characters in length. username: label: Užívateľské meno caption: Ľudia vás môžu spomenúť ako „@používateľské meno“. msg: Užívateľské meno nemôže byť prázdne. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile image gravatar: Gravatar gravatar_text: You can change image on custom: Vlastný custom_text: Môžete nahrať svoj obrázok. default: Systém msg: Nahrajte avatara prosím bio: label: About me website: label: Webová stránka placeholder: "https://priklad.com" msg: Nesprávny formát webovej stránky location: label: Poloha placeholder: "Mesto, Krajina" notification: heading: Email Notifications turn_on: Turn on inbox: label: Inbox notifications description: Answers to your questions, comments, invites, and more. all_new_question: label: All new questions description: Get notified of all new questions. Up to 50 questions per week. all_new_question_for_following_tags: label: All new questions for following tags description: Get notified of new questions for following tags. account: heading: Účet change_email_btn: Zmeniť e-mail change_pass_btn: Zmeniť heslo change_email_info: >- Na túto adresu sme poslali e-mail. Postupujte podľa pokynov na potvrdenie. email: label: Email new_email: label: New email msg: New email cannot be empty. pass: label: Current password msg: Password cannot be empty. password_title: Heslo current_pass: label: Current password msg: empty: Current password cannot be empty. length: Dĺžka musí byť medzi 8 a 32. different: Dve zadané heslá sa nezhodujú. new_pass: label: New password pass_confirm: label: Confirm new password interface: heading: Rozhranie lang: label: Interface language text: Jazyk používateľského rozhrania. Zmení sa pri obnove stránky. my_logins: title: My logins label: Log in or sign up on this site using these accounts. modal_title: Remove login modal_content: Are you sure you want to remove this login from your account? modal_confirm_btn: Remove remove_success: Removed successfully toast: update: aktualizácia úspešna update_password: Heslo bolo úspešne zmenené. flag_success: Ďakujeme za nahlásenie. forbidden_operate_self: Zakázané operovať seba review: Vaša revízia sa zobrazí po preskúmaní. sent_success: Sent successfully related_question: title: Related answers: odpovede linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: People Asked desc: Select people who you think might know the answer. invite: Invite to answer add: Add people search: Search people question_detail: action: Action created: Created Asked: Opýtané asked: opýtané update: Aktualizované Edited: Edited edit: upravené commented: commented Views: Videné Follow: Sledovať Following: Sledované follow_tip: Follow this question to receive notifications answered: zodpovedaný closed_in: Uzatvorené show_exist: Ukázať existujúcu otázku. useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: title: Odpovede score: Skóre newest: Najnovšie oldest: Oldest btn_accept: Súhlasiť btn_accepted: Prijaté write_answer: title: Vaša odpoveď edit_answer: Edit my existing answer btn_name: Pošlite svoju odpoveď add_another_answer: Pridajte ďalšiu odpoveď confirm_title: Pokračovať v odpovedi continue: Pokračovať confirm_info: >-

Ste si istí, že chcete pridať ďalšiu odpoveď?

Mohli by ste namiesto toho použiť úpravu na vylepšenie svojej už existujúcej odpovede.

empty: Odpoveď nemôže byť prázdna. characters: Minimálna dĺžka obsahu musí byť 6 znakov. tips: header_1: Thanks for your answer li1_1: Please be sure to answer the question. Provide details and share your research. li1_2: Back up any statements you make with references or personal experience. header_2: But avoid ... li2_1: Asking for help, seeking clarification, or responding to other answers. reopen: confirm_btn: Reopen title: Znovu otvoriť tento príspevok content: Ste si istý, že ho chcete znovu otvoriť? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. confirm_btn: Pin delete: title: Odstrániť tento príspevok question: >- Neodporúčame mazanie otázok s odpoveďmi pretože týmto oberáte budúcich čitateľov o tieto vedomostí.

Opakované mazanie zodpovedaných otázok môže mať za následok zablokovanie možnosti kladenia otázok z vášho účtu. Ste si istí, že chcete otázku odstrániť? answer_accepted: >-

Neodporúčame odstránenie akceptovanej odpovede pretože týmto oberáte budúcich čitateľov o tieto vedomostí.

Opakované mazanie akceptovaných odpovedí môže mať za následok zablokovanie možnosti odpovedať z vášho účtu. Ste si istí, že chcete odstrániť odpoveď? other: Ste si istí, že ju chcete odstrániť? tip_answer_deleted: Táto odpoveď bola odstránená undelete_title: Undelete this post undelete_desc: Are you sure you wish to undelete? btns: confirm: Potvrdiť cancel: Zrušiť edit: Edit save: Uložiť delete: Vymazať undelete: Undelete list: List unlist: Unlist unlisted: Unlisted login: Prihlásiť sa signup: Registrovať sa logout: Odhlásiť sa verify: Preveriť create: Create approve: Schváliť reject: Odmietnuť skip: Preskočiť discard_draft: Zahodiť koncept pinned: Pinned all: All question: Question answer: Answer comment: Comment refresh: Refresh resend: Resend deactivate: Deactivate active: Active suspend: Suspend unsuspend: Unsuspend close: Close reopen: Reopen ok: OK light: Light dark: Dark system_setting: System setting default: Default reset: Reset tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Výsledky vyhľadávania keywords: Kľúčové slová options: možnosti follow: Sledovať following: Sledované counts: "{{count}} výsledky" counts_loading: "... Results" more: Viac sort_btns: relevance: Relevantnosť newest: Najnovšie active: Aktívne score: Skóre more: Viac tips: title: Tipy na pokročilé vyhľadávanie tag: "<1>[tag] hľadať v rámci značky" user: "<1>user:username hľadať podľa autora" answer: "<1>answers:0 nezodpovedané otázky" score: "<1>score:3 Príspevky so skóre 3+" question: "<1>is:question hľadať otázky" is_answer: "<1>is:answer hľadať odpovede" empty: Nemohli sme nič nájsť.
Vyskúšajte iné alebo menej špecifické kľúčové slová. share: name: Zdieľať copy: Skopírovať odkaz via: Zdieľajte príspevok cez... copied: Skopírované facebook: Zdieľať na Facebooku twitter: Share to X cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Chyba... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Váš nový účet je potvrdený; Budete presmerovaný na domovskú stránku. link: Pokračovať na domovskú stránku oops: Oops! invalid: The link you used no longer works. confirm_new_email: Váš e-mail bol aktualizovaný. confirm_new_email_invalid: >- Ospravedlňujeme sa, tento potvrdzovací odkaz už nie je platný. Váš e-mail je už môžno zmenený. unsubscribe: page_title: Zrušiť odber success_title: Úspešne zrušenie odberu success_desc: Boli ste úspešne odstránený zo zoznamu odoberateľov a nebudete od nás dostávať žiadne ďalšie e-maily. link: Zmeniť nastavenia question: following_tags: Nasledujúce značky edit: Upraviť save: Uložiť follow_tag_tip: Postupujte podľa značiek a upravte si zoznam otázok. hot_questions: Najlepšie otázky all_questions: Všetky otázky x_questions: "{{ count }} otázky/otázok" x_answers: "{{ count }} odpovede/odpovedí" x_posts: "{{ count }} Posts" questions: Otázky answers: Odpovede newest: Najnovšie active: Aktívne hot: Hot frequent: Frequent recommend: Recommend score: Skóre unanswered: Nezodpovedané modified: upravené answered: zodpovedané asked: opýtané closed: uzatvorené follow_a_tag: Postupujte podľa značky more: Viac personal: overview: Prehľad answers: Odpovede answer: odpoveď questions: Otázky question: otázka bookmarks: Záložky reputation: Reputácia comments: Komentáre votes: Hlasovanie badges: Badges newest: Najnovšie score: Skóre edit_profile: Edit profile visited_x_days: "Navštívené {{ count }} dni" viewed: Videné joined: Pripojené comma: "," last_login: Videné about_me: O mne about_me_empty: "// Dobrý deň, svet!" top_answers: Najlepšie odpovede top_questions: Najlepšie otázky stats: Štatistiky list_empty: Nenašli sa žiadne príspevky.
Možno by ste chceli vybrať inú kartu? content_empty: No posts found. accepted: Prijaté answered: zodpovedané asked: opýtané downvoted: downvoted mod_short: MOD mod_long: Moderátori x_reputation: reputácia x_votes: prijatých hlasov x_answers: odpovede x_questions: otázky recent_badges: Recent Badges install: title: Installation next: Ďalšie done: Hotový config_yaml_error: Nie je možné vytvoriť súbor config.yaml. lang: label: Please choose a language db_type: label: Database engine db_username: label: Užívateľské meno placeholder: super užívateľ msg: Užívateľské meno nemôže byť prázdne. db_password: label: Heslo placeholder: super užívateľ msg: Heslo nemôže byť prázdne. db_host: label: Database host placeholder: "db:3306" msg: Database host cannot be empty. db_name: label: Database name placeholder: odpoveď msg: Database name cannot be empty. db_file: label: Database file placeholder: /data/answer.db msg: Database file cannot be empty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Vytvoriť config.yaml label: Vytvorený súbor Config.yaml. desc: >- Súbor <1>config.yaml môžete vytvoriť manuálne v adresári <1>/var/www/xxx/ a vložiť doň nasledujúci text. info: Potom, čo ste to urobili, kliknite na tlačidlo „Ďalej“. site_information: Informácie o stránke admin_account: Správca site_name: label: Site name msg: Site name cannot be empty. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: URL stránky text: Adresa vašej stránky. msg: empty: URL stránky nemôže byť prázdny. incorrect: Nesprávny formát adresy URL. max_length: Site URL must be at maximum 512 characters in length. contact_email: label: Contact email text: E-mailová adresa kontaktu zodpovedného za túto stránku. msg: empty: Contact email cannot be empty. incorrect: Contact email incorrect format. login_required: label: Private switch: Login required text: Only logged in users can access this community. admin_name: label: Meno msg: Meno nemôže byť prázdne. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Heslo text: >- Na prihlásenie budete potrebovať toto heslo. Uložte si ho na bezpečné miesto. msg: Heslo nemôže byť prázdne. msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: E-mail text: Na prihlásenie budete potrebovať tento e-mail. msg: empty: E-mail nemôže byť prázdny. incorrect: Nesprávny formát e-mailu ready_title: Your site is ready ready_desc: >- Ak niekedy budete chcieť zmeniť viac nastavení, navštívte stránku <1>admin section; Nájdete ju v ponuke stránok. good_luck: "„Bavte sa a veľa šťastia!“" warn_title: Upozornenie warn_desc: >- Súbor <1>config.yaml už existuje. Ak potrebujete resetovať niektorú z konfiguračných položiek v tomto súbore, najskôr ju odstráňte. install_now: Môžete skúsiť <1>installing now. installed: Už nainštalované installed_desc: >- Zdá sa, že ste už aplikáciu answer nainštalovali. Ak chcete aplikáciu preinštalovať, najprv vymažte staré tabuľky z databázy. db_failed: Databázové pripojenie zlyhalo db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: názory votes: hlasy answers: odpovede accepted: prijaté page_error: http_error: HTTP Error {{ code }} desc_403: You don't have permission to access this page. desc_404: Unfortunately, this page doesn't exist. desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "Prebieha údržba, čoskoro sa vrátime." nav_menus: dashboard: Nástenka contents: Obsah questions: Otázky answers: Odpovede users: Užívatelia badges: Badges flags: Vlajky settings: Nastavenia general: Všeobecné interface: Rozhranie smtp: SMTP branding: Budovanie značky legal: legálne write: písať terms: Terms tos: Podmienky služby privacy: Súkromie seo: SEO customize: Prispôsobiť themes: Témy login: Prihlásiť sa privileges: Privileges plugins: Plugins installed_plugins: Installed Plugins apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Welcome to {{site_name}} user_center: login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Administrátor dashboard: title: Nástenka welcome: Welcome to Admin! site_statistics: Site statistics questions: "Otázky:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Odpovede:" comments: "Komentáre:" votes: "Hlasy:" users: "Users:" flags: "Vlajky:" reviews: "Reviews:" site_health: Site health version: "Verzia:" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Private public: Public smtp: "SMTP:" timezone: "Časové pásmo:" system_info: System info go_version: "Go version:" database: "Database:" database_size: "Database size:" storage_used: "Použité úložisko:" uptime: "Doba prevádzky:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Contact forum: Forum documents: Dokumenty feedback: Spätná väzba support: Podpora review: Preskúmanie config: Konfigurácia update_to: Aktualizovať na latest: Posledné check_failed: Skontrolovať zlyhanie "yes": "Áno" "no": "Nie" not_allowed: Nepovolené allowed: Povolené enabled: Povolené disabled: Zablokované writable: Writable not_writable: Not writable flags: title: Vlajky pending: Prebiehajúce completed: Dokončené flagged: Označené flagged_type: Flagged {{ type }} created: Vytvorené action: Akcia review: Preskúmanie user_role_modal: title: Zmeňte rolu používateľa na... btn_cancel: Zrušiť btn_submit: Odovzdať new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: users: label: Bulk add user placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separate “name, email, password” with commas. One user per line. msg: "Please enter the user's email, one per line." display_name: label: Display name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit users: title: Používatelia name: Meno email: E-mail reputation: Reputácia created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Stav role: Rola action: Akcia change: Zmena all: Všetko staff: Personál more: More inactive: Neaktívne suspended: Pozastavené deleted: Vymazané normal: Normálné Moderator: Moderátor Admin: Správca User: Používateľ filter: placeholder: "Filter podľa mena, používateľ: ID" set_new_password: Nastaviť nové heslo edit_profile: Edit profile change_status: Zmentiť stavu change_role: Zmeniť rolu show_logs: Zobraziť protokoly add_user: Pridať používateľa deactivate_user: title: Deactivate user content: An inactive user must re-validate their email. delete_user: title: Delete this user content: Are you sure you want to delete this user? This is permanent! remove: Remove their content label: Remove all questions, answers, comments, etc. text: Don’t check this if you wish to only delete the user’s account. suspend_user: title: Suspend this user content: A suspended user can't log in. label: How long will the user be suspended for? forever: Forever questions: page_title: Otázky unlisted: Unlisted post: poslané votes: Hlasy answers: Odpovede created: Vytvorené status: Stav action: Akcia change: Zmena pending: Pending filter: placeholder: "Filter podľa názvu, otázka:id" answers: page_title: Odpovede post: Poslané votes: Hlasy created: Vytvorené status: Stav action: Akcia change: Zmena filter: placeholder: "Filter podľa názvu, odpoveď:id" general: page_title: Všeobecné name: label: Site name msg: Názov stránky nemôže byť prázdny. text: "Názov tejto lokality, ako sa používa v značke názvu." site_url: label: URL stránky msg: Adresa Url stránky nemôže byť prázdna. validate: Prosím uveďte platnú webovú adresu. text: Adresa vašej stránky. short_desc: label: Short site description msg: Krátky popis stránky nemôže byť prázdny. text: "Krátky popis, ako sa používa v značke názvu na domovskej stránke." desc: label: Site description msg: Popis stránky nemôže byť prázdny. text: "Opíšte túto stránku jednou vetou, ako sa používa v značke meta description." contact_email: label: Contact email msg: Kontaktný e-mail nemôže byť prázdny. validate: Kontaktný e-mail je neplatný. text: E-mailová adresa kontaktu zodpovedného za túto stránku. check_update: label: Software updates text: Automatically check for updates interface: page_title: Rozhranie language: label: Interface language msg: Jazyk rozhrania nemôže byť prázdny. text: Jazyk používateľského rozhrania. Zmení sa, keď stránku obnovíte. time_zone: label: Časové pásmo msg: Časové pásmo nemôže byť prázdne. text: Vyberte si mesto v rovnakom časovom pásme ako vy. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: From email msg: Z e-mailu nemôže byť prázdne. text: E-mailová adresa, z ktorej sa odosielajú e-maily. from_name: label: From name msg: Názov od nemôže byť prázdny. text: Meno, z ktorého sa odosielajú e-maily. smtp_host: label: SMTP host msg: Hostiteľ SMTP nemôže byť prázdny. text: Váš mailový server. encryption: label: Šifrovanie msg: Šifrovanie nemôže byť prázdne. text: Pre väčšinu serverov je SSL odporúčaná možnosť. ssl: SSL tls: TLS none: Žiadne smtp_port: label: SMTP port msg: Port SMTP musí byť číslo 1 ~ 65535. text: Port na váš poštový server. smtp_username: label: SMTP username msg: Používateľské meno SMTP nemôže byť prázdne. smtp_password: label: SMTP password msg: Heslo SMTP nemôže byť prázdne. test_email_recipient: label: Test email recipients text: Zadajte e-mailovú adresu, na ktorú sa budú odosielať testy. msg: Príjemcovia testovacieho e-mailu sú neplatní smtp_authentication: label: Povoliť autentifikáciu title: SMTP authentication msg: Overenie SMTP nemôže byť prázdne. "yes": "Áno" "no": "Nie" branding: page_title: Budovanie značky logo: label: Logo msg: Logo nemôže byť prázdne. text: Obrázok loga v ľavej hornej časti vašej stránky. Použite široký obdĺžnikový obrázok s výškou 56 a pomerom strán väčším ako 3:1. Ak ho ponecháte prázdne, zobrazí sa text názvu stránky. mobile_logo: label: Mobile logo text: Logo použité na mobilnej verzii vášho webu. Použite široký obdĺžnikový obrázok s výškou 56. Ak pole ponecháte prázdne, použije sa obrázok z nastavenia „logo“. square_icon: label: Square icon msg: Ikona štvorca nemôže byť prázdna. text: Obrázok použitý ako základ pre ikony metadát. V ideálnom prípade by mal byť väčšií ako 512 x 512. favicon: label: favicon text: Favicon pre váš web. Ak chcete správne fungovať cez CDN, musí to byť png. Veľkosť sa zmení na 32 x 32. Ak zostane prázdne, použije sa „štvorcová ikona“. legal: page_title: Legálne terms_of_service: label: Terms of service text: "Tu môžete pridať obsah zmluvných podmienok. Ak už máte dokument umiestnený inde, uveďte tu celú URL adresu." privacy_policy: label: Privacy policy text: "Tu môžete pridať obsah zásad ochrany osobných údajov. Ak už máte dokument umiestnený inde, uveďte tu celú URL adresu." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for each question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Každá nová otázka musí mať aspoň jedenu odporúčaciu značku." reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: trvalý odkaz text: Vlastné štruktúry URL môžu zlepšiť použiteľnosť a doprednú kompatibilitu vašich odkazov. robots: label: robots.txt text: Toto natrvalo prepíše všetky nastavenia súvisiace so stránkou. themes: page_title: Témy themes: label: Témy text: Vyberte existujúcu tému. color_scheme: label: Color scheme navbar_style: label: Navbar background style primary_color: label: Primary color text: Upraviť farby používané vašími motívmi layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS a HTML custom_css: label: Vlastné CSS text: > head: label: Head text: > header: label: Hlavička text: > footer: label: Päta text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: Prihlásenie membership: title: Členstvo label: Povoliť nové registrácie text: Vypnúť, aby sa zabránilo vytvorenie nového účtu hocikým. email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: Allowed email domains text: Email domains that users must register accounts with. One domain per line. Ignored when empty. private: title: Súkromné label: Vyžaduje sa prihlásenie text: Do tejto komunity majú prístup iba prihlásení používatelia password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: All active: Active inactive: Inactive outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Name version: Version status: Status action: Action deactivate: Deactivate activate: Activate settings: Settings settings_users: title: Users avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar Base URL text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Allow users to change their display name allow_update_username: label: Allow users to change their username allow_update_avatar: label: Allow users to change their profile image allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (voliteľné) empty: nemôže byť prázdne invalid: je neplatné btn_submit: Uložiť not_found_props: "Požadovaná vlastnosť {{ key }} nebola nájdená." select: Select page_review: review: Preskúmanie proposed: navrhované question_edit: Úprava otázky answer_edit: Úprava odpovede tag_edit: Úprava značky edit_summary: Upraviť súhrn edit_question: Upraviť otázku edit_answer: Upraviť odpoveď edit_tag: Upraviť značku empty: Nezostali žiadne úlohy kontroly. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: zrušené zmazanie deleted: vymazané downvote: hlasovať proti upvote: hlasovať za accept: akceptované cancelled: zrušené commented: komentované rollback: Návrat edited: zmenené answered: odpovedané asked: spýtané closed: uzavreté reopened: znovu otvorené created: vytvorené pin: pinned unpin: unpinned show: listed hide: unlisted title: "História pre" tag_title: "Časová os pre" show_votes: "Zobraziť hlasy" n_or_a: N/A title_for_question: "Časová os pre" title_for_answer: "Časová os odpovede na {{ title }} od {{ author }}" title_for_tag: "Časová os pre značku" datetime: Dátum a čas type: Typ by: Od comment: Komentár no_data: "Nič sa nám nepodarilo nájsť." users: title: Použivatelia users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: Zamestnanci našej komunity reputation: reputácia votes: hlasy prompt: leave_page: Ste si istý, že chcete opustiť stránku? changes_not_save: Vaše zmeny nemusia byť uložené. draft: discard_confirm: Naozaj chcete zahodiť svoj koncept? messages: post_deleted: Tento príspevok bol odstránený. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/sq_AL.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: "Success." unknown: other: "Unknown error." request_format_error: other: "Request format is not valid." unauthorized_error: other: "Unauthorized." database_error: other: "Data server error." role: name: user: other: "User" admin: other: "Admin" moderator: other: "Moderator" description: user: other: "Default with no special access." admin: other: "Have the full power to access the site." moderator: other: "Has access to all posts except admin settings." email: other: "Email" password: other: "Password" email_or_password_wrong_error: other: "Email and password do not match." error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: "Answer do not found." cannot_deleted: other: "No permission to delete." cannot_update: other: "No permission to update." comment: edit_without_permission: other: "Comment are not allowed to edit." not_found: other: "Comment not found." email: duplicate: other: "Email already exists." need_to_be_verified: other: "Email should be verified." verify_url_expired: other: "Email verified URL has expired, please resend the email." lang: not_found: other: "Language file not found." object: captcha_verification_failed: other: "Captcha wrong." disallow_follow: other: "You are not allowed to follow." disallow_vote: other: "You are not allowed to vote." disallow_vote_your_self: other: "You can't vote for your own post." not_found: other: "Object not found." verification_failed: other: "Verification failed." email_or_password_incorrect: other: "Email and password do not match." old_password_verification_failed: other: "The old password verification failed" new_password_same_as_previous_setting: other: "The new password is the same as the previous one." question: not_found: other: "Question not found." cannot_deleted: other: "No permission to delete." cannot_close: other: "No permission to close." cannot_update: other: "No permission to update." rank: fail_to_meet_the_condition: other: "Rank fail to meet the condition." report: handle_failed: other: "Report handle failed." not_found: other: "Report not found." tag: not_found: other: "Tag not found." recommend_tag_not_found: other: "Recommend Tag is not exist." recommend_tag_enter: other: "Please enter at least one required tag." not_contain_synonym_tags: other: "Should not contain synonym tags." cannot_update: other: "No permission to update." cannot_set_synonym_as_itself: other: "You cannot set the synonym of the current tag as itself." smtp: config_from_name_cannot_be_email: other: "The From Name cannot be a email address." theme: not_found: other: "Theme not found." revision: review_underway: other: "Can't edit currently, there is a version in the review queue." no_permission: other: "No permission to Revision." user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: "User not found." suspended: other: "User has been suspended." username_invalid: other: "Username is invalid." username_duplicate: other: "Username is already in use." set_avatar: other: "Avatar set failed." cannot_update_your_role: other: "You cannot modify your role." not_allowed_registration: other: "Currently the site is not open for registration" config: read_config_failed: other: "Read config failed" database: connection_failed: other: "Database connection failed" create_table_failed: other: "Create table failed" install: create_config_failed: other: "Can't create the config.yaml file." report: spam: name: other: "spam" desc: other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." rude: name: other: "rude or abusive" desc: other: "A reasonable person would find this content inappropriate for respectful discourse." duplicate: name: other: "a duplicate" desc: other: "This question has been asked before and already has an answer." not_answer: name: other: "not an answer" desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." not_need: name: other: "no longer needed" desc: other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" desc: other: "This post requires staff attention for another reason not listed above." question: close: duplicate: name: other: "spam" desc: other: "This question has been asked before and already has an answer." guideline: name: other: "a community-specific reason" desc: other: "This question doesn't meet a community guideline." multiple: name: other: "needs details or clarity" desc: other: "This question currently includes multiple questions in one. It should focus on one problem only." other: name: other: "something else" desc: other: "This post requires another reason not listed above." operation_type: asked: other: "asked" answered: other: "answered" modified: other: "modified" notification: action: update_question: other: "updated question" answer_the_question: other: "answered question" update_answer: other: "updated answer" accept_answer: other: "accepted answer" comment_question: other: "commented question" comment_answer: other: "commented answer" reply_to_you: other: "replied to you" mention_you: other: "mentioned you" your_question_is_closed: other: "Your question has been closed" your_question_was_deleted: other: "Your question has been deleted" your_answer_was_deleted: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comment tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to Answer btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name up to 30 characters username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username up to 30 characters character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to Answer success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: "After you've done that, click “Next” button." site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: display_name must be at 2 - 30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8 - 32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/sr_SP.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #The following fields are used for back-end backend: base: success: other: Success. unknown: other: Unknown error. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. email: other: Email password: other: Password email_or_password_wrong_error: other: Email and password do not match. error: admin: email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. rank: fail_to_meet_the_condition: other: Rank fail to meet the condition. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: not_found: other: Tag not found. recommend_tag_not_found: other: Recommend Tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: other: The From Name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to Revision. user: email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. report: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude: name: other: rude or abusive desc: other: A reasonable person would find this content inappropriate for respectful discourse. duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. not_answer: name: other: not an answer desc: other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. not_need: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. other: name: other: something else desc: other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted #The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next page_title: question: Question questions: Questions tag: Tag tags: Tags tag_wiki: tag wiki edit_tag: Edit Tag ask_a_question: Add Question edit_question: Edit Question edit_answer: Edit Answer search: Search posts_containing: Posts containing settings: Settings notifications: Notifications login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users notifications: title: Notifications inbox: Inbox achievement: Achievements all_read: Mark all as read show_more: Show more suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language (optional) placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal Rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image File btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed 4 MB. desc: label: Description (optional) tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description (optional) btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered List unordered_list: text: Bulleted List table: text: Table heading: Heading cell: Cell close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. tag_modal: title: Create new tag form: fields: display_name: label: Display Name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL Slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description (optional) btn_cancel: Cancel btn_submit: Submit tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag content: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

content2: Are you sure you wish to delete? close: Close edit_tag: title: Edit Tag default_reason: Edit tag form: fields: revision: label: Revision display_name: label: Display Name slug_name: label: URL Slug info: URL slug up to 35 characters. desc: label: Description edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: Show more comments tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. edit_answer: title: Edit Answer default_reason: Edit answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More ask: title: Add Question edit_title: Edit Question default_reason: Edit question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: Be specific and imagine you're asking a question to another person msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit Summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users profile: Profile setting: Settings logout: Log out admin: Admin review: Review search: placeholder: Search footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. login: page_title: Welcome to {{site_name}} login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: page_title: Welcome to {{site_name}} btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New Email msg: empty: Email cannot be empty. password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm New Password settings: page_title: Settings nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display Name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile Image gravatar: Gravatar gravatar_text: You can change image on <1>gravatar.com custom: Custom btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About Me (optional) website: label: Website (optional) placeholder: "https://example.com" msg: Website incorrect format location: label: Location (optional) placeholder: "City, Country" notification: heading: Notifications email: label: Email Notifications radio: "Answers to your questions, comments, and more" account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. password_title: Password current_pass: label: Current Password msg: empty: Current Password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New Password pass_confirm: label: Confirm New Password interface: heading: Interface lang: label: Interface Language text: User interface language. It will change when you refresh the page. toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions btn: Add question answers: answers question_detail: Asked: Asked asked: asked update: Modified edit: edited Views: Viewed Follow: Follow Following: Following answered: answered closed_in: Closed in show_exist: Show existing question. answers: title: Answers score: Score newest: Newest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. reopen: title: Reopen this post content: Are you sure you want to reopen? success: This post has been reopened delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted btns: confirm: Confirm cancel: Cancel save: Save delete: Delete login: Log in signup: Sign up logout: Log out verify: Verify add_question: Add question approve: Approve reject: Reject skip: Skip search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post modal_confirm: title: Error... account_result: page_title: Welcome to {{site_name}} success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage invalid: >- Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" questions: Questions answers: Answers newest: Newest active: Active hot: Hot score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes newest: Newest score: Score edit_profile: Edit Profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? accepted: Accepted answered: answered asked: asked upvote: upvote downvote: downvote mod_short: Mod mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please Choose a Language db_type: label: Database Engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database Host placeholder: "db:3306" msg: Database Host cannot be empty. db_name: label: Database Name placeholder: answer msg: Database Name cannot be empty. db_file: label: Database File placeholder: /data/answer.db msg: Database File cannot be empty. config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site Name msg: Site Name cannot be empty. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. contact_email: label: Contact Email text: Email address of key contact responsible for this site. msg: empty: Contact Email cannot be empty. incorrect: Contact Email incorrect format. admin_name: label: Name msg: Name cannot be empty. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_404: desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes css-html: CSS/HTML login: Login admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site Statistics questions: "Questions:" answers: "Answers:" comments: "Comments:" votes: "Votes:" active_users: "Active users:" flags: "Flags:" site_health_status: Site Health Status version: "Version:" https: "HTTPS:" uploading_files: "Uploading files:" smtp: "SMTP:" timezone: "Timezone:" system_info: System Info storage_used: "Storage used:" uptime: "Uptime:" answer_links: Answer Links documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled flags: title: Flags pending: Pending completed: Completed flagged: Flagged created: Created action: Action review: Review change_modal: title: Change user status to... btn_cancel: Cancel btn_submit: Submit normal_name: normal normal_desc: A normal user can ask and answer questions. suspended_name: suspended suspended_desc: A suspended user can't log in. deleted_name: deleted deleted_desc: "Delete profile, authentication associations." inactive_name: inactive inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete msg: empty: Please select a reason. status_modal: title: "Change {{ type }} status to..." normal_name: normal normal_desc: A normal post available to everyone. closed_name: closed closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created Time delete_at: Deleted Time suspend_at: Suspended Time status: Status role: Role action: Action change: Change all: All staff: Staff inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: display_name: label: Display Name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit questions: page_title: Questions normal: Normal closed: Closed deleted: Deleted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, question:id" answers: page_title: Answers normal: Normal deleted: Deleted post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site Name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact Email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface logo: label: Logo (optional) msg: Site logo cannot be empty. text: You can upload your image or <1>reset it to the site title text. theme: label: Theme msg: Theme cannot be empty. text: Select an existing theme. language: label: Interface Language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: label: From Email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From Name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP Host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP Username msg: SMTP username cannot be empty. smtp_password: label: SMTP Password msg: SMTP password cannot be empty. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo (optional) msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile Logo (optional) text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square Icon (optional) msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon (optional) text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of Service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy Policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." write: page_title: Write recommend_tags: label: Recommend Tags text: "Please input tag slug above, one tag per line." required_tag: title: Required Tag label: Set recommend tag as required text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. navbar_style: label: Navbar Style text: Select an existing theme. primary_color: label: Primary Color text: Modify the colors used by your themes css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: This will insert as head: label: Head text: This will insert before header: label: Header text: This will insert after footer: label: Footer text: This will insert before . login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: title: Private label: Login required text: Only logged in users can access this community. form: empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_vote: Users who voted the most staffs: Our community staff reputation: reputation votes: votes ================================================ FILE: i18n/sv_SE.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Åtgärden lyckades unknown: other: Okänt fel. request_format_error: other: Request format is not valid. unauthorized_error: other: Access saknas database_error: other: Data server error. forbidden_error: other: Förbjudet. duplicate_request_error: other: Dubblett inlämning. action: report: other: Flagga edit: other: Redigera delete: other: Radera close: other: Stäng reopen: other: Öppna igen forbidden_error: other: Förbjudet. pin: other: Fäst hide: other: Göm unpin: other: Lossa show: other: Lista invite_someone_to_answer: other: Redigera undelete: other: Återskapa merge: other: Sammanfoga role: name: user: other: Användare admin: other: Administratör moderator: other: Moderator description: user: other: Normal tillgång admin: other: Full kontroll över webbplatsen moderator: other: Tillgång till allt utom administratörsinställningar privilege: level_1: description: other: Nivå 1 level_2: description: other: Nivå 2 level_3: description: other: Nivå 3 level_custom: description: other: Anpassad nivå rank_question_add_label: other: Ställ en fråga rank_answer_add_label: other: Skriv ett svar rank_comment_add_label: other: Skriv en kommentar rank_report_add_label: other: Flagga rank_comment_vote_up_label: other: Bra kommentar rank_link_url_limit_label: other: Mer än 2 länkar samtidigt rank_question_vote_up_label: other: Bra fråga rank_answer_vote_up_label: other: Bra svar rank_question_vote_down_label: other: Dålig fråga rank_answer_vote_down_label: other: Dåligt svar rank_invite_someone_to_answer_label: other: Bjud in någon att svara rank_tag_add_label: other: Skapa ny tagg rank_tag_edit_label: other: Beskriv etiketten (behöver granskas) rank_question_edit_label: other: Editera annans fråga (behöver granskas) rank_answer_edit_label: other: Editera annans svar (behöver granskas) rank_question_edit_without_review_label: other: Editera annans fråga utan granskning rank_answer_edit_without_review_label: other: Editera annans svar utan granskning rank_question_audit_label: other: Granska ändringar av fråga rank_answer_audit_label: other: Granska ändringar av svar rank_tag_audit_label: other: Granska ändringar av etikett rank_tag_edit_without_review_label: other: Ändra etikett-beskrivningen utan granskning rank_tag_synonym_label: other: Hantera etikett-synonymer email: other: E-post e_mail: other: E-post password: other: Lösenord pass: other: Lösenord old_pass: other: Nuvarande lösenord original_text: other: Detta inlägg email_or_password_wrong_error: other: Fel e-post eller lösenord error: common: invalid_url: other: Ogiltig URL. status_invalid: other: Ogiltig status. password: space_invalid: other: Lösenordet får inte innehålla mellanslag. admin: cannot_update_their_password: other: Du får inte ändra ditt lösenord. cannot_edit_their_profile: other: Du får inte ändra din profil. cannot_modify_self_status: other: Du får inte ändra din status. email_or_password_wrong: other: Fel e-post eller lösenord. answer: not_found: other: Svar hittades inte. cannot_deleted: other: Radering tillåts inte. cannot_update: other: No permission to update. question_closed_cannot_add: other: Questions are closed and cannot be added. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. content_cannot_empty: other: Kommentarsfältet får inte vara tomt. email: duplicate: other: E-postadressen finns redan. need_to_be_verified: other: E-postadressen ska vara verifierad. verify_url_expired: other: Länken för att verifiera e-postadressen har gått ut. Vänligen skicka igen. illegal_email_domain_error: other: E-post från den domänen tillåts inte. Vänligen använt en annan. lang: not_found: other: Språkfilen hittas inte. object: captcha_verification_failed: other: Fel Captcha. disallow_follow: other: Du tillåts inte följa. disallow_vote: other: Du tillåts inte rösta. disallow_vote_your_self: other: Du får inte rösta på ditt eget inlägg. not_found: other: Objektet hittas inte. verification_failed: other: Verifiering misslyckades. email_or_password_incorrect: other: Fel e-postadress eller lösenord. old_password_verification_failed: other: Den gamla verifieringen av lösenordet misslyckades. new_password_same_as_previous_setting: other: Det nya lösenordet är samma som det förra. already_deleted: other: Det här inlägget har raderats. meta: object_not_found: other: Meta-objekt hittas inte. question: already_deleted: other: Det här inlägget har raderats. under_review: other: Ditt inlägg väntar på granskning. Det kommer att publiceras så snart det har blivit godkänt. not_found: other: . cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. vote_fail_to_meet_the_condition: other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. no_enough_rank_to_operate: other: You need at least {{.Rank}} reputation to do this. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: already_exist: other: Tag already exists. not_found: other: Tag not found. recommend_tag_not_found: other: Recommend tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. is_used_cannot_delete: other: You cannot delete a tag that is in use. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: The from name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to revise. user: external_login_missing_user_id: other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. external_login_unbinding_forbidden: other: Please set a login password for your account before you remove this login. email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration. not_allowed_login_via_password: other: Currently the site is not allowed to login via password. access_denied: other: Access denied page_access_denied: other: You do not have access to this page. add_bulk_users_format_error: other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Tabellen kunde inte skapas. install: create_config_failed: other: Filen config.yaml kan inte skapas. upload: unsupported_file_format: other: Filformatet tillåts inte. site_info: config_not_found: other: Webbplats inställningarna hittar inte. badge: object_not_found: other: Badge object not found reason: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude_or_abusive: name: other: rude or abusive desc: other: "A reasonable person would find this content inappropriate for respectful discourse." a_duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. placeholder: other: Enter the existing question link not_a_answer: name: other: not an answer desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." no_longer_needed: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. something: name: other: something else desc: other: This post requires staff attention for another reason not listed above. placeholder: other: Let us know specifically what you are concerned about community_specific: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. not_clarity: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. looks_ok: name: other: looks OK desc: other: This post is good as-is and not low quality. needs_edit: name: other: needs edit, and I did it desc: other: Improve and correct problems with this post yourself. needs_close: name: other: needs close desc: other: A closed question can't answer, but still can edit, vote and comment. needs_delete: name: other: needs delete desc: other: This post will be deleted. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified deleted_title: other: Deleted question questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Din fråga har raderats your_answer_was_deleted: other: Ditt svar har raderats your_comment_was_deleted: other: Din kommentar har raderats up_voted_question: other: upvoted question down_voted_question: other: downvoted question up_voted_answer: other: upvoted answer down_voted_answer: other: downvoted answer up_voted_comment: other: upvoted comment invited_you_to_answer: other: invited you to answer earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "[{{.SiteName}}] Bekräfta din nya e-postadress" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] Ny fråga: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Password reset" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Bekräfta ditt nya konto" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Test Email" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: upvote upvoted: other: upvoted downvote: other: downvote downvoted: other: downvoted accept: other: accept accepted: other: accepted edit: other: edit review: queued_post: other: Queued post flagged_post: other: Flagged post suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: Editor desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Nästa page_title: question: Fråga questions: Frågor tag: Tagg tags: Taggar tag_wiki: tag wiki create_tag: Create Tag edit_tag: Edit Tag ask_a_question: Create Question edit_question: Redigera fråga edit_answer: Redigera svar search: Sök posts_containing: Posts containing settings: Inställningar notifications: Notifications login: Logga in sign_up: Registrera dig account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: Account Suspended admin: Admin change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Användare oauth_callback: Processing http_404: HTTP Error 404 http_50X: HTTP Error 500 http_403: HTTP Error 403 logout: Logga ut posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Notifications inbox: Inkorg achievement: Achievements new_alerts: New alerts all_read: Markera alla som lästa show_more: Visa mer someone: Someone inbox_type: all: Alla posts: Inlägg invites: Inbjudningar votes: Röster answer: Answer question: Question badge_award: Badge suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. contact_us: Kontakta oss editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Kod msg: empty: Code cannot be empty. language: label: Språk placeholder: Automatic detection btn_cancel: Avbryt btn_confirm: Lägg till formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Hjälp hr: text: Horizontal rule image: text: Bild add_image: Lägg till bild tab_image: Ladda upp bild form_image: fields: file: label: Image file btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed {{size}} MB. desc: label: Beskrivning tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Beskrivning btn_cancel: Avbryt btn_confirm: Lägg till uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Beskrivning btn_cancel: Avbryt btn_confirm: Lägg till ordered_list: text: Numbered list unordered_list: text: Bulleted list table: text: Tabell heading: Heading cell: Cell file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: I am closing this post as... btn_cancel: Avbryt btn_submit: Skicka remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Avbryt btn_submit: Skicka remark: empty: Cannot be empty. msg: empty: Please select a reason. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: Create new tag form: fields: display_name: label: Visningsnamn msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Beskrivning revision: label: Revision edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_cancel: Avbryt btn_submit: Skicka btn_post: Post new tag tag_info: created_at: Skapad edited_at: Edited history: Historik synonyms: title: Synonymer text: The following tags will be remapped to empty: Inga synonymer hittades. btn_add: Lägg till en synonym btn_edit: Redigera btn_save: Spara synonyms_text: The following tags will be remapped to delete: title: Delete this tag tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Are you sure you wish to delete? close: Stäng merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Edit Tag default_reason: Edit tag default_first_reason: Lägg till tagg btn_save_edits: Save edits btn_cancel: Avbryt dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: nu x_seconds_ago: "{{count}} s sedan" x_minutes_ago: "{{count}} m sedan" x_hours_ago: "{{count}} t sedan" hour: timme day: dag hours: timmar days: dagar month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Lägg till kommentar reply_to: Reply to btn_reply: Svara btn_edit: Redigera btn_delete: Radera btn_flag: Flag btn_save_edits: Save edits btn_cancel: Avbryt show_more: "{{count}} more comments" tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. tip_vote: It adds something useful to the post edit_answer: title: Edit Answer default_reason: Edit answer default_first_reason: Add answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Avbryt tags: title: Tags sort_buttons: popular: Popular name: Namn newest: Newest button_follow: Följ button_following: Följer tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More wiki: Wiki ask: title: Create Question edit_title: Edit Question default_reason: Edit question default_first_reason: Create question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: What's your topic? Be specific. msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Användare badges: Badges profile: Profil setting: Inställningar logout: Logga ut admin: Admin review: Review bookmark: Bokmärken moderation: Moderation search: placeholder: Sök footer: build_on: Powered by <1> Apache Answer upload_img: name: Ändra loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. resend_email: url_label: Are you sure you want to resend the activation email? url_text: You can also give the activation link above to the user. login: login_to_continue: Logga in för att fortsätta info_sign: Har du inget konto? <1>Registrera dig info_login: Har du redan ett konto? <1>Logga in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Glömt lösenord? name: label: Namn msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: E-postadress msg: empty: Email cannot be empty. password: label: Lösenord msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Glömt ditt lösenord btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: E-postadress msg: empty: Email cannot be empty. change_email: btn_cancel: Avbryt btn_update: Uppdatera e-postadress send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Ny e-postadress msg: empty: Email cannot be empty. oauth: connect: Connect with {{ auth_name }} remove: Remove {{ auth_name }} oauth_bind_email: subtitle: Add a recovery email to your account. btn_update: Uppdatera e-postadress email: label: Email msg: empty: Email cannot be empty. modal_title: Email already existes. modal_content: This email address already registered. Are you sure you want to connect to the existing account? modal_cancel: Change email modal_confirm: Connect to the existing account password_reset: page_title: Password Reset btn_name: Återställ mitt lösenord reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Lösenord msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Bekräfta nytt lösenord settings: page_title: Inställningar goto_modify: Go to modify nav: profile: Profil notification: Notifications account: Konto interface: Interface profile: heading: Profil btn_name: Spara display_name: label: Visningsnamn msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Användarnamn caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profilbild gravatar: Gravatar gravatar_text: You can change image on custom: Custom custom_text: Du kan ladda upp din bild. default: System msg: Please upload an avatar bio: label: Om mig website: label: Webbplats placeholder: "https://example.com" msg: Website incorrect format location: label: Location placeholder: "Stad, Land" notification: heading: Email Notifications turn_on: Turn on inbox: label: Inbox notifications description: Answers to your questions, comments, invites, and more. all_new_question: label: All new questions description: Get notified of all new questions. Up to 50 questions per week. all_new_question_for_following_tags: label: All new questions for following tags description: Get notified of new questions for following tags. account: heading: Konto change_email_btn: Change email change_pass_btn: Ändra lösenord change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Ny e-postadress new_email: label: New email msg: New email cannot be empty. pass: label: Nuvarande lösenord msg: Password cannot be empty. password_title: Lösenord current_pass: label: Nuvarande lösenord msg: empty: Current password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: Nytt lösenord pass_confirm: label: Bekräfta nytt lösenord interface: heading: Interface lang: label: Interface language text: User interface language. It will change when you refresh the page. my_logins: title: My logins label: Log in or sign up on this site using these accounts. modal_title: Remove login modal_content: Are you sure you want to remove this login from your account? modal_confirm_btn: Remove remove_success: Removed successfully toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. sent_success: Sent successfully related_question: title: Related answers: answers linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: People Asked desc: Bjud in personer som du tror kan svara. invite: Invite to answer add: Add people search: Search people question_detail: action: Action created: Created Asked: Asked asked: asked update: Modified Edited: Edited edit: edited commented: commented Views: Viewed Follow: Follow Following: Following follow_tip: Follow this question to receive notifications answered: answered closed_in: Closed in show_exist: Show existing question. useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: title: Answers score: Score newest: Newest oldest: Oldest btn_accept: Accept btn_accepted: Accepted write_answer: title: Ditt svar edit_answer: Edit my existing answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Fortsätt confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. tips: header_1: Thanks for your answer li1_1: Please be sure to answer the question. Provide details and share your research. li1_2: Back up any statements you make with references or personal experience. header_2: But avoid ... li2_1: Asking for help, seeking clarification, or responding to other answers. reopen: confirm_btn: Reopen title: Reopen this post content: Are you sure you want to reopen? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. confirm_btn: Fäst delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Är du säker på att du vill radera? tip_answer_deleted: This answer has been deleted undelete_title: Undelete this post undelete_desc: Are you sure you wish to undelete? btns: confirm: Bekräfta cancel: Avbryt edit: Redigera save: Spara delete: Radera undelete: Undelete list: List unlist: Unlist unlisted: Unlisted login: Logga in signup: Registrera dig logout: Logga ut verify: Verify create: Create approve: Approve reject: Reject skip: Hoppa över discard_draft: Discard draft pinned: Pinned all: All question: Question answer: Answer comment: Comment refresh: Uppdatera resend: Resend deactivate: Deactivate active: Active suspend: Suspend unsuspend: Unsuspend close: Stäng reopen: Reopen ok: OK light: Ljust dark: Mörkt system_setting: System setting default: Standard reset: Återställ tag: Tag post_lowercase: post filter: Filter ignore: Ignorera submit: Skicka normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Sökresultat keywords: Keywords options: Alternativ follow: Follow following: Following counts: "{{count}} resultat" counts_loading: "... Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Dela copy: Kopiera länk via: Dela inlägg via... copied: Copied facebook: Dela på Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Error... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage oops: Oops! invalid: The link you used no longer works. confirm_new_email: Din e-postadress har uppdaterats. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Redigera save: Spara follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} frågor" x_answers: "{{ count }} svar" x_posts: "{{ count }} Posts" questions: Questions answers: Answers newest: Newest active: Active hot: Hot frequent: Frequent recommend: Recommend score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bokmärken reputation: Reputation comments: Kommentarer votes: Röster badges: Badges newest: Newest score: Score edit_profile: Redigera profil visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined comma: "," last_login: Seen about_me: Om mig about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? content_empty: No posts found. accepted: Accepted answered: answered asked: asked downvoted: downvoted mod_short: MOD mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions recent_badges: Recent Badges install: title: Installation next: Nästa done: Klar config_yaml_error: Can't create the config.yaml file. lang: label: Välj ett språk db_type: label: Database engine db_username: label: Användarnamn placeholder: root msg: Username cannot be empty. db_password: label: Lösenord placeholder: root msg: Password cannot be empty. db_host: label: Database host placeholder: "db:3306" msg: Database host cannot be empty. db_name: label: Databasnamn placeholder: answer msg: Database name cannot be empty. db_file: label: Database file placeholder: /data/answer.db msg: Database file cannot be empty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Skapa config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site name msg: Site name cannot be empty. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. max_length: Site URL must be at maximum 512 characters in length. contact_email: label: Contact email text: Email address of key contact responsible for this site. msg: empty: Contact email cannot be empty. incorrect: Contact email incorrect format. login_required: label: Privat switch: Login required text: Only logged in users can access this community. admin_name: label: Namn msg: Name cannot be empty. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Lösenord text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Varning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_error: http_error: HTTP Error {{ code }} desc_403: You don't have permission to access this page. desc_404: Unfortunately, this page doesn't exist. desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Användare badges: Badges flags: Flags settings: Inställningar general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write terms: Terms tos: Användarvillkor privacy: Privacy seo: SEO customize: Anpassa themes: Teman login: Logga in privileges: Privileges plugins: Plugins installed_plugins: Installed Plugins apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Välkommen till {{site_name}} user_center: login: Login qrcode_login_tip: Använd {{ agentName }} för att skanna QR-koden och logga in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: modal: title: Grattis content: You've earned a new badge. close: Stäng confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site statistics questions: "Frågor:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Svar:" comments: "Kommentarer:" votes: "Röster:" users: "Användare:" flags: "Flags:" reviews: "Reviews:" site_health: Site health version: "Version:" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Privat public: Public smtp: "SMTP:" timezone: "Tidszon:" system_info: System info go_version: "Go version:" database: "Databas:" database_size: "Database size:" storage_used: "Storage used:" uptime: "Uptime:" links: Länkar plugins: Plugins github: GitHub blog: Blogg contact: Kontakt forum: Forum documents: Dokument feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Ja" "no": "Nej" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled writable: Writable not_writable: Not writable flags: title: Flags pending: Pending completed: Completed flagged: Flagged flagged_type: Flagged {{ type }} created: Created action: Action review: Review user_role_modal: title: Ändra användarroll till... btn_cancel: Avbryt btn_submit: Skicka new_password_modal: title: Set new password form: fields: password: label: Lösenord text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Avbryt btn_submit: Skicka edit_profile_modal: title: Redigera profil form: fields: display_name: label: Visningsnamn msg_range: Display name must be 2-30 characters in length. username: label: Användarnamn msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Ogiltig e-postadress. edit_success: Edited successfully btn_cancel: Avbryt btn_submit: Skicka user_modal: title: Lägg till ny användare form: fields: users: label: Bulk add user placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separera "namn, e-postadress, lösenord" med kommatecken. En användare per rad. msg: "Please enter the user's email, one per line." display_name: label: Visningsnamn msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Lösenord msg: Password must be at 8-32 characters in length. btn_cancel: Avbryt btn_submit: Skicka users: title: Användare name: Namn email: Email reputation: Reputation created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Status role: Roll action: Action change: Ändra all: Alla staff: Staff more: More inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: Användare filter: placeholder: "Filter by name, user:id" set_new_password: Set new password edit_profile: Edit profile change_status: Ändra status change_role: Ändra roll show_logs: Visa loggar add_user: Lägg till användare deactivate_user: title: Deactivate user content: An inactive user must re-validate their email. delete_user: title: Delete this user content: Are you sure you want to delete this user? This is permanent! remove: Remove their content label: Remove all questions, answers, comments, etc. text: Don’t check this if you wish to only delete the user’s account. suspend_user: title: Suspend this user content: A suspended user can't log in. label: How long will the user be suspended for? forever: Forever questions: page_title: Questions unlisted: Unlisted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Ändra pending: Pending filter: placeholder: "Filter by title, question:id" answers: page_title: Answers post: Post votes: Votes created: Created status: Status action: Action change: Ändra filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Ange en giltig URL. text: The address of your site. short_desc: label: Short site description msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site description msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. check_update: label: Software updates text: Automatically check for updates interface: page_title: Interface language: label: Interface language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Tidszon msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: From email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Kryptering msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL tls: TLS none: Ingen smtp_port: label: SMTP port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP username msg: SMTP username cannot be empty. smtp_password: label: SMTP password msg: SMTP password cannot be empty. test_email_recipient: label: Test email recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Aktivera autentisering title: SMTP authentication msg: SMTP authentication cannot be empty. "yes": "Ja" "no": "Nej" branding: page_title: Branding logo: label: Logo msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile logo text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square icon msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Användarvillkor text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Integritetspolicy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for each question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Permalänk text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Teman themes: label: Teman text: Select an existing theme. color_scheme: label: Färgschema navbar_style: label: Navbar background style primary_color: label: Primary color text: Modify the colors used by your themes layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS och HTML custom_css: label: Anpassad CSS text: > head: label: Head text: > header: label: Header text: > footer: label: Footer text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: Login membership: title: Medlemskap label: Tillåt nya registreringar text: Turn off to prevent anyone from creating a new account. email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: Allowed email domains text: Email domains that users must register accounts with. One domain per line. Ignored when empty. private: title: Private label: Login required text: Only logged in users can access this community. password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: Alla active: Aktiv inactive: Inaktiv outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Namn version: Version status: Status action: Action deactivate: Deactivate activate: Aktivera settings: Inställningar settings_users: title: Användare avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Tillåt användare att ändra sitt visningsnamn allow_update_username: label: Tillåt användare att ändra sitt användarnamn allow_update_avatar: label: Tillåt användare att ändra sin profilbild allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Aktiv activate: Aktivera all: Alla awards: Awards deactivate: Inaktivera filter: placeholder: Filter by name, badge:id group: Grupp inactive: Inaktiv name: Namn show_logs: Visa loggar status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optional) empty: cannot be empty invalid: is invalid btn_submit: Spara not_found_props: "Required property {{ key }} not found." select: Select page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Redigera tagg empty: No review tasks left. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created pin: pinned unpin: unpinned show: listed hide: unlisted title: "History for" tag_title: "Timeline for" show_votes: "Visa röster" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Användare users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: Our community staff reputation: reputation votes: röster prompt: leave_page: Are you sure you want to leave the page? changes_not_save: Dina ändringar kanske inte sparas. draft: discard_confirm: Are you sure you want to discard your draft? messages: post_deleted: This post has been deleted. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/te_IN.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: విజయవంతమైంది. unknown: other: తెలియని సమస్య. request_format_error: other: Request format is not valid. unauthorized_error: other: Unauthorized. database_error: other: Data server error. forbidden_error: other: Forbidden. duplicate_request_error: other: Duplicate submission. action: report: other: Flag edit: other: Edit delete: other: Delete close: other: Close reopen: other: Reopen forbidden_error: other: Forbidden. pin: other: Pin hide: other: Unlist unpin: other: Unpin show: other: List invite_someone_to_answer: other: Edit undelete: other: Undelete merge: other: Merge role: name: user: other: User admin: other: Admin moderator: other: Moderator description: user: other: Default with no special access. admin: other: Have the full power to access the site. moderator: other: Has access to all posts except admin settings. privilege: level_1: description: other: Level 1 (less reputation required for private team, group) level_2: description: other: Level 2 (low reputation required for startup community) level_3: description: other: Level 3 (high reputation required for mature community) level_custom: description: other: Custom Level rank_question_add_label: other: Ask question rank_answer_add_label: other: Write answer rank_comment_add_label: other: Write comment rank_report_add_label: other: Flag rank_comment_vote_up_label: other: Upvote comment rank_link_url_limit_label: other: Post more than 2 links at a time rank_question_vote_up_label: other: Upvote question rank_answer_vote_up_label: other: Upvote answer rank_question_vote_down_label: other: Downvote question rank_answer_vote_down_label: other: Downvote answer rank_invite_someone_to_answer_label: other: Invite someone to answer rank_tag_add_label: other: Create new tag rank_tag_edit_label: other: Edit tag description (need to review) rank_question_edit_label: other: Edit other's question (need to review) rank_answer_edit_label: other: Edit other's answer (need to review) rank_question_edit_without_review_label: other: Edit other's question without review rank_answer_edit_without_review_label: other: Edit other's answer without review rank_question_audit_label: other: Review question edits rank_answer_audit_label: other: Review answer edits rank_tag_audit_label: other: Review tag edits rank_tag_edit_without_review_label: other: Edit tag description without review rank_tag_synonym_label: other: Manage tag synonyms email: other: Email e_mail: other: Email password: other: Password pass: other: Password old_pass: other: Current password original_text: other: This post email_or_password_wrong_error: other: Email and password do not match. error: common: invalid_url: other: Invalid URL. status_invalid: other: Invalid status. password: space_invalid: other: Password cannot contain spaces. admin: cannot_update_their_password: other: You cannot modify your password. cannot_edit_their_profile: other: You cannot modify your profile. cannot_modify_self_status: other: You cannot modify your status. email_or_password_wrong: other: Email and password do not match. answer: not_found: other: Answer do not found. cannot_deleted: other: No permission to delete. cannot_update: other: No permission to update. question_closed_cannot_add: other: Questions are closed and cannot be added. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Comment are not allowed to edit. not_found: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: Email already exists. need_to_be_verified: other: Email should be verified. verify_url_expired: other: Email verified URL has expired, please resend the email. illegal_email_domain_error: other: Email is not allowed from that email domain. Please use another one. lang: not_found: other: Language file not found. object: captcha_verification_failed: other: Captcha wrong. disallow_follow: other: You are not allowed to follow. disallow_vote: other: You are not allowed to vote. disallow_vote_your_self: other: You can't vote for your own post. not_found: other: Object not found. verification_failed: other: Verification failed. email_or_password_incorrect: other: Email and password do not match. old_password_verification_failed: other: The old password verification failed new_password_same_as_previous_setting: other: The new password is the same as the previous one. already_deleted: other: This post has been deleted. meta: object_not_found: other: Meta object not found question: already_deleted: other: This post has been deleted. under_review: other: Your post is awaiting review. It will be visible after it has been approved. not_found: other: Question not found. cannot_deleted: other: No permission to delete. cannot_close: other: No permission to close. cannot_update: other: No permission to update. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. vote_fail_to_meet_the_condition: other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. no_enough_rank_to_operate: other: You need at least {{.Rank}} reputation to do this. report: handle_failed: other: Report handle failed. not_found: other: Report not found. tag: already_exist: other: Tag already exists. not_found: other: Tag not found. recommend_tag_not_found: other: Recommend tag is not exist. recommend_tag_enter: other: Please enter at least one required tag. not_contain_synonym_tags: other: Should not contain synonym tags. cannot_update: other: No permission to update. is_used_cannot_delete: other: You cannot delete a tag that is in use. cannot_set_synonym_as_itself: other: You cannot set the synonym of the current tag as itself. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: The from name cannot be a email address. theme: not_found: other: Theme not found. revision: review_underway: other: Can't edit currently, there is a version in the review queue. no_permission: other: No permission to revise. user: external_login_missing_user_id: other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. external_login_unbinding_forbidden: other: Please set a login password for your account before you remove this login. email_or_password_wrong: other: other: Email and password do not match. not_found: other: User not found. suspended: other: User has been suspended. username_invalid: other: Username is invalid. username_duplicate: other: Username is already in use. set_avatar: other: Avatar set failed. cannot_update_your_role: other: You cannot modify your role. not_allowed_registration: other: Currently the site is not open for registration. not_allowed_login_via_password: other: Currently the site is not allowed to login via password. access_denied: other: Access denied page_access_denied: other: You do not have access to this page. add_bulk_users_format_error: other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Read config failed database: connection_failed: other: Database connection failed create_table_failed: other: Create table failed install: create_config_failed: other: Can't create the config.yaml file. upload: unsupported_file_format: other: Unsupported file format. site_info: config_not_found: other: Site config not found. badge: object_not_found: other: Badge object not found reason: spam: name: other: spam desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude_or_abusive: name: other: rude or abusive desc: other: "A reasonable person would find this content inappropriate for respectful discourse." a_duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. placeholder: other: Enter the existing question link not_a_answer: name: other: not an answer desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." no_longer_needed: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. something: name: other: something else desc: other: This post requires staff attention for another reason not listed above. placeholder: other: Let us know specifically what you are concerned about community_specific: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. not_clarity: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. looks_ok: name: other: looks OK desc: other: This post is good as-is and not low quality. needs_edit: name: other: needs edit, and I did it desc: other: Improve and correct problems with this post yourself. needs_close: name: other: needs close desc: other: A closed question can't answer, but still can edit, vote and comment. needs_delete: name: other: needs delete desc: other: This post will be deleted. question: close: duplicate: name: other: spam desc: other: This question has been asked before and already has an answer. guideline: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. multiple: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: something else desc: other: This post requires another reason not listed above. operation_type: asked: other: asked answered: other: answered modified: other: modified deleted_title: other: Deleted question questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: updated question answer_the_question: other: answered question update_answer: other: updated answer accept_answer: other: accepted answer comment_question: other: commented question comment_answer: other: commented answer reply_to_you: other: replied to you mention_you: other: mentioned you your_question_is_closed: other: Your question has been closed your_question_was_deleted: other: Your question has been deleted your_answer_was_deleted: other: Your answer has been deleted your_comment_was_deleted: other: Your comment has been deleted up_voted_question: other: upvoted question down_voted_question: other: downvoted question up_voted_answer: other: upvoted answer down_voted_answer: other: downvoted answer up_voted_comment: other: upvoted comment invited_you_to_answer: other: invited you to answer earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "[{{.SiteName}}] Confirm your new email address" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Password reset" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Confirm your new account" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Test Email" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: upvote upvoted: other: upvoted downvote: other: downvote downvoted: other: downvoted accept: other: accept accepted: other: accepted edit: other: edit review: queued_post: other: Queued post flagged_post: other: Flagged post suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: Editor desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: First Link desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: Reader desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: Welcome desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: Thank You desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: మునుపటి next: Next page_title: question: ప్రశ్న questions: ప్రశ్నలు tag: ట్యాగ్ tags: టాగ్లు tag_wiki: tag wiki create_tag: ట్యాగ్‌ని సృష్టించండి edit_tag: ట్యాగ్‌ని సవరించండి ask_a_question: Create Question edit_question: ప్రశ్నను సవరించండి edit_answer: సమాధానాన్ని సవరించండి search: Search posts_containing: కలిగి ఉన్న పోస్ట్‌లు settings: Settings notifications: నోటిఫికేషన్‌లు login: Log In sign_up: Sign Up account_recovery: Account Recovery account_activation: Account Activation confirm_email: Confirm Email account_suspended: ఖాతా నిలిపివేయబడింది admin: అడ్మిన్ change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade maintenance: Website Maintenance users: Users oauth_callback: Processing http_404: HTTP Error 404 http_50X: HTTP Error 500 http_403: HTTP Error 403 logout: Log Out posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: నోటిఫికేషన్లు inbox: ఇన్‌బాక్స్ achievement: విజయాలు new_alerts: New alerts all_read: Mark all as read show_more: Show more someone: Someone inbox_type: all: All posts: Posts invites: Invites votes: Votes answer: Answer question: Question badge_award: Badge suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." forever: This user was suspended forever. end: You don't meet a community guideline. contact_us: Contact us editor: blockquote: text: Blockquote bold: text: Strong chart: text: Chart flow_chart: Flow chart sequence_diagram: Sequence diagram class_diagram: Class diagram state_diagram: State diagram entity_relationship_diagram: Entity relationship diagram user_defined_diagram: User defined diagram gantt_chart: Gantt chart pie_chart: Pie chart code: text: Code Sample add_code: Add code sample form: fields: code: label: Code msg: empty: Code cannot be empty. language: label: Language placeholder: Automatic detection btn_cancel: Cancel btn_confirm: Add formula: text: Formula options: inline: Inline formula block: Block formula heading: text: Heading options: h1: Heading 1 h2: Heading 2 h3: Heading 3 h4: Heading 4 h5: Heading 5 h6: Heading 6 help: text: Help hr: text: Horizontal rule image: text: Image add_image: Add image tab_image: Upload image form_image: fields: file: label: Image file btn: Select image msg: empty: File cannot be empty. only_image: Only image files are allowed. max_size: File size cannot exceed {{size}} MB. desc: label: Description tab_url: Image URL form_url: fields: url: label: Image URL msg: empty: Image URL cannot be empty. name: label: Description btn_cancel: Cancel btn_confirm: Add uploading: Uploading indent: text: Indent outdent: text: Outdent italic: text: Emphasis link: text: Hyperlink add_link: Add hyperlink form: fields: url: label: URL msg: empty: URL cannot be empty. name: label: Description btn_cancel: Cancel btn_confirm: Add ordered_list: text: Numbered list unordered_list: text: Bulleted list table: text: Table heading: Heading cell: Cell file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: I am closing this post as... btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. report_modal: flag_title: I am flagging to report this post as... close_title: I am closing this post as... review_question_title: Review question review_answer_title: Review answer review_comment_title: Review comment btn_cancel: Cancel btn_submit: Submit remark: empty: Cannot be empty. msg: empty: Please select a reason. not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: Create new tag form: fields: display_name: label: Display name msg: empty: Display name cannot be empty. range: Display name up to 35 characters. slug_name: label: URL slug desc: URL slug up to 35 characters. msg: empty: URL slug cannot be empty. range: URL slug up to 35 characters. character: URL slug contains unallowed character set. desc: label: Description revision: label: Revision edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_cancel: Cancel btn_submit: Submit btn_post: Post new tag tag_info: created_at: Created edited_at: Edited history: History synonyms: title: Synonyms text: The following tags will be remapped to empty: No synonyms found. btn_add: Add a synonym btn_edit: Edit btn_save: Save synonyms_text: The following tags will be remapped to delete: title: Delete this tag tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: Are you sure you wish to delete? close: Close merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Edit Tag default_reason: Edit tag default_first_reason: Add tag btn_save_edits: Save edits btn_cancel: Cancel dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: now x_seconds_ago: "{{count}}s ago" x_minutes_ago: "{{count}}m ago" x_hours_ago: "{{count}}h ago" hour: hour day: day hours: hours days: days month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: Add comment reply_to: Reply to btn_reply: Reply btn_edit: Edit btn_delete: Delete btn_flag: Flag btn_save_edits: Save edits btn_cancel: Cancel show_more: "{{count}} more comments" tip_question: >- Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. tip_vote: It adds something useful to the post edit_answer: title: Edit Answer default_reason: Edit answer default_first_reason: Add answer form: fields: revision: label: Revision answer: label: Answer feedback: characters: content must be at least 6 characters in length. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_save_edits: Save edits btn_cancel: Cancel tags: title: Tags sort_buttons: popular: Popular name: Name newest: Newest button_follow: Follow button_following: Following tag_label: questions search_placeholder: Filter by tag name no_desc: The tag has no description. more: More wiki: Wiki ask: title: Create Question edit_title: Edit Question default_reason: Edit question default_first_reason: Create question similar_questions: Similar questions form: fields: revision: label: Revision title: label: Title placeholder: What's your topic? Be specific. msg: empty: Title cannot be empty. range: Title up to 150 characters body: label: Body msg: empty: Body cannot be empty. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Tags msg: empty: Tags cannot be empty. answer: label: Answer msg: empty: Answer cannot be empty. edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_post_question: Post your question btn_save_edits: Save edits answer_question: Answer your own question post_question&answer: Post your question and answer tag_selector: add_btn: Add tag create_btn: Create new tag search_tag: Search tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: No tags matched tag_required_text: Required tag (at least one) header: nav: question: Questions tag: Tags user: Users badges: Badges profile: Profile setting: Settings logout: Log out admin: Admin review: Review bookmark: Bookmarks moderation: Moderation search: placeholder: Search footer: build_on: Powered by <1> Apache Answer upload_img: name: Change loading: loading... pic_auth_code: title: Captcha placeholder: Type the text above msg: empty: Captcha cannot be empty. inactive: first: >- You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. info: "If it doesn't arrive, check your spam folder." another: >- We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. btn_name: Resend activation email change_btn_name: Change email msg: empty: Cannot be empty. resend_email: url_label: Are you sure you want to resend the activation email? url_text: You can also give the activation link above to the user. login: login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. forgot_pass: Forgot password? name: label: Name msg: empty: Name cannot be empty. range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Email msg: empty: Email cannot be empty. password: label: Password msg: empty: Password cannot be empty. different: The passwords entered on both sides are inconsistent account_forgot: page_title: Forgot Your Password btn_name: Send me recovery email send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: Email msg: empty: Email cannot be empty. change_email: btn_cancel: Cancel btn_update: Update email address send_success: >- If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. email: label: New email msg: empty: Email cannot be empty. oauth: connect: Connect with {{ auth_name }} remove: Remove {{ auth_name }} oauth_bind_email: subtitle: Add a recovery email to your account. btn_update: Update email address email: label: Email msg: empty: Email cannot be empty. modal_title: Email already existes. modal_content: This email address already registered. Are you sure you want to connect to the existing account? modal_cancel: Change email modal_confirm: Connect to the existing account password_reset: page_title: Password Reset btn_name: Reset my password reset_success: >- You successfully changed your password; you will be redirected to the log in page. link_invalid: >- Sorry, this password reset link is no longer valid. Perhaps your password is already reset? to_login: Continue to log in page password: label: Password msg: empty: Password cannot be empty. length: The length needs to be between 8 and 32 different: The passwords entered on both sides are inconsistent password_confirm: label: Confirm new password settings: page_title: Settings goto_modify: Go to modify nav: profile: Profile notification: Notifications account: Account interface: Interface profile: heading: Profile btn_name: Save display_name: label: Display name msg: Display name cannot be empty. msg_range: Display name must be 2-30 characters in length. username: label: Username caption: People can mention you as "@username". msg: Username cannot be empty. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile image gravatar: Gravatar gravatar_text: You can change image on custom: Custom custom_text: You can upload your image. default: System msg: Please upload an avatar bio: label: About me website: label: Website placeholder: "https://example.com" msg: Website incorrect format location: label: Location placeholder: "City, Country" notification: heading: Email Notifications turn_on: Turn on inbox: label: Inbox notifications description: Answers to your questions, comments, invites, and more. all_new_question: label: All new questions description: Get notified of all new questions. Up to 50 questions per week. all_new_question_for_following_tags: label: All new questions for following tags description: Get notified of new questions for following tags. account: heading: Account change_email_btn: Change email change_pass_btn: Change password change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: label: Email new_email: label: New email msg: New email cannot be empty. pass: label: Current password msg: Password cannot be empty. password_title: Password current_pass: label: Current password msg: empty: Current password cannot be empty. length: The length needs to be between 8 and 32. different: The two entered passwords do not match. new_pass: label: New password pass_confirm: label: Confirm new password interface: heading: Interface lang: label: Interface language text: User interface language. It will change when you refresh the page. my_logins: title: My logins label: Log in or sign up on this site using these accounts. modal_title: Remove login modal_content: Are you sure you want to remove this login from your account? modal_confirm_btn: Remove remove_success: Removed successfully toast: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. sent_success: Sent successfully related_question: title: Related answers: answers linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: People Asked desc: Select people who you think might know the answer. invite: Invite to answer add: Add people search: Search people question_detail: action: Action created: Created Asked: Asked asked: asked update: Modified Edited: Edited edit: edited commented: commented Views: Viewed Follow: Follow Following: Following follow_tip: Follow this question to receive notifications answered: answered closed_in: Closed in show_exist: Show existing question. useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: title: Answers score: Score newest: Newest oldest: Oldest btn_accept: Accept btn_accepted: Accepted write_answer: title: Your Answer edit_answer: Edit my existing answer btn_name: Post your answer add_another_answer: Add another answer confirm_title: Continue to answer continue: Continue confirm_info: >-

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

empty: Answer cannot be empty. characters: content must be at least 6 characters in length. tips: header_1: Thanks for your answer li1_1: Please be sure to answer the question. Provide details and share your research. li1_2: Back up any statements you make with references or personal experience. header_2: But avoid ... li2_1: Asking for help, seeking clarification, or responding to other answers. reopen: confirm_btn: Reopen title: Reopen this post content: Are you sure you want to reopen? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. confirm_btn: Pin delete: title: Delete this post question: >- We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? answer_accepted: >-

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? tip_answer_deleted: This answer has been deleted undelete_title: Undelete this post undelete_desc: Are you sure you wish to undelete? btns: confirm: Confirm cancel: Cancel edit: Edit save: Save delete: Delete undelete: Undelete list: List unlist: Unlist unlisted: Unlisted login: Log in signup: Sign up logout: Log out verify: Verify create: Create approve: Approve reject: Reject skip: Skip discard_draft: Discard draft pinned: Pinned all: All question: Question answer: Answer comment: Comment refresh: Refresh resend: Resend deactivate: Deactivate active: Active suspend: Suspend unsuspend: Unsuspend close: Close reopen: Reopen ok: OK light: Light dark: Dark system_setting: System setting default: Default reset: Reset tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Search Results keywords: Keywords options: Options follow: Follow following: Following counts: "{{count}} Results" counts_loading: "... Results" more: More sort_btns: relevance: Relevance newest: Newest active: Active score: Score more: More tips: title: Advanced Search Tips tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" question: "<1>is:question search questions" is_answer: "<1>is:answer search answers" empty: We couldn't find anything.
Try different or less specific keywords. share: name: Share copy: Copy link via: Share post via... copied: Copied facebook: Share to Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Error... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage oops: Oops! invalid: The link you used no longer works. confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? unsubscribe: page_title: Unsubscribe success_title: Unsubscribe Successful success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. link: Change settings question: following_tags: Following Tags edit: Edit save: Save follow_tag_tip: Follow tags to curate your list of questions. hot_questions: Hot Questions all_questions: All Questions x_questions: "{{ count }} Questions" x_answers: "{{ count }} answers" x_posts: "{{ count }} Posts" questions: Questions answers: Answers newest: Newest active: Active hot: Hot frequent: Frequent recommend: Recommend score: Score unanswered: Unanswered modified: modified answered: answered asked: asked closed: closed follow_a_tag: Follow a tag more: More personal: overview: Overview answers: Answers answer: answer questions: Questions question: question bookmarks: Bookmarks reputation: Reputation comments: Comments votes: Votes badges: Badges newest: Newest score: Score edit_profile: Edit profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined comma: "," last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" top_answers: Top Answers top_questions: Top Questions stats: Stats list_empty: No posts found.
Perhaps you'd like to select a different tab? content_empty: No posts found. accepted: Accepted answered: answered asked: asked downvoted: downvoted mod_short: MOD mod_long: Moderators x_reputation: reputation x_votes: votes received x_answers: answers x_questions: questions recent_badges: Recent Badges install: title: Installation next: Next done: Done config_yaml_error: Can't create the config.yaml file. lang: label: Please choose a language db_type: label: Database engine db_username: label: Username placeholder: root msg: Username cannot be empty. db_password: label: Password placeholder: root msg: Password cannot be empty. db_host: label: Database host placeholder: "db:3306" msg: Database host cannot be empty. db_name: label: Database name placeholder: answer msg: Database name cannot be empty. db_file: label: Database file placeholder: /data/answer.db msg: Database file cannot be empty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Create config.yaml label: The config.yaml file created. desc: >- You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information admin_account: Admin Account site_name: label: Site name msg: Site name cannot be empty. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: Site URL text: The address of your site. msg: empty: Site URL cannot be empty. incorrect: Site URL incorrect format. max_length: Site URL must be at maximum 512 characters in length. contact_email: label: Contact email text: Email address of key contact responsible for this site. msg: empty: Contact email cannot be empty. incorrect: Contact email incorrect format. login_required: label: Private switch: Login required text: Only logged in users can access this community. admin_name: label: Name msg: Name cannot be empty. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Password text: >- You will need this password to log in. Please store it in a secure location. msg: Password cannot be empty. msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: You will need this email to log in. msg: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your site is ready ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning warn_desc: >- The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now. installed: Already installed installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: views votes: votes answers: answers accepted: Accepted page_error: http_error: HTTP Error {{ code }} desc_403: You don't have permission to access this page. desc_404: Unfortunately, this page doesn't exist. desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "We are under maintenance, we'll be back soon." nav_menus: dashboard: Dashboard contents: Contents questions: Questions answers: Answers users: Users badges: Badges flags: Flags settings: Settings general: General interface: Interface smtp: SMTP branding: Branding legal: Legal write: Write terms: Terms tos: Terms of Service privacy: Privacy seo: SEO customize: Customize themes: Themes login: Login privileges: Privileges plugins: Plugins installed_plugins: Installed Plugins apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Welcome to {{site_name}} user_center: login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: Admin dashboard: title: Dashboard welcome: Welcome to Admin! site_statistics: Site statistics questions: "Questions:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "Answers:" comments: "Comments:" votes: "Votes:" users: "Users:" flags: "Flags:" reviews: "Reviews:" site_health: Site health version: "Version:" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Private public: Public smtp: "SMTP:" timezone: "Timezone:" system_info: System info go_version: "Go version:" database: "Database:" database_size: "Database size:" storage_used: "Storage used:" uptime: "Uptime:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Contact forum: Forum documents: Documents feedback: Feedback support: Support review: Review config: Config update_to: Update to latest: Latest check_failed: Check failed "yes": "Yes" "no": "No" not_allowed: Not allowed allowed: Allowed enabled: Enabled disabled: Disabled writable: Writable not_writable: Not writable flags: title: Flags pending: Pending completed: Completed flagged: Flagged flagged_type: Flagged {{ type }} created: Created action: Action review: Review user_role_modal: title: Change user role to... btn_cancel: Cancel btn_submit: Submit new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: users: label: Bulk add user placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separate “name, email, password” with commas. One user per line. msg: "Please enter the user's email, one per line." display_name: label: Display name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit users: title: Users name: Name email: Email reputation: Reputation created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Status role: Role action: Action change: Change all: All staff: Staff more: More inactive: Inactive suspended: Suspended deleted: Deleted normal: Normal Moderator: Moderator Admin: Admin User: User filter: placeholder: "Filter by name, user:id" set_new_password: Set new password edit_profile: Edit profile change_status: Change status change_role: Change role show_logs: Show logs add_user: Add user deactivate_user: title: Deactivate user content: An inactive user must re-validate their email. delete_user: title: Delete this user content: Are you sure you want to delete this user? This is permanent! remove: Remove their content label: Remove all questions, answers, comments, etc. text: Don’t check this if you wish to only delete the user’s account. suspend_user: title: Suspend this user content: A suspended user can't log in. label: How long will the user be suspended for? forever: Forever questions: page_title: Questions unlisted: Unlisted post: Post votes: Votes answers: Answers created: Created status: Status action: Action change: Change pending: Pending filter: placeholder: "Filter by title, question:id" answers: page_title: Answers post: Post votes: Votes created: Created status: Status action: Action change: Change filter: placeholder: "Filter by title, answer:id" general: page_title: General name: label: Site name msg: Site name cannot be empty. text: "The name of this site, as used in the title tag." site_url: label: Site URL msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. short_desc: label: Short site description msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." desc: label: Site description msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." contact_email: label: Contact email msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. check_update: label: Software updates text: Automatically check for updates interface: page_title: Interface language: label: Interface language msg: Interface language cannot be empty. text: User interface language. It will change when you refresh the page. time_zone: label: Timezone msg: Timezone cannot be empty. text: Choose a city in the same timezone as you. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: From email msg: From email cannot be empty. text: The email address which emails are sent from. from_name: label: From name msg: From name cannot be empty. text: The name which emails are sent from. smtp_host: label: SMTP host msg: SMTP host cannot be empty. text: Your mail server. encryption: label: Encryption msg: Encryption cannot be empty. text: For most servers SSL is the recommended option. ssl: SSL tls: TLS none: None smtp_port: label: SMTP port msg: SMTP port must be number 1 ~ 65535. text: The port to your mail server. smtp_username: label: SMTP username msg: SMTP username cannot be empty. smtp_password: label: SMTP password msg: SMTP password cannot be empty. test_email_recipient: label: Test email recipients text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: label: Enable authentication title: SMTP authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" branding: page_title: Branding logo: label: Logo msg: Logo cannot be empty. text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: label: Mobile logo text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: label: Square icon msg: Square icon cannot be empty. text: Image used as the base for metadata icons. Should ideally be larger than 512x512. favicon: label: Favicon text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: label: Terms of service text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." privacy_policy: label: Privacy policy text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for each question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "Every new question must have at least one recommend tag." reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: SEO permalink: label: Permalink text: Custom URL structures can improve the usability, and forward-compatibility of your links. robots: label: robots.txt text: This will permanently override any related site settings. themes: page_title: Themes themes: label: Themes text: Select an existing theme. color_scheme: label: Color scheme navbar_style: label: Navbar background style primary_color: label: Primary color text: Modify the colors used by your themes layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS and HTML custom_css: label: Custom CSS text: > head: label: Head text: > header: label: Header text: > footer: label: Footer text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: Login membership: title: Membership label: Allow new registrations text: Turn off to prevent anyone from creating a new account. email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: Allowed email domains text: Email domains that users must register accounts with. One domain per line. Ignored when empty. private: title: Private label: Login required text: Only logged in users can access this community. password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: All active: Active inactive: Inactive outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Name version: Version status: Status action: Action deactivate: Deactivate activate: Activate settings: Settings settings_users: title: Users avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Allow users to change their display name allow_update_username: label: Allow users to change their username allow_update_avatar: label: Allow users to change their profile image allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (optional) empty: cannot be empty invalid: is invalid btn_submit: Save not_found_props: "Required property {{ key }} not found." select: Select page_review: review: Review proposed: proposed question_edit: Question edit answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary edit_question: Edit question edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: undeleted deleted: deleted downvote: downvote upvote: upvote accept: accept cancelled: cancelled commented: commented rollback: rollback edited: edited answered: answered asked: asked closed: closed reopened: reopened created: created pin: pinned unpin: unpinned show: listed hide: unlisted title: "History for" tag_title: "Timeline for" show_votes: "Show votes" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" title_for_tag: "Timeline for tag" datetime: Datetime type: Type by: By comment: Comment no_data: "We couldn't find anything." users: title: Users users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: Our community staff reputation: reputation votes: votes prompt: leave_page: Are you sure you want to leave the page? changes_not_save: Your changes may not be saved. draft: discard_confirm: Are you sure you want to discard your draft? messages: post_deleted: This post has been deleted. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/tr_TR.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Başarılı. unknown: other: Bilinmeyen hata. request_format_error: other: İstek formatı geçerli değil. unauthorized_error: other: Yetkisiz erişim. database_error: other: Veri tabanı sunucu hatası. forbidden_error: other: Erişim engellendi. duplicate_request_error: other: Yinelenen gönderim. action: report: other: Bildir edit: other: Düzenle delete: other: Sil close: other: Kapat reopen: other: Yeniden Aç forbidden_error: other: Erişim engellendi. pin: other: Sabitle hide: other: Listeden Kaldır unpin: other: Sabitlemeyi Kaldır show: other: Listele invite_someone_to_answer: other: Cevaplamaya Davet Et undelete: other: Silmeyi Geri Al merge: other: Birleştir role: name: user: other: Kullanıcı admin: other: Yönetici moderator: other: Moderatör description: user: other: Özel erişimi olmayan varsayılan kullanıcı. admin: other: Siteye tam erişim gücüne sahiptir. moderator: other: Yönetici ayarları dışında tüm gönderilere erişebilir. privilege: level_1: description: other: Seviye 1 (özel takım, grup için daha az itibar gerektirir) level_2: description: other: Seviye 2 (başlangıç topluluğu için düşük itibar gerektirir) level_3: description: other: Seviye 3 (gelişmiş topluluk için yüksek itibar gerektirir) level_custom: description: other: Özel Seviye rank_question_add_label: other: Soru sor rank_answer_add_label: other: Cevap yaz rank_comment_add_label: other: Yorum yaz rank_report_add_label: other: Bildir rank_comment_vote_up_label: other: Yorumu yukarı oyla rank_link_url_limit_label: other: Bir seferde 2'den fazla bağlantı paylaş rank_question_vote_up_label: other: Soruyu yukarı oyla rank_answer_vote_up_label: other: Cevabı yukarı oyla rank_question_vote_down_label: other: Soruyu aşağı oyla rank_answer_vote_down_label: other: Cevabı aşağı oyla rank_invite_someone_to_answer_label: other: Birini cevaplamaya davet et rank_tag_add_label: other: Yeni etiket oluştur rank_tag_edit_label: other: Etiket açıklamasını düzenle (inceleme gerekli) rank_question_edit_label: other: Başkasının sorusunu düzenle (inceleme gerekli) rank_answer_edit_label: other: Başkasının cevabını düzenle (inceleme gerekli) rank_question_edit_without_review_label: other: Başkasının sorusunu inceleme olmadan düzenle rank_answer_edit_without_review_label: other: Başkasının cevabını inceleme olmadan düzenle rank_question_audit_label: other: Soru düzenlemelerini incele rank_answer_audit_label: other: Cevap düzenlemelerini incele rank_tag_audit_label: other: Etiket düzenlemelerini incele rank_tag_edit_without_review_label: other: Etiket açıklamasını inceleme olmadan düzenle rank_tag_synonym_label: other: Etiket eş anlamlılarını yönet email: other: E-posta e_mail: other: E-posta password: other: Parola pass: other: Parola old_pass: other: Mevcut parola original_text: other: Bu gönderi email_or_password_wrong_error: other: E-posta ve parola eşleşmiyor. error: common: invalid_url: other: Geçersiz URL. status_invalid: other: Geçersiz durum. password: space_invalid: other: Parola boşluk içeremez. admin: cannot_update_their_password: other: Parolanızı değiştiremezsiniz. cannot_edit_their_profile: other: Profilinizi değiştiremezsiniz. cannot_modify_self_status: other: Durumunuzu değiştiremezsiniz. email_or_password_wrong: other: E-posta ve parola eşleşmiyor. answer: not_found: other: Cevap bulunamadı. cannot_deleted: other: Silme izni yok. cannot_update: other: Güncelleme izni yok. question_closed_cannot_add: other: Sorular kapatıldı ve cevap eklenemez. content_cannot_empty: other: Cevap içeriği boş olamaz. comment: edit_without_permission: other: Yorumları düzenleme izniniz yok. not_found: other: Yorum bulunamadı. cannot_edit_after_deadline: other: Yorum süresi çok uzun olduğu için artık düzenlenemez. content_cannot_empty: other: Yorum içeriği boş olamaz. email: duplicate: other: Bu e-posta adresi zaten kullanılmaktadır. need_to_be_verified: other: E-posta doğrulanmalıdır. verify_url_expired: other: E-posta doğrulama URL'sinin süresi dolmuş, lütfen e-postayı yeniden gönderin. illegal_email_domain_error: other: Bu e-posta alan adına izin verilmiyor. Lütfen başka bir e-posta kullanın. lang: not_found: other: Dil dosyası bulunamadı. object: captcha_verification_failed: other: Captcha yanlış. disallow_follow: other: Takip etme izniniz yok. disallow_vote: other: Oy verme izniniz yok. disallow_vote_your_self: other: Kendi gönderinize oy veremezsiniz. not_found: other: Nesne bulunamadı. verification_failed: other: Doğrulama başarısız. email_or_password_incorrect: other: E-posta ve parola eşleşmiyor. old_password_verification_failed: other: Eski parola doğrulaması başarısız oldu. new_password_same_as_previous_setting: other: Yeni parola öncekiyle aynı. already_deleted: other: Bu gönderi silinmiş. meta: object_not_found: other: Meta nesnesi bulunamadı. question: already_deleted: other: Bu gönderi silinmiş. under_review: other: Gönderiniz inceleme bekliyor. Onaylandıktan sonra görünür olacaktır. not_found: other: Soru bulunamadı. cannot_deleted: other: Silme izni yok. cannot_close: other: Kapatma izni yok. cannot_update: other: Güncelleme izni yok. content_cannot_empty: other: İçerik boş olamaz. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: İtibar seviyesi koşulu karşılamıyor. vote_fail_to_meet_the_condition: other: Geri bildiriminiz için teşekkürler. Oy kullanmak için en az {{.Rank}} itibara ihtiyacınız var. no_enough_rank_to_operate: other: Bu işlemi yapmak için en az {{.Rank}} itibara ihtiyacınız var. report: handle_failed: other: Rapor işleme başarısız. not_found: other: Rapor bulunamadı. tag: already_exist: other: Etiket zaten var. not_found: other: Etiket bulunamadı. recommend_tag_not_found: other: Önerilen etiket mevcut değil. recommend_tag_enter: other: Lütfen en az bir adet gerekli etiket giriniz. not_contain_synonym_tags: other: Eş anlamlı etiketler içermemelidir. cannot_update: other: Güncelleme izni yok. is_used_cannot_delete: other: Kullanımda olan bir etiketi silemezsiniz. cannot_set_synonym_as_itself: other: Bir etiketin eş anlamlısını kendisi olarak ayarlayamazsınız. minimum_count: other: Yeterli etiket girilmedi. smtp: config_from_name_cannot_be_email: other: Gönderen adı bir e-posta adresi olamaz. theme: not_found: other: Tema bulunamadı. revision: review_underway: other: Şu anda düzenlenemez, inceleme kuyruğunda bir sürüm var. no_permission: other: Düzenleme izniniz yok. user: external_login_missing_user_id: other: Üçüncü taraf platform benzersiz bir Kullanıcı ID'si sağlamıyor, bu nedenle giriş yapamazsınız. Lütfen site yöneticisiyle iletişime geçin. external_login_unbinding_forbidden: other: Bu girişi kaldırmadan önce lütfen hesabınız için bir giriş parolası ayarlayın. email_or_password_wrong: other: other: E-posta ve parola eşleşmiyor. not_found: other: Kullanıcı bulunamadı. suspended: other: Kullanıcı askıya alındı. username_invalid: other: Kullanıcı adı geçersiz. username_duplicate: other: Kullanıcı adı zaten kullanımda. set_avatar: other: Avatar ayarlama başarısız. cannot_update_your_role: other: Kendi rolünüzü değiştiremezsiniz. not_allowed_registration: other: Şu anda site kayıt için açık değil. not_allowed_login_via_password: other: Şu anda site parola ile giriş yapmaya izin vermiyor. access_denied: other: Erişim reddedildi. page_access_denied: other: Bu sayfaya erişim izniniz yok. add_bulk_users_format_error: other: "{{.Line}} satırındaki '{{.Content}}' içeriğinde {{.Field}} biçimi hatası. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Bir kerede eklediğiniz kullanıcı sayısı 1-{{.MaxAmount}} aralığında olmalıdır." status_suspended_forever: other: "Bu kullanıcı kalıcı olarak uzaklaştırıldı. Bu kullanıcı topluluk yönergelerine uymuyor." status_suspended_until: other: "Bu kullanıcı {{.SuspendedUntil}} tarihine kadar askıya alındı. Bu kullanıcı topluluk yönergelerine uymuyor." status_deleted: other: "Bu kullanıcı silindi." status_inactive: other: "Bu kullanıcı aktif değil." config: read_config_failed: other: Yapılandırma okunamadı. database: connection_failed: other: Veritabanı bağlantısı başarısız. create_table_failed: other: Tablo oluşturma başarısız. install: create_config_failed: other: config.yaml dosyası oluşturulamıyor. upload: unsupported_file_format: other: Desteklenmeyen dosya formatı. site_info: config_not_found: other: Site yapılandırması bulunamadı. badge: object_not_found: other: Rozet nesnesi bulunamadı. reason: spam: name: other: spam desc: other: Bu gönderi bir reklam veya vandalizm. Mevcut konuyla ilgili veya yararlı değil. rude_or_abusive: name: other: kaba veya taciz edici desc: other: "Makul bir kişi bu içeriği saygılı bir iletişim için uygunsuz bulurdu." a_duplicate: name: other: kopya desc: other: Bu soru daha önce sorulmuş ve cevaplandırılmış. placeholder: other: Mevcut soru bağlantısını girin not_a_answer: name: other: cevap değil desc: other: "Bu bir cevap olarak gönderilmiş, ancak soruyu cevaplamaya çalışmıyor. Bir düzenleme, yorum, başka bir soru olabilir veya tamamen silinmesi gerekebilir." no_longer_needed: name: other: artık gerekli değil desc: other: Bu yorum güncelliğini yitirmiş, sohbet niteliğinde veya bu gönderiyle ilgili değil. something: name: other: başka bir şey desc: other: Bu gönderi, yukarıda listelenmeyen başka bir nedenden dolayı personel ilgisi gerektiriyor. placeholder: other: Endişelerinizin ne olduğunu spesifik olarak belirtin community_specific: name: other: topluluk kurallarına aykırı desc: other: Bu soru bir topluluk kılavuzuna uymuyor. not_clarity: name: other: detay veya açıklık gerekiyor desc: other: Bu soru şu anda tek soruda birden fazla soru içeriyor. Sadece tek bir soruna odaklanmalı. looks_ok: name: other: iyi görünüyor desc: other: Bu gönderi olduğu gibi iyi ve düşük kaliteli değil. needs_edit: name: other: düzenleme gerektiriyor ve ben yaptım desc: other: Bu gönderideki sorunları kendiniz düzeltin ve iyileştirin. needs_close: name: other: kapatılması gerekiyor desc: other: Kapatılmış bir soru cevaplanamaz, ancak düzenlenebilir, oylanabilir ve yorum yapılabilir. needs_delete: name: other: silinmesi gerekiyor desc: other: Bu gönderi silinecek. question: close: duplicate: name: other: kopya desc: other: Bu soru daha önce sorulmuş ve cevaplandırılmış. guideline: name: other: topluluk kurallarına aykırı desc: other: Bu soru bir topluluk kılavuzuna uymuyor. multiple: name: other: detay veya açıklık gerekiyor desc: other: Bu soru şu anda tek soruda birden fazla soru içeriyor. Sadece tek bir soruna odaklanmalı. other: name: other: başka bir şey desc: other: Bu gönderi yukarıda listelenmeyen başka bir neden gerektiriyor. operation_type: asked: other: soruldu answered: other: cevaplandı modified: other: değiştirildi deleted_title: other: Silinmiş soru questions_title: other: Sorular tag: tags_title: other: Etiketler no_description: other: Bu etiketin açıklaması yok. notification: action: update_question: other: soruyu güncelledi answer_the_question: other: soruyu cevapladı update_answer: other: cevabı güncelledi accept_answer: other: cevabı kabul etti comment_question: other: soruya yorum yaptı comment_answer: other: cevaba yorum yaptı reply_to_you: other: size yanıt verdi mention_you: other: sizden bahsetti your_question_is_closed: other: Sorunuz kapatıldı your_question_was_deleted: other: Sorunuz silindi your_answer_was_deleted: other: Cevabınız silindi your_comment_was_deleted: other: Yorumunuz silindi up_voted_question: other: soruyu yukarı oyladı down_voted_question: other: soruyu aşağı oyladı up_voted_answer: other: cevabı yukarı oyladı down_voted_answer: other: cevabı aşağı oyladı up_voted_comment: other: yorumu yukarı oyladı invited_you_to_answer: other: sizi cevaplamaya davet etti earned_badge: other: Rozet kazandınız "{{.BadgeName}}" email_tpl: change_email: title: other: "[{{.SiteName}}] Yeni e-posta adresinizi onaylayın" body: other: "{{.SiteName}} için aşağıdaki bağlantıya tıklayarak yeni e-posta adresinizi onaylayın:
\n{{.ChangeEmailUrl}}

\n\nEğer bu değişikliği siz talep etmediyseniz, lütfen bu e-postayı dikkate almayın.

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} sorunuzu cevapladı" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n{{.SiteName}} üzerinde görüntüle

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.

\n\nAbonelikten çık" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} sizi cevaplamaya davet etti" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Cevabı biliyor olabileceğinizi düşünüyorum.

\n{{.SiteName}} üzerinde görüntüle

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.

\n\nAbonelikten çık" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} gönderinize yorum yaptı" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n{{.SiteName}} üzerinde görüntüle

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.

\n\nAbonelikten çık" new_question: title: other: "[{{.SiteName}}] Yeni soru: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.

\n\nAbonelikten çık" pass_reset: title: other: "[{{.SiteName }}] Parola sıfırlama" body: other: "Birisi {{.SiteName}} üzerindeki parolanızı sıfırlamak istedi.

\n\nEğer bu siz değilseniz, bu e-postayı güvenle görmezden gelebilirsiniz.

\n\nYeni bir parola seçmek için aşağıdaki bağlantıya tıklayın:
\n{{.PassResetUrl}}\n

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir." register: title: other: "[{{.SiteName}}] Yeni hesabınızı onaylayın" body: other: "{{.SiteName}} sitesine hoş geldiniz!

\n\nYeni hesabınızı onaylamak ve etkinleştirmek için aşağıdaki bağlantıya tıklayın:
\n{{.RegisterUrl}}

\n\nYukarıdaki bağlantı tıklanabilir değilse, web tarayıcınızın adres çubuğuna kopyalayıp yapıştırmayı deneyin.\n

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir." test: title: other: "[{{.SiteName}}] Test E-postası" body: other: "Bu bir test e-postasıdır.\n

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir." action_activity_type: upvote: other: yukarı oyla upvoted: other: yukarı oyladı downvote: other: aşağı oyla downvoted: other: aşağı oyladı accept: other: kabul et accepted: other: kabul edildi edit: other: düzenle review: queued_post: other: Sıradaki gönderi flagged_post: other: Bildirilen gönderi suggested_post_edit: other: Önerilen düzenlemeler reaction: tooltip: other: "{{ .Names }} ve {{ .Count }} kişi daha..." badge: default_badges: autobiographer: name: other: Otobiyografi Yazarı desc: other: Profil bilgilerini doldurdu. certified: name: other: Sertifikalı desc: other: Yeni kullanıcı eğitimimizi tamamladı. editor: name: other: Editör desc: other: İlk gönderi düzenlemesi. first_flag: name: other: İlk Bildirim desc: other: İlk kez bir gönderiyi bildirdi. first_upvote: name: other: İlk Yukarı Oylama desc: other: İlk kez bir gönderiyi yukarı oyladı. first_link: name: other: İlk Bağlantı desc: other: İlk kez başka bir gönderiye bağlantı ekledi. first_reaction: name: other: İlk Tepki desc: other: İlk kez bir gönderiye tepki verdi. first_share: name: other: İlk Paylaşım desc: other: İlk kez bir gönderi paylaştı. scholar: name: other: Bilim İnsanı desc: other: Bir soru sordu ve bir cevabı kabul etti. commentator: name: other: Yorumcu desc: other: 5 yorum bıraktı. new_user_of_the_month: name: other: Ayın Yeni Kullanıcısı desc: other: İlk aylarında üstün katkılarda bulundu. read_guidelines: name: other: Kuralları Okuyan desc: other: '[Topluluk kurallarını] oku.' reader: name: other: Okuyucu desc: other: 10'dan fazla cevap içeren bir konudaki tüm cevapları okudu. welcome: name: other: Hoş Geldin desc: other: Bir yukarı oy aldı. nice_share: name: other: Güzel Paylaşım desc: other: 25 farklı ziyaretçi tarafından görüntülenen bir gönderi paylaştı. good_share: name: other: İyi Paylaşım desc: other: 300 farklı ziyaretçi tarafından görüntülenen bir gönderi paylaştı. great_share: name: other: Harika Paylaşım desc: other: 1000 farklı ziyaretçi tarafından görüntülenen bir gönderi paylaştı. out_of_love: name: other: Sevgiden desc: other: Bir günde 50 yukarı oy kullandı. higher_love: name: other: Yüksek Sevgi desc: other: Bir günde 50 yukarı oyu 5 kez kullandı. crazy_in_love: name: other: Çılgınca Aşık desc: other: Bir günde 50 yukarı oyu 20 kez kullandı. promoter: name: other: Destekçi desc: other: Bir kullanıcıyı davet etti. campaigner: name: other: Kampanyacı desc: other: 3 temel kullanıcıyı davet etti. champion: name: other: Şampiyon desc: other: 5 üye davet etti. thank_you: name: other: Teşekkürler desc: other: 20 yukarı oy aldı ve 10 yukarı oy verdi. gives_back: name: other: Karşılık Veren desc: other: 100 yukarı oy aldı ve 100 yukarı oy verdi. empathetic: name: other: Empatik desc: other: 500 yukarı oy aldı ve 1000 yukarı oy verdi. enthusiast: name: other: Hevesli desc: other: 10 gün üst üste ziyaret etti. aficionado: name: other: Meraklı desc: other: 100 gün üst üste ziyaret etti. devotee: name: other: Hayran desc: other: 365 gün üst üste ziyaret etti. anniversary: name: other: Yıldönümü desc: other: Bir yıl boyunca aktif üye, en az bir gönderi paylaştı. appreciated: name: other: Takdir Edilen desc: other: 20 gönderisinde 1 yukarı oy aldı. respected: name: other: Saygın desc: other: 100 gönderisinde 2 yukarı oy aldı. admired: name: other: Hayranlık Uyandıran desc: other: 300 gönderisinde 5 yukarı oy aldı. solved: name: other: Çözüldü desc: other: Bir cevabı kabul edildi. guidance_counsellor: name: other: Rehber Danışman desc: other: 10 cevabı kabul edildi. know_it_all: name: other: Her Şeyi Bilen desc: other: 50 cevabı kabul edildi. solution_institution: name: other: Çözüm Kurumu desc: other: 150 cevabı kabul edildi. nice_answer: name: other: Güzel Cevap desc: other: 10 veya daha fazla cevap puanı. good_answer: name: other: İyi Cevap desc: other: 25 veya daha fazla cevap puanı. great_answer: name: other: Harika Cevap desc: other: 50 veya daha fazla cevap puanı. nice_question: name: other: Güzel Soru desc: other: 10 veya daha fazla soru puanı. good_question: name: other: İyi Soru desc: other: 25 veya daha fazla soru puanı. great_question: name: other: Harika Soru desc: other: 50 veya daha fazla soru puanı. popular_question: name: other: Popüler Soru desc: other: 500 görüntülenme alan soru. notable_question: name: other: Dikkat Çeken Soru desc: other: 1.000 görüntülenme alan soru. famous_question: name: other: Ünlü Soru desc: other: 5.000 görüntülenme alan soru. popular_link: name: other: Popüler Bağlantı desc: other: 50 tıklama alan harici bir bağlantı paylaştı. hot_link: name: other: Sıcak Bağlantı desc: other: 300 tıklama alan harici bir bağlantı paylaştı. famous_link: name: other: Ünlü Bağlantı desc: other: 100 tıklama alan harici bir bağlantı paylaştı. default_badge_groups: getting_started: name: other: Başlangıç community: name: other: Topluluk posting: name: other: Gönderi Yazmak # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Nasıl Biçimlendirilir desc: >-
  • bir gönderiden bahsetmek için: #post_id

  • bağlantı oluşturmak için

    <https://url.com>

    [Başlık](https://url.com)
  • paragraflar arasında boşluk bırakın

  • _italik_ veya **kalın**

  • kodu 4 boşlukla girintileyin

  • satırın başına > koyarak alıntı yapın

  • ters tırnak işaretleriyle kaçış yapın `_böyle_`

  • ters tırnak işaretleriyle ` kod blokları oluşturun

    ```
    kod buraya
    ```
pagination: prev: Önceki next: Sonraki page_title: question: Soru questions: Sorular tag: Etiket tags: Etiketler tag_wiki: etiket wikisi create_tag: Etiket Oluştur edit_tag: Etiketi Düzenle ask_a_question: Soru Oluştur edit_question: Soruyu Düzenle edit_answer: Cevabı Düzenle search: Ara posts_containing: İçeren gönderiler settings: Ayarlar notifications: Bildirimler login: Giriş Yap sign_up: Kayıt Ol account_recovery: Hesap Kurtarma account_activation: Hesap Aktivasyonu confirm_email: E-posta Onayı account_suspended: Hesap Askıya Alındı admin: Yönetici change_email: E-posta Değiştir install: Answer Kurulumu upgrade: Answer Yükseltme maintenance: Website Bakımı users: Kullanıcılar oauth_callback: İşleniyor http_404: HTTP Hatası 404 http_50X: HTTP Hatası 500 http_403: HTTP Hatası 403 logout: Çıkış Yap posts: Gönderiler ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Bildirimler inbox: Gelen Kutusu achievement: Başarılar new_alerts: Yeni uyarılar all_read: Tümünü okundu olarak işaretle show_more: Daha fazla göster someone: Birisi inbox_type: all: Tümü posts: Gönderiler invites: Davetler votes: Oylar answer: Cevap question: Soru badge_award: Rozet suspended: title: Hesabınız Askıya Alındı until_time: "Hesabınız {{ time }} tarihine kadar askıya alındı." forever: Bu kullanıcı süresiz olarak askıya alındı. end: Topluluk kurallarını karşılamıyorsunuz. contact_us: Bize ulaşın editor: blockquote: text: Alıntı bold: text: Kalın chart: text: Grafik flow_chart: Akış şeması sequence_diagram: Sıralama diyagramı class_diagram: Sınıf diyagramı state_diagram: Durum diyagramı entity_relationship_diagram: Varlık ilişki diyagramı user_defined_diagram: Kullanıcı tanımlı diyagram gantt_chart: Gantt şeması pie_chart: Pasta grafiği code: text: Kod Örneği add_code: Kod örneği ekle form: fields: code: label: Kod msg: empty: Kod boş olamaz. language: label: Dil placeholder: Otomatik algılama btn_cancel: İptal btn_confirm: Ekle formula: text: Formül options: inline: Satır içi formül block: Blok formül heading: text: Başlık options: h1: Başlık 1 h2: Başlık 2 h3: Başlık 3 h4: Başlık 4 h5: Başlık 5 h6: Başlık 6 help: text: Yardım hr: text: Yatay çizgi image: text: Resim add_image: Resim ekle tab_image: Resim Yükle form_image: fields: file: label: Resim dosyası btn: Resim seç msg: empty: Dosya boş olamaz. only_image: Sadece resim dosyalarına izin verilir. max_size: Dosya boyutu {{size}} MB'ı geçemez. desc: label: Açıklama tab_url: Resim URL'si form_url: fields: url: label: Resim URL'si msg: empty: Resim URL'si boş olamaz. name: label: Açıklama btn_cancel: İptal btn_confirm: Ekle uploading: Yükleniyor indent: text: Girinti outdent: text: Girintiyi azalt italic: text: İtalik link: text: Bağlantı add_link: Bağlantı ekle form: fields: url: label: URL msg: empty: URL boş olamaz. name: label: Açıklama btn_cancel: İptal btn_confirm: Ekle ordered_list: text: Numaralı liste unordered_list: text: Madde işaretli liste table: text: Tablo heading: Başlık cell: Hücre file: text: Dosya ekle not_supported: "Bu dosya türü desteklenmiyor. {{file_type}} ile tekrar deneyin." max_size: "Eklenen dosyaların boyutu {{size}} MB'ı geçemez." close_modal: title: Bu gönderiyi kapatıyorum çünkü... btn_cancel: İptal btn_submit: Gönder remark: empty: Boş olamaz. msg: empty: Lütfen bir neden seçin. report_modal: flag_title: Bu gönderiyi şu nedenle bildiriyorum... close_title: Bu gönderiyi şu nedenle kapatıyorum... review_question_title: Soruyu incele review_answer_title: Cevabı incele review_comment_title: Yorumu incele btn_cancel: İptal btn_submit: Gönder remark: empty: Boş olamaz. msg: empty: Lütfen bir neden seçin. not_a_url: URL formatı yanlış. url_not_match: URL kaynağı mevcut web sitesiyle eşleşmiyor. tag_modal: title: Yeni etiket oluştur form: fields: display_name: label: Görünen ad msg: empty: Görünen ad boş olamaz. range: Görünen ad en fazla 35 karakter olabilir. slug_name: label: URL kısaltması desc: URL kısaltması en fazla 35 karakter olabilir. msg: empty: URL kısaltması boş olamaz. range: URL kısaltması en fazla 35 karakter olabilir. character: URL kısaltması izin verilmeyen karakter içeriyor. desc: label: Açıklama revision: label: Revizyon edit_summary: label: Düzenleme özeti placeholder: >- Değişikliklerinizi kısaca açıklayın (yazım hatası düzeltildi, dilbilgisi düzeltildi, biçimlendirme geliştirildi) btn_cancel: İptal btn_submit: Gönder btn_post: Yeni etiket gönder tag_info: created_at: Oluşturuldu edited_at: Düzenlendi history: Geçmiş synonyms: title: Eş Anlamlılar text: Aşağıdaki etiketler şuna yeniden eşlenecek empty: Eş anlamlı bulunamadı. btn_add: Eş anlamlı ekle btn_edit: Düzenle btn_save: Kaydet synonyms_text: Aşağıdaki etiketler şuna yeniden eşlenecek delete: title: Bu etiketi sil tip_with_posts: >-

Gönderileri olan etiketin silinmesine izin vermiyoruz.

Lütfen önce bu etiketi gönderilerden kaldırın.

tip_with_synonyms: >-

Eş anlamlıları olan etiketin silinmesine izin vermiyoruz.

Lütfen önce bu etiketin eş anlamlılarını kaldırın.

tip: Silmek istediğinizden emin misiniz? close: Kapat merge: title: Etiket birleştir source_tag_title: Kaynak etiket source_tag_description: Kaynak etiket ve ilişkili verileri hedef etikete yeniden eşlenecek. target_tag_title: Hedef etiket target_tag_description: Birleştirmeden sonra bu iki etiket arasında bir eş anlamlı ilişkisi oluşturulacak. no_results: Eşleşen etiket bulunamadı btn_submit: Gönder btn_close: Kapat edit_tag: title: Etiketi Düzenle default_reason: Etiketi düzenle default_first_reason: Etiket ekle btn_save_edits: Düzenlemeleri kaydet btn_cancel: İptal dates: long_date: D MMM long_date_with_year: "D MMM, YYYY" long_date_with_time: "D MMM, YYYY [saat] HH:mm" now: şimdi x_seconds_ago: "{{count}} saniye önce" x_minutes_ago: "{{count}} dakika önce" x_hours_ago: "{{count}} saat önce" hour: saat day: gün hours: saatler days: günler month: ay months: aylar year: yıl reaction: heart: kalp smile: gülümseme frown: üzgün btn_label: tepki ekle veya kaldır undo_emoji: '{{emoji}} tepkisini geri al' react_emoji: '{{emoji}} ile tepki ver' unreact_emoji: '{{emoji}} tepkisini kaldır' comment: btn_add_comment: Yorum ekle reply_to: Yanıtla btn_reply: Yanıtla btn_edit: Düzenle btn_delete: Sil btn_flag: Bildir btn_save_edits: Düzenlemeleri kaydet btn_cancel: İptal show_more: "{{count}} daha fazla yorum" tip_question: >- Daha fazla bilgi istemek veya iyileştirmeler önermek için yorumları kullanın. Yorumlarda soruları cevaplamaktan kaçının. tip_answer: >- Diğer kullanıcılara yanıt vermek veya onları değişikliklerden haberdar etmek için yorumları kullanın. Yeni bilgi ekliyorsanız, yorum yapmak yerine gönderinizi düzenleyin. tip_vote: Gönderiye faydalı bir şey ekliyor edit_answer: title: Cevabı Düzenle default_reason: Cevabı düzenle default_first_reason: Cevap ekle form: fields: revision: label: Revizyon answer: label: Cevap feedback: characters: içerik en az 6 karakter uzunluğunda olmalıdır. edit_summary: label: Düzenleme özeti placeholder: >- Yaptığınız değişiklikleri kısaca açıklayın (düzeltilmiş yazım geliştirilmiş biçimlendirme) btn_save_edits: Düzenlemeleri kaydet btn_cancel: İptal tags: title: Etiketler sort_buttons: popular: Popüler name: İsim newest: En Yeni button_follow: Takip Et button_following: Takip Ediliyor tag_label: sorular search_placeholder: Etiket adına göre filtrele no_desc: Bu etiketin açıklaması yok. more: Daha Fazla wiki: Wiki ask: title: Soru Oluştur edit_title: Soruyu Düzenle default_reason: Soruyu düzenle default_first_reason: Soru oluştur similar_questions: Benzer sorular form: fields: revision: label: Revizyon title: label: Başlık placeholder: Konu nedir? Ayrıntılı yaz. msg: empty: Başlık boş olamaz. range: Başlık en fazla 150 karakter olabilir body: label: İçerik msg: empty: İçerik boş olamaz. hint: optional_body: Sorunun neyle ilgili olduğunu açıklayın. minimum_characters: "Sorunun neyle ilgili olduğunu açıklayın, en az {{min_content_length}} karakter gereklidir." tags: label: Etiketler msg: empty: Etiketler boş olamaz. answer: label: Cevap msg: empty: Cevap boş olamaz. edit_summary: label: Düzenleme özeti placeholder: >- Değişikliklerinizi kısaca açıklayın (yazım hatası düzeltildi, dilbilgisi düzeltildi, biçimlendirme geliştirildi) btn_post_question: Sorunuzu gönderin btn_save_edits: Düzenlemeleri kaydet answer_question: Kendi sorunuzu cevaplayın post_question&answer: Sorunuzu ve cevabınızı gönderin tag_selector: add_btn: Etiket ekle create_btn: Yeni etiket oluştur search_tag: Etiket ara hint: Sorunuzun ne hakkında olduğunu tanımlayın, en az bir etiket gereklidir. hint_zero_tags: İçeriğinizin neyle ilgili olduğunu açıklayın. hint_more_than_one_tag: "İçeriğinizin neyle ilgili olduğunu açıklayın, en az {{min_tags_number}} etiket gereklidir." no_result: Eşleşen etiket bulunamadı tag_required_text: Gerekli etiket (en az bir tane) header: nav: question: Sorular tag: Etiketler user: Kullanıcılar badges: Rozetler profile: Profil setting: Ayarlar logout: Çıkış yap admin: Yönetici review: İnceleme bookmark: Yer İşaretleri moderation: Moderasyon search: placeholder: Ara footer: build_on: Powered by <1> Apache Answer upload_img: name: Değiştir loading: yükleniyor... pic_auth_code: title: Captcha placeholder: Yukarıdaki metni yazın msg: empty: Captcha boş olamaz. inactive: first: >- Neredeyse tamamlandı! {{mail}} adresine bir aktivasyon e-postası gönderdik. Hesabınızı etkinleştirmek için lütfen e-postadaki talimatları izleyin. info: "E-posta gelmezse, spam klasörünüzü kontrol edin." another: >- {{mail}} adresine başka bir aktivasyon e-postası gönderdik. Gelmesi birkaç dakika sürebilir; spam klasörünüzü kontrol etmeyi unutmayın. btn_name: Aktivasyon e-postasını yeniden gönder change_btn_name: E-posta değiştir msg: empty: Boş olamaz. resend_email: url_label: Aktivasyon e-postasını yeniden göndermek istediğinizden emin misiniz? url_text: Ayrıca yukarıdaki aktivasyon bağlantısını kullanıcıya verebilirsiniz. login: login_to_continue: Devam etmek için giriş yapın info_sign: Hesabınız yok mu? <1>Kaydolun info_login: Zaten hesabınız var mı? <1>Giriş yapın agreements: Kaydolarak <1>gizlilik politikasını ve <3>hizmet şartlarını kabul etmiş olursunuz. forgot_pass: Parolanızı mı unuttunuz? name: label: İsim msg: empty: İsim boş olamaz. range: İsim 2 ile 30 karakter arasında olmalıdır. character: 'Yalnızca "a-z", "0-9", " - . _" karakterleri kullanılabilir' email: label: E-posta msg: empty: E-posta boş olamaz. password: label: Parola msg: empty: Parola boş olamaz. different: Her iki tarafta girilen parolalar tutarsız account_forgot: page_title: Parolanızı mı Unuttunuz btn_name: Bana kurtarma e-postası gönder send_success: >- Eğer bir hesap {{mail}} ile eşleşirse, kısa süre içinde parolanızı nasıl sıfırlayacağınıza dair talimatlar içeren bir e-posta almalısınız. email: label: E-posta msg: empty: E-posta boş olamaz. change_email: btn_cancel: İptal btn_update: E-posta adresini güncelle send_success: >- Eğer bir hesap {{mail}} ile eşleşirse, kısa süre içinde parolanızı nasıl sıfırlayacağınıza dair talimatlar içeren bir e-posta almalısınız. email: label: Yeni e-posta msg: empty: E-posta boş olamaz. oauth: connect: '{{auth_name}} ile bağlan' remove: '{{auth_name}} kaldır' oauth_bind_email: subtitle: Hesabınıza bir kurtarma e-postası ekleyin. btn_update: E-posta adresini güncelle email: label: E-posta msg: empty: E-posta boş olamaz. modal_title: E-posta zaten mevcut. modal_content: Bu e-posta adresi zaten kayıtlı. Mevcut hesaba bağlanmak istediğinizden emin misiniz? modal_cancel: E-posta değiştir modal_confirm: Mevcut hesaba bağlan password_reset: page_title: Parola Sıfırlama btn_name: Parolamı sıfırla reset_success: >- Parolanızı başarıyla değiştirdiniz; giriş sayfasına yönlendirileceksiniz. link_invalid: >- Üzgünüz, bu parola sıfırlama bağlantısı artık geçerli değil. Belki de parolanız zaten sıfırlanmış? to_login: Giriş sayfasına devam et password: label: Parola msg: empty: Parola boş olamaz. length: Uzunluk 8 ile 32 arasında olmalıdır different: Her iki tarafta girilen parolalar tutarsız password_confirm: label: Yeni parolayı onayla settings: page_title: Ayarlar goto_modify: Değiştirmeye git nav: profile: Profil notification: Bildirimler account: Hesap interface: Arayüz profile: heading: Profil btn_name: Kaydet display_name: label: Görünen ad msg: Görünen ad boş olamaz. msg_range: Görünen ad 2-30 karakter uzunluğunda olmalıdır. username: label: Kullanıcı adı caption: İnsanlar size "@kullaniciadi" şeklinde bahsedebilir. msg: Kullanıcı adı boş olamaz. msg_range: Kullanıcı adı 2-30 karakter uzunluğunda olmalıdır. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profil resmi gravatar: Gravatar gravatar_text: Resmi şurada değiştirebilirsiniz custom: Özel custom_text: Kendi resminizi yükleyebilirsiniz. default: Sistem msg: Lütfen bir avatar yükleyin bio: label: Hakkımda website: label: Website placeholder: "https://example.com" msg: Website formatı yanlış location: label: Konum placeholder: "Şehir, Ülke" notification: heading: E-posta Bildirimleri turn_on: Aç inbox: label: Gelen kutusu bildirimleri description: Sorularınıza cevaplar, yorumlar, davetler ve daha fazlası. all_new_question: label: Tüm yeni sorular description: Tüm yeni sorulardan haberdar olun. Haftada en fazla 50 soru. all_new_question_for_following_tags: label: Takip edilen etiketler için tüm yeni sorular description: Takip ettiğiniz etiketlerdeki yeni sorulardan haberdar olun. account: heading: Hesap change_email_btn: E-posta değiştir change_pass_btn: Parola değiştir change_email_info: >- Bu adrese bir e-posta gönderdik. Lütfen onay talimatlarını takip edin. email: label: E-posta new_email: label: Yeni e-posta msg: Yeni e-posta boş olamaz. pass: label: Mevcut parola msg: Parola boş olamaz. password_title: Parola current_pass: label: Mevcut parola msg: empty: Mevcut parola boş olamaz. length: Uzunluk 8 ile 32 arasında olmalıdır. different: Girilen iki parola eşleşmiyor. new_pass: label: Yeni parola pass_confirm: label: Yeni parolayı onayla interface: heading: Arayüz lang: label: Arayüz dili text: Kullanıcı arayüzü dili. Sayfa yenilendiğinde değişecektir. my_logins: title: Girişlerim label: Bu hesapları kullanarak bu sitede giriş yapın veya kaydolun. modal_title: Girişi kaldır modal_content: Bu girişi hesabınızdan kaldırmak istediğinizden emin misiniz? modal_confirm_btn: Kaldır remove_success: Başarıyla kaldırıldı toast: update: güncelleme başarılı update_password: Parola başarıyla değiştirildi. flag_success: Bildirdiğiniz için teşekkürler. forbidden_operate_self: Kendinizle ilgili işlem yapmak yasaktır review: Revizyonunuz incelendikten sonra görünecek. sent_success: Başarıyla gönderildi related_question: title: İle ilgili answers: cevap linked_question: title: Bağlantılı description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: İnsanları Davet Et desc: Cevap verebileceğini düşündüğünüz kişileri davet edin. invite: Cevaplamaya davet et add: Kişi ekle search: Kişi ara question_detail: action: Eylem created: Created Asked: Soruldu asked: sordu update: Değiştirildi Edited: Edited edit: düzenledi commented: yorum yaptı Views: Görüntülendi Follow: Takip Et Following: Takip Ediliyor follow_tip: Bildirim almak için bu soruyu takip edin answered: cevapladı closed_in: Şurada kapatıldı show_exist: Var olan soruyu göster. useful: Faydalı question_useful: Faydalı ve açık question_un_useful: Belirsiz veya faydalı değil question_bookmark: Bu soruyu yer işaretlerine ekle answer_useful: Faydalı answer_un_useful: Faydalı değil answers: title: Cevaplar score: Puan newest: En Yeni oldest: En Eski btn_accept: Kabul Et btn_accepted: Kabul Edildi write_answer: title: Cevabınız edit_answer: Mevcut cevabımı düzenle btn_name: Cevabınızı gönderin add_another_answer: Başka bir cevap ekle confirm_title: Cevaplamaya devam et continue: Devam et confirm_info: >-

Başka bir cevap eklemek istediğinizden emin misiniz?

Bunun yerine mevcut cevabınızı iyileştirmek ve geliştirmek için düzenleme bağlantısını kullanabilirsiniz.

empty: Cevap boş olamaz. characters: içerik en az 6 karakter uzunluğunda olmalıdır. tips: header_1: Cevabınız için teşekkürler li1_1: Lütfen soruyu cevapladığınızdan emin olun. Detaylar verin ve araştırmanızı paylaşın. li1_2: Yaptığınız ifadeleri referanslar veya kişisel deneyimlerle destekleyin. header_2: Ancak şunlardan kaçının ... li2_1: Yardım istemek, açıklama istemek veya diğer cevaplara yanıt vermek. reopen: confirm_btn: Yeniden Aç title: Bu gönderiyi yeniden aç content: Yeniden açmak istediğinizden emin misiniz? list: confirm_btn: Listele title: Bu gönderiyi listele content: Listelemek istediğinizden emin misiniz? unlist: confirm_btn: Listeden Kaldır title: Bu gönderiyi listeden kaldır content: Listeden kaldırmak istediğinizden emin misiniz? pin: title: Bu gönderiyi sabitle content: Küresel olarak sabitlemek istediğinizden emin misiniz? Bu gönderi tüm gönderi listelerinin en üstünde görünecektir. confirm_btn: Sabitle delete: title: Bu gönderiyi sil question: >- Cevapları olan soruları silmenizi önermiyoruz çünkü bunu yapmak gelecekteki okuyucuları bu bilgiden mahrum bırakır.

Cevaplanmış soruları tekrar tekrar silmek, hesabınızın soru sorma yeteneğinin engellenmesine neden olabilir. Silmek istediğinizden emin misiniz? answer_accepted: >-

Kabul edilmiş cevabı silmenizi önermiyoruz çünkü bunu yapmak gelecekteki okuyucuları bu bilgiden mahrum bırakır.

Kabul edilmiş cevapları tekrar tekrar silmek, hesabınızın cevap verme yeteneğinin engellenmesine neden olabilir. Silmek istediğinizden emin misiniz? other: Silmek istediğinizden emin misiniz? tip_answer_deleted: Bu cevap silinmiştir undelete_title: Bu gönderinin silmesini geri al undelete_desc: Silmeyi geri almak istediğinizden emin misiniz? btns: confirm: Onayla cancel: İptal edit: Düzenle save: Kaydet delete: Sil undelete: Silmeyi Geri Al list: Listele unlist: Listeden Kaldır unlisted: Listede Değil login: Giriş Yap signup: Kayıt Ol logout: Çıkış Yap verify: Doğrula create: Oluştur approve: Onayla reject: Reddet skip: Atla discard_draft: Taslağı at pinned: Sabitlendi all: Tümü question: Soru answer: Cevap comment: Yorum refresh: Yenile resend: Yeniden Gönder deactivate: Devre Dışı Bırak active: Aktif suspend: Askıya Al unsuspend: Askıyı Kaldır close: Kapat reopen: Yeniden Aç ok: Tamam light: Açık dark: Koyu system_setting: Sistem ayarı default: Varsayılan reset: Sıfırla tag: Etiket post_lowercase: gönderi filter: Filtrele ignore: Yoksay submit: Gönder normal: Normal closed: Kapalı deleted: Silindi deleted_permanently: Kalıcı olarak silindi pending: Beklemede more: Daha Fazla view: Görüntüle card: Kart compact: Kompakt display_below: Aşağıda göster always_display: Her zaman göster or: veya back_sites: Sitelere geri dön search: title: Arama Sonuçları keywords: Anahtar Kelimeler options: Seçenekler follow: Takip Et following: Takip Ediliyor counts: "{{count}} Sonuç" counts_loading: "... Results" more: Daha Fazla sort_btns: relevance: İlgililik newest: En Yeni active: Aktif score: Puan more: Daha Fazla tips: title: Gelişmiş Arama İpuçları tag: "<1>[etiket] bir etiketle ara" user: "<1>user:kullanıcıadı yazara göre ara" answer: "<1>answers:0 cevaplanmamış sorular" score: "<1>score:3 3+ puana sahip gönderiler" question: "<1>is:question soruları ara" is_answer: "<1>is:answer cevapları ara" empty: Hiçbir şey bulamadık.
Farklı veya daha az spesifik anahtar kelimeler deneyin. share: name: Paylaş copy: Bağlantıyı kopyala via: Gönderiyi şurada paylaş... copied: Kopyalandı facebook: Facebook'ta Paylaş twitter: X'te Paylaş cannot_vote_for_self: Kendi gönderinize oy veremezsiniz. modal_confirm: title: Hata... delete_permanently: title: Kalıcı olarak sil content: Kalıcı olarak silmek istediğinizden emin misiniz? account_result: success: Yeni hesabınız onaylandı; ana sayfaya yönlendirileceksiniz. link: Ana sayfaya devam et oops: Hay aksi! invalid: Kullandığınız bağlantı artık çalışmıyor. confirm_new_email: E-postanız güncellendi. confirm_new_email_invalid: >- Üzgünüz, bu onay bağlantısı artık geçerli değil. Belki e-postanız zaten değiştirildi? unsubscribe: page_title: Abonelikten Çık success_title: Abonelikten Çıkma Başarılı success_desc: Bu abone listesinden başarıyla çıkarıldınız ve bizden başka e-posta almayacaksınız. link: Ayarları değiştir question: following_tags: Takip Edilen Etiketler edit: Düzenle save: Kaydet follow_tag_tip: Soru listenizi oluşturmak için etiketleri takip edin. hot_questions: Popüler Sorular all_questions: Tüm Sorular x_questions: "{{ count }} Soru" x_answers: "{{ count }} cevap" x_posts: "{{ count }} Posts" questions: Sorular answers: Cevaplar newest: En Yeni active: Aktif hot: Popüler frequent: Sık Sorulan recommend: Önerilenler score: Puan unanswered: Cevaplanmamış modified: değiştirildi answered: cevaplandı asked: soruldu closed: kapandı follow_a_tag: Bir etiketi takip et more: Daha Fazla personal: overview: Genel Bakış answers: Cevaplar answer: cevap questions: Sorular question: soru bookmarks: Yer İşaretleri reputation: İtibar comments: Yorumlar votes: Oylar badges: Rozetler newest: En Yeni score: Puan edit_profile: Profili düzenle visited_x_days: "{{ count }} gün ziyaret edildi" viewed: Görüntülendi joined: Katıldı comma: "," last_login: Görüldü about_me: Hakkımda about_me_empty: "// Merhaba, Dünya !" top_answers: En İyi Cevaplar top_questions: En İyi Sorular stats: İstatistikler list_empty: Gönderi bulunamadı.
Belki farklı bir sekme seçmek istersiniz? content_empty: Gönderi bulunamadı. accepted: Kabul Edildi answered: cevaplandı asked: sordu downvoted: negatif oylandı mod_short: MOD mod_long: Moderatörler x_reputation: itibar x_votes: alınan oy x_answers: cevap x_questions: soru recent_badges: Son Rozetler install: title: Kurulum next: İleri done: Tamamlandı config_yaml_error: config.yaml dosyası oluşturulamıyor. lang: label: Lütfen bir dil seçin db_type: label: Veritabanı motoru db_username: label: Kullanıcı adı placeholder: root msg: Kullanıcı adı boş olamaz. db_password: label: Parola placeholder: root msg: Parola boş olamaz. db_host: label: Veritabanı sunucusu placeholder: "db:3306" msg: Veritabanı sunucusu boş olamaz. db_name: label: Veritabanı adı placeholder: answer msg: Veritabanı adı boş olamaz. db_file: label: Veritabanı dosyası placeholder: /data/answer.db msg: Veritabanı dosyası boş olamaz. ssl_enabled: label: SSL'i Etkinleştir ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Modu ssl_root_cert: placeholder: sslrootcert dosya yolu msg: sslrootcert dosya yolu boş olamaz ssl_cert: placeholder: sslcert dosya yolu msg: sslcert dosya yolu boş olamaz ssl_key: placeholder: sslkey dosya yolu msg: sslkey dosya yolu boş olamaz config_yaml: title: config.yaml Oluştur label: config.yaml dosyası oluşturuldu. desc: >- <1>config.yaml dosyasını <1>/var/wwww/xxx/ dizininde manuel olarak oluşturabilir ve aşağıdaki metni içine yapıştırabilirsiniz. info: Bunu yaptıktan sonra "İleri" düğmesine tıklayın. site_information: Site Bilgisi admin_account: Yönetici Hesabı site_name: label: Site adı msg: Saat dilimi boş olamaz. msg_max_length: Site adı en fazla 30 karakter uzunluğunda olmalıdır. site_url: label: Site URL'si text: Sitenizin adresi. msg: empty: Site URL'si boş olamaz. incorrect: Site URL'si formatı yanlış. max_length: Site URL'si en fazla 512 karakter uzunluğunda olmalıdır. contact_email: label: İletişim e-postası text: Bu siteden sorumlu kilit kişinin e-posta adresi. msg: empty: İletişim e-postası boş olamaz. incorrect: İletişim e-postası formatı yanlış. login_required: label: Özel switch: Giriş gerekli text: Bu topluluğa sadece giriş yapmış kullanıcılar erişebilir. admin_name: label: İsim msg: İsim boş olamaz. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: İsim 2 ile 30 karakter arasında olmalıdır. admin_password: label: Parola text: >- Giriş yapmak için bu parolaya ihtiyacınız olacak. Lütfen güvenli bir yerde saklayın. msg: Parola boş olamaz. msg_min_length: Parola en az 8 karakter uzunluğunda olmalıdır. msg_max_length: Parola en fazla 32 karakter uzunluğunda olmalıdır. admin_confirm_password: label: "Parolayı Onayla" text: "Lütfen onaylamak için parolanızı tekrar girin." msg: "Onay parolası eşleşmiyor." admin_email: label: E-posta text: Giriş yapmak için bu e-postaya ihtiyacınız olacak. msg: empty: E-posta boş olamaz. incorrect: E-posta formatı yanlış. ready_title: Siteniz hazır ready_desc: >- Daha fazla ayarı değiştirmek isterseniz, <1>yönetici bölümünü ziyaret edin; site menüsünde bulabilirsiniz. good_luck: "İyi eğlenceler ve iyi şanslar!" warn_title: Uyarı warn_desc: >- <1>config.yaml dosyası zaten var. Bu dosyadaki herhangi bir yapılandırma öğesini sıfırlamanız gerekiyorsa, lütfen önce dosyayı silin. install_now: <1>Şimdi kurulum yapmayı deneyebilirsiniz. installed: Zaten kurulu installed_desc: >- Zaten kurulum yapmış görünüyorsunuz. Yeniden kurmak için lütfen önce eski veritabanı tablolarınızı temizleyin. db_failed: Veritabanı bağlantısı başarısız db_failed_desc: >- Bu, <1>config.yaml dosyanızdaki veritabanı bilgilerinin yanlış olduğu veya veritabanı sunucusuyla bağlantı kurulamadığı anlamına gelir. Bu, sunucunuzun veritabanı sunucusunun çalışmadığı anlamına gelebilir. counts: views: görüntülenme votes: oy answers: cevap accepted: Kabul Edildi page_error: http_error: HTTP Hatası {{ code }} desc_403: Bu sayfaya erişim izniniz yok. desc_404: Maalesef, bu sayfa mevcut değil. desc_50X: Sunucu bir hatayla karşılaştı ve isteğinizi tamamlayamadı. back_home: Ana sayfaya dön page_maintenance: desc: "Bakım altındayız, yakında geri döneceğiz." nav_menus: dashboard: Gösterge Paneli contents: İçerikler questions: Sorular answers: Cevaplar users: Kullanıcılar badges: Rozetler flags: Bildirimler settings: Ayarlar general: Genel interface: Arayüz smtp: SMTP branding: Marka legal: Yasal write: Yaz terms: Terms tos: Kullanım Şartları privacy: Gizlilik seo: SEO customize: Özelleştir themes: Temalar login: Giriş privileges: Ayrıcalıklar plugins: Eklentiler installed_plugins: Kurulu Eklentiler apperance: Görünüm community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: '{{site_name}} sitesine hoş geldiniz' user_center: login: Giriş qrcode_login_tip: Lütfen QR kodu taramak ve giriş yapmak için {{ agentName }} kullanın. login_failed_email_tip: Giriş başarısız oldu, lütfen tekrar denemeden önce bu uygulamanın e-posta bilgilerinize erişmesine izin verin. badges: modal: title: Tebrikler content: Yeni bir rozet kazandınız. close: Kapat confirm: Rozetleri görüntüle title: Rozetler awarded: Kazanıldı earned_×: '{{ number }} kez kazanıldı' ×_awarded: "{{ number }} kez verildi" can_earn_multiple: Bunu birden çok kez kazanabilirsiniz. earned: Kazanıldı admin: admin_header: title: Yönetici dashboard: title: Gösterge Paneli welcome: Yönetici'ye Hoş Geldiniz! site_statistics: Site istatistikleri questions: "Sorular:" resolved: "Çözülmüş:" unanswered: "Cevaplanmamış:" answers: "Cevaplar:" comments: "Yorumlar:" votes: "Oylar:" users: "Kullanıcılar:" flags: "Bildirimler:" reviews: "İncelemeler:" site_health: Site sağlığı version: "Sürüm:" https: "HTTPS:" upload_folder: "Yükleme klasörü:" run_mode: "Çalışma modu:" private: Özel public: Herkese Açık smtp: "SMTP:" timezone: "Saat dilimi:" system_info: Sistem bilgisi go_version: "Go sürümü:" database: "Veritabanı:" database_size: "Veritabanı boyutu:" storage_used: "Kullanılan depolama:" uptime: "Çalışma süresi:" links: Bağlantılar plugins: Eklentiler github: GitHub blog: Blog contact: İletişim forum: Forum documents: Belgeler feedback: Geribildirim support: Destek review: İnceleme config: Yapılandırma update_to: Güncelle latest: En son check_failed: Kontrol başarısız "yes": "Evet" "no": "Hayır" not_allowed: İzin verilmiyor allowed: İzin veriliyor enabled: Etkin disabled: Devre dışı writable: Yazılabilir not_writable: Yazılamaz flags: title: Bildirimler pending: Bekleyen completed: Tamamlanan flagged: Bildirilen flagged_type: Bildirilen {{ type }} created: Oluşturuldu action: Eylem review: İnceleme user_role_modal: title: Kullanıcı rolünü şuna değiştir... btn_cancel: İptal btn_submit: Gönder new_password_modal: title: Yeni parola belirle form: fields: password: label: Parola text: Kullanıcının oturumu kapatılacak ve tekrar giriş yapması gerekecek. msg: Parola 8-32 karakter uzunluğunda olmalıdır. btn_cancel: İptal btn_submit: Gönder edit_profile_modal: title: Profili düzenle form: fields: display_name: label: Görünen ad msg_range: Görünen ad 2-30 karakter uzunluğunda olmalıdır. username: label: Kullanıcı adı msg_range: Kullanıcı adı 2-30 karakter uzunluğunda olmalıdır. email: label: E-posta msg_invalid: Geçersiz E-posta Adresi. edit_success: Başarıyla düzenlendi btn_cancel: İptal btn_submit: Gönder user_modal: title: Yeni kullanıcı ekle form: fields: users: label: Toplu kullanıcı ekle placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: İsim, e-posta, parola bilgilerini virgülle ayırın. Her satırda bir kullanıcı. msg: "Lütfen kullanıcının e-postasını girin, her satırda bir tane." display_name: label: Görünen ad msg: Görünen ad 2-30 karakter uzunluğunda olmalıdır. email: label: E-posta msg: E-posta geçerli değil. password: label: Parola msg: Parola 8-32 karakter uzunluğunda olmalıdır. btn_cancel: İptal btn_submit: Gönder users: title: Kullanıcılar name: İsim email: E-posta reputation: İtibar created_at: Oluşturulma zamanı delete_at: Silinme zamanı suspend_at: Askıya Alınma Zamanı suspend_until: Suspend until status: Durum role: Rol action: Eylem change: Değiştir all: Tümü staff: Ekip more: Daha Fazla inactive: Etkin Değil suspended: Askıya Alınmış deleted: Silinmiş normal: Normal Moderator: Moderatör Admin: Yönetici User: Kullanıcı filter: placeholder: "İsme göre filtreleme, user:id" set_new_password: Yeni parola belirle edit_profile: Profili düzenle change_status: Durumu değiştir change_role: Rolü değiştir show_logs: Kayıtları göster add_user: Kullanıcı ekle deactivate_user: title: Kullanıcıyı devre dışı bırak content: Etkin olmayan bir kullanıcının e-postasını yeniden doğrulaması gerekir. delete_user: title: Bu kullanıcıyı sil content: Bu kullanıcıyı silmek istediğinizden emin misiniz? Bu kalıcıdır! remove: İçeriklerini kaldır label: Tüm soruları, cevapları, yorumları vb. kaldır. text: Sadece kullanıcının hesabını silmek istiyorsanız bunu işaretlemeyin. suspend_user: title: Bu kullanıcıyı askıya al content: Askıya alınmış bir kullanıcı giriş yapamaz. label: How long will the user be suspended for? forever: Forever questions: page_title: Sorular unlisted: Listelenmemiş post: Gönderi votes: Oylar answers: Cevaplar created: Oluşturuldu status: Durum action: Eylem change: Değiştir pending: Beklemede filter: placeholder: "Başlığa göre filtreleme, question:id" answers: page_title: Cevaplar post: Gönderi votes: Oylar created: Oluşturuldu status: Durum action: Eylem change: Değiştir filter: placeholder: "Başlığa göre filtreleme, answer:id" general: page_title: Genel name: label: Site adı msg: Site adı boş olamaz. text: "Başlık etiketinde kullanılan bu sitenin adı." site_url: label: Site URL'si msg: Site url'si boş olamaz. validate: Lütfen geçerli bir URL girin. text: Sitenizin adresi. short_desc: label: Kısa site açıklaması msg: Kısa site açıklaması boş olamaz. text: "Ana sayfadaki başlık etiketinde kullanılan kısa açıklama." desc: label: Site açıklaması msg: Site açıklaması boş olamaz. text: "Bu siteyi bir cümleyle açıklayın, meta açıklama etiketinde kullanılır." contact_email: label: İletişim e-postası msg: İletişim e-postası boş olamaz. validate: İletişim e-postası geçerli değil. text: Bu siteden sorumlu kilit kişinin e-posta adresi. check_update: label: Yazılım güncellemeleri text: Güncellemeleri otomatik olarak kontrol et interface: page_title: Arayüz language: label: Arayüz dili msg: Arayüz dili boş olamaz. text: Kullanıcı arayüzü dili. Sayfa yenilendiğinde değişecektir. time_zone: label: Saat dilimi msg: Saat dilimi boş olamaz. text: Sizinle aynı saat dilimindeki bir şehri seçin. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: Gönderen e-posta msg: Gönderen e-posta boş olamaz. text: E-postaların gönderildiği e-posta adresi. from_name: label: Gönderen adı msg: Gönderen adı boş olamaz. text: E-postaların gönderildiği isim. smtp_host: label: SMTP sunucusu msg: SMTP sunucusu boş olamaz. text: Mail sunucunuz. encryption: label: Şifreleme msg: Şifreleme boş olamaz. text: Çoğu sunucu için SSL önerilen seçenektir. ssl: SSL tls: TLS none: Yok smtp_port: label: SMTP portu msg: SMTP portu 1 ~ 65535 arasında bir sayı olmalıdır. text: Mail sunucunuzun portu. smtp_username: label: SMTP kullanıcı adı msg: SMTP kullanıcı adı boş olamaz. smtp_password: label: SMTP parolası msg: SMTP parolası boş olamaz. test_email_recipient: label: Test e-posta alıcıları text: Test gönderimlerini alacak e-posta adresini girin. msg: Test e-posta alıcıları geçersiz smtp_authentication: label: Kimlik doğrulamayı etkinleştir title: SMTP kimlik doğrulaması msg: SMTP kimlik doğrulaması boş olamaz. "yes": "Evet" "no": "Hayır" branding: page_title: Marka logo: label: Logo msg: Logo boş olamaz. text: Sitenizin sol üst köşesindeki logo resmi. 56 yüksekliğinde ve 3:1'den büyük en-boy oranlı geniş dikdörtgen bir resim kullanın. Boş bırakılırsa, site başlık metni gösterilecektir. mobile_logo: label: Mobil logo text: Sitenizin mobil versiyonunda kullanılan logo. 56 yüksekliğinde geniş dikdörtgen bir resim kullanın. Boş bırakılırsa, "logo" ayarındaki resim kullanılacaktır. square_icon: label: Kare simge msg: Kare simge boş olamaz. text: Meta veri simgeleri için temel olarak kullanılan resim. İdeal olarak 512x512'den büyük olmalıdır. favicon: label: Favicon text: Siteniz için bir favicon. CDN üzerinde düzgün çalışması için png olmalıdır. 32x32 boyutuna yeniden boyutlandırılacaktır. Boş bırakılırsa, "kare simge" kullanılacaktır. legal: page_title: Yasal terms_of_service: label: Kullanım şartları text: "Buraya kullanım şartları içeriği ekleyebilirsiniz. Başka bir yerde barındırılan bir belgeniz varsa, tam URL'yi buraya girin." privacy_policy: label: Gizlilik politikası text: "Buraya gizlilik politikası içeriği ekleyebilirsiniz. Başka bir yerde barındırılan bir belgeniz varsa, tam URL'yi buraya girin." external_content_display: label: Harici içerik text: "İçerik, harici web sitelerinden gömülen resimler, videolar ve medyayı içerir." always_display: Her zaman harici içeriği göster ask_before_display: Harici içeriği göstermeden önce sor write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Cevap yazma label: Her kullanıcı aynı soru için sadece bir cevap yazabilir text: "Kullanıcıların aynı soruya birden fazla cevap yazmasına izin vermek için kapatın, bu cevapların odaktan uzaklaşmasına neden olabilir." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Önerilen etiketler text: "Önerilen etiketler varsayılan olarak açılır listede gösterilecektir." msg: contain_reserved: "önerilen etiketler ayrılmış etiketleri içeremez" required_tag: title: Gerekli etiketleri ayarla label: Önerilen etiketleri gerekli etiketler olarak ayarla text: "Her yeni soru en az bir önerilen etikete sahip olmalıdır." reserved_tags: label: Ayrılmış etiketler text: "Ayrılmış etiketler sadece moderatör tarafından kullanılabilir." image_size: label: Maksimum resim boyutu (MB) text: "Maksimum resim yükleme boyutu." attachment_size: label: Maksimum ek dosya boyutu (MB) text: "Maksimum ek dosya yükleme boyutu." image_megapixels: label: Maksimum resim megapikseli text: "Bir resim için izin verilen maksimum megapiksel sayısı." image_extensions: label: İzin verilen resim uzantıları text: "Resim gösterimi için izin verilen dosya uzantılarının listesi, virgülle ayırın." attachment_extensions: label: İzin verilen ek dosya uzantıları text: "Yükleme için izin verilen dosya uzantılarının listesi, virgülle ayırın. UYARI: Yüklemelere izin vermek güvenlik sorunlarına neden olabilir." seo: page_title: SEO permalink: label: Kalıcı bağlantı text: Özel URL yapıları, bağlantılarınızın kullanılabilirliğini ve ileriye dönük uyumluluğunu iyileştirebilir. robots: label: robots.txt text: Bu, ilgili tüm site ayarlarını kalıcı olarak geçersiz kılacaktır. themes: page_title: Temalar themes: label: Temalar text: Mevcut bir tema seçin. color_scheme: label: Renk şeması navbar_style: label: Gezinme çubuğu arka plan stili primary_color: label: Ana renk text: Temalarınızda kullanılan renkleri değiştirin layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS ve HTML custom_css: label: Özel CSS text: > Bu <link> olarak eklenecektir head: label: Head text: > Bu </head> öncesine eklenecektir header: label: Header text: > Bu <body> sonrasına eklenecektir footer: label: Footer text: Bu </body> öncesine eklenecektir sidebar: label: Kenar çubuğu text: Bu kenar çubuğuna eklenecektir. login: page_title: Giriş membership: title: Üyelik label: Yeni kayıtlara izin ver text: Herhangi birinin yeni hesap oluşturmasını engellemek için kapatın. email_registration: title: E-posta kaydı label: E-posta kaydına izin ver text: Herhangi birinin e-posta yoluyla yeni hesap oluşturmasını engellemek için kapatın. allowed_email_domains: title: İzin verilen e-posta alan adları text: Kullanıcıların hesap kaydı yapması gereken e-posta alan adları. Her satırda bir alan adı. Boş olduğunda dikkate alınmaz. private: title: Özel label: Giriş gerekli text: Bu topluluğa sadece giriş yapmış kullanıcılar erişebilir. password_login: title: Parola ile giriş label: E-posta ve parola ile girişe izin ver text: "UYARI: Kapatırsanız, daha önce başka bir giriş yöntemi yapılandırmadıysanız giriş yapamayabilirsiniz." installed_plugins: title: Kurulu Eklentiler plugin_link: Eklentiler işlevselliği genişletir ve artırır. Eklentileri <1>Eklenti Deposu'nda bulabilirsiniz. filter: all: Tümü active: Aktif inactive: Pasif outdated: Güncel değil plugins: label: Eklentiler text: Mevcut bir eklenti seçin. name: İsim version: Sürüm status: Durum action: Eylem deactivate: Devre dışı bırak activate: Etkinleştir settings: Ayarlar settings_users: title: Kullanıcılar avatar: label: Varsayılan avatar text: Kendi özel avatarı olmayan kullanıcılar için. gravatar_base_url: label: Gravatar temel URL'si text: Gravatar sağlayıcısının API temel URL'si. Boş olduğunda dikkate alınmaz. profile_editable: title: Profil düzenlenebilir allow_update_display_name: label: Kullanıcıların görünen adlarını değiştirmelerine izin ver allow_update_username: label: Kullanıcıların kullanıcı adlarını değiştirmelerine izin ver allow_update_avatar: label: Kullanıcıların profil resimlerini değiştirmelerine izin ver allow_update_bio: label: Kullanıcıların hakkında bilgilerini değiştirmelerine izin ver allow_update_website: label: Kullanıcıların web sitelerini değiştirmelerine izin ver allow_update_location: label: Kullanıcıların konumlarını değiştirmelerine izin ver privilege: title: Ayrıcalıklar level: label: Gereken itibar seviyesi text: Ayrıcalıklar için gereken itibarı seçin msg: should_be_number: giriş bir sayı olmalıdır number_larger_1: sayı 1'e eşit veya daha büyük olmalıdır badges: action: Eylem active: Aktif activate: Etkinleştir all: Tümü awards: Ödüller deactivate: Devre dışı bırak filter: placeholder: İsme göre filtreleme, badge:id group: Grup inactive: Pasif name: İsim show_logs: Kayıtları göster status: Durum title: Rozetler apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (isteğe bağlı) empty: boş olamaz invalid: geçersiz btn_submit: Kaydet not_found_props: "Gerekli {{ key }} özelliği bulunamadı." select: Seç page_review: review: İnceleme proposed: önerilen question_edit: Soru düzenleme answer_edit: Cevap düzenleme tag_edit: Etiket düzenleme edit_summary: Düzenleme özeti edit_question: Soruyu düzenle edit_answer: Cevabı düzenle edit_tag: Etiketi düzenle empty: İncelenecek görev kalmadı. approve_revision_tip: Bu revizyonu onaylıyor musunuz? approve_flag_tip: Bu bildirimi onaylıyor musunuz? approve_post_tip: Bu gönderiyi onaylıyor musunuz? approve_user_tip: Bu kullanıcıyı onaylıyor musunuz? suggest_edits: Önerilen düzenlemeler flag_post: Gönderiyi bildir flag_user: Kullanıcıyı bildir queued_post: Sıradaki gönderi queued_user: Sıradaki kullanıcı filter_label: Tür reputation: itibar flag_post_type: Bu gönderiyi {{ type }} olarak bildirdi. flag_user_type: Bu kullanıcıyı {{ type }} olarak bildirdi. edit_post: Gönderiyi düzenle list_post: Gönderiyi listele unlist_post: Gönderiyi listeden kaldır timeline: undeleted: silme geri alındı deleted: silindi downvote: negatif oy upvote: pozitif oy accept: kabul et cancelled: iptal edildi commented: yorum yapıldı rollback: geri alındı edited: düzenlendi answered: cevaplandı asked: soruldu closed: kapatıldı reopened: yeniden açıldı created: oluşturuldu pin: sabitlendi unpin: sabitlenme kaldırıldı show: listelendi hide: listelenmedi title: "Geçmiş:" tag_title: "Zaman çizelgesi:" show_votes: "Oyları göster" n_or_a: Yok title_for_question: "Zaman çizelgesi:" title_for_answer: "{{ author }} tarafından {{ title }} sorusuna verilen cevabın zaman çizelgesi" title_for_tag: "Etiket için zaman çizelgesi" datetime: Tarih/Saat type: Tür by: Yapan comment: Yorum no_data: "Hiçbir şey bulamadık." users: title: Kullanıcılar users_with_the_most_reputation: Bu hafta en yüksek itibar puanına sahip kullanıcılar users_with_the_most_vote: Bu hafta en çok oy veren kullanıcılar staffs: Topluluk ekibimiz reputation: itibar votes: oy prompt: leave_page: Sayfadan ayrılmak istediğinizden emin misiniz? changes_not_save: Değişiklikleriniz kaydedilmeyebilir. draft: discard_confirm: Taslağınızı atmak istediğinizden emin misiniz? messages: post_deleted: Bu gönderi silindi. post_cancel_deleted: Bu gönderinin silme işlemi geri alındı. post_pin: Bu gönderi sabitlendi. post_unpin: Bu gönderinin sabitlenmesi kaldırıldı. post_hide_list: Bu gönderi listede gizlendi. post_show_list: Bu gönderi listede gösterildi. post_reopen: Bu gönderi yeniden açıldı. post_list: Bu gönderi listelendi. post_unlist: Bu gönderi listeden kaldırıldı. post_pending: Gönderiniz inceleme bekliyor. Bu bir önizlemedir, onaylandıktan sonra görünür olacaktır. post_closed: Bu gönderi kapatıldı. answer_deleted: Bu cevap silindi. answer_cancel_deleted: Bu cevabın silme işlemi geri alındı. change_user_role: Bu kullanıcının rolü değiştirildi. user_inactive: Bu kullanıcı zaten etkin değil. user_normal: Bu kullanıcı zaten normal durumda. user_suspended: Bu kullanıcı askıya alındı. user_deleted: Bu kullanıcı silindi. user_added: User has been added successfully. badge_activated: Bu rozet etkinleştirildi. badge_inactivated: Bu rozet devre dışı bırakıldı. users_deleted: Bu kullanıcılar silindi. posts_deleted: Bu sorular silindi. answers_deleted: Bu cevaplar silindi. copy: Panoya kopyala copied: Kopyalandı external_content_warning: Harici resimler/medya gösterilmiyor. ================================================ FILE: i18n/uk_UA.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Успішно. unknown: other: Невідома помилка. request_format_error: other: Неприпустимий формат запиту. unauthorized_error: other: Не авторизовано. database_error: other: Помилка сервера даних. forbidden_error: other: Заборонено. duplicate_request_error: other: Повторний запит. action: report: other: Відмітити edit: other: Редагувати delete: other: Видалити close: other: Закрити reopen: other: Відкрити знову forbidden_error: other: Заборонено. pin: other: Закріпити hide: other: Вилучити зі списку unpin: other: Відкріпити show: other: Список invite_someone_to_answer: other: Редагувати undelete: other: Скасувати видалення merge: other: Merge role: name: user: other: Користувач admin: other: Адмін moderator: other: Модератор description: user: other: За замовчуванням без спеціального доступу. admin: other: Має повний доступ до сайту. moderator: other: Має доступ до всіх дописів, окрім налаштувань адміністратора. privilege: level_1: description: other: Рівень 1 (для приватної команди, групи потрібна менша репутація) level_2: description: other: Рівень 2 (низька репутація, необхідна для стартап-спільноти) level_3: description: other: Рівень 3 (висока репутація, необхідна для зрілої спільноти) level_custom: description: other: Користувацький рівень rank_question_add_label: other: Задати питання rank_answer_add_label: other: Написати відповідь rank_comment_add_label: other: Написати коментар rank_report_add_label: other: Відмітити rank_comment_vote_up_label: other: Проголосувати за коментар rank_link_url_limit_label: other: Публікуйте більш ніж 2 посилання одночасно rank_question_vote_up_label: other: Проголосувати за питання rank_answer_vote_up_label: other: Проголосувати за відповідь rank_question_vote_down_label: other: Проголосувати проти питання rank_answer_vote_down_label: other: Проголосувати проти відповіді rank_invite_someone_to_answer_label: other: Запросити когось відповісти rank_tag_add_label: other: Створити новий теґ rank_tag_edit_label: other: Редагувати опис теґу (необхідно розглянути) rank_question_edit_label: other: Редагувати чуже питання (необхідно розглянути) rank_answer_edit_label: other: Редагувати чужу відповідь (необхідно розглянути) rank_question_edit_without_review_label: other: Редагувати чуже питання без розгляду rank_answer_edit_without_review_label: other: Редагувати чужу відповідь без розгляду rank_question_audit_label: other: Переглянути редагування питання rank_answer_audit_label: other: Переглянути редагування відповіді rank_tag_audit_label: other: Переглянути редагування теґу rank_tag_edit_without_review_label: other: Редагувати опис теґу без розгляду rank_tag_synonym_label: other: Керування синонімами тегів email: other: Електронна пошта e_mail: other: Електронна пошта password: other: Пароль pass: other: Пароль old_pass: other: Current password original_text: other: Цей допис email_or_password_wrong_error: other: Електронна пошта та пароль не збігаються. error: common: invalid_url: other: Невірна URL. status_invalid: other: Неприпустимий статус. password: space_invalid: other: Пароль не може містити пробіли. admin: cannot_update_their_password: other: Ви не можете змінити свій пароль. cannot_edit_their_profile: other: Ви не можете змінити свій профіль. cannot_modify_self_status: other: Ви не можете змінити свій статус. email_or_password_wrong: other: Електронна пошта та пароль не збігаються. answer: not_found: other: Відповідь не знайдено. cannot_deleted: other: Немає дозволу на видалення. cannot_update: other: Немає дозволу на оновлення. question_closed_cannot_add: other: Питання закриті й не можуть бути додані. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Коментарі не можна редагувати. not_found: other: Коментар не знайдено. cannot_edit_after_deadline: other: Час коментаря був занадто довгим, щоб його можна було змінити. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: Такий E-mail вже існує. need_to_be_verified: other: Електронна пошта повинна бути підтверджена. verify_url_expired: other: Термін дії підтвердженої URL-адреси закінчився, будь ласка, надішліть листа повторно. illegal_email_domain_error: other: З цього поштового домену заборонено надсилати електронну пошту. Будь ласка, використовуйте інший. lang: not_found: other: Мовний файл не знайдено. object: captcha_verification_failed: other: Неправильно введено капчу. disallow_follow: other: Вам не дозволено підписатися. disallow_vote: other: Вам не дозволено голосувати. disallow_vote_your_self: other: Ви не можете проголосувати за власну публікацію. not_found: other: Обʼєкт не знайдено. verification_failed: other: Не вдалося виконати перевірку. email_or_password_incorrect: other: Електронна пошта та пароль не збігаються. old_password_verification_failed: other: Не вдалося перевірити старий пароль new_password_same_as_previous_setting: other: Новий пароль збігається з попереднім. already_deleted: other: Публікацію видалено. meta: object_not_found: other: Мета-об'єкт не знайдено question: already_deleted: other: Публікацію видалено. under_review: other: Ваше повідомлення очікує на розгляд. Його буде видно після того, як воно буде схвалено. not_found: other: Питання не знайдено. cannot_deleted: other: Немає дозволу на видалення. cannot_close: other: Немає дозволу на закриття. cannot_update: other: Немає дозволу на оновлення. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Ранг репутації не відповідає умові. vote_fail_to_meet_the_condition: other: Дякуємо за відгук. Щоб проголосувати, вам потрібна репутація не нижче {{.Rank}}. no_enough_rank_to_operate: other: Щоб це зробити, вам потрібна репутація не менше {{.Rank}}. report: handle_failed: other: Не вдалося обробити звіт. not_found: other: Звіт не знайдено. tag: already_exist: other: Теґ уже існує. not_found: other: Теґ не знайдено. recommend_tag_not_found: other: Рекомендований теґ не існує. recommend_tag_enter: other: Будь ласка, введіть принаймні один необхідний тег. not_contain_synonym_tags: other: Не повинно містити теґи синонімів. cannot_update: other: Немає дозволу на оновлення. is_used_cannot_delete: other: Ви не можете видалити теґ, який використовується. cannot_set_synonym_as_itself: other: Ви не можете встановити синонім поточного тегу як сам тег. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: Ім’я відправника не може бути електронною адресою. theme: not_found: other: Тему не знайдено. revision: review_underway: other: Наразі неможливо редагувати, є версія в черзі перегляду. no_permission: other: Немає дозволу на перегляд. user: external_login_missing_user_id: other: Платформа сторонніх розробників не надає унікальний ідентифікатор користувача, тому ви не можете увійти, будь ласка, зв’яжіться з адміністратором вебсайту. external_login_unbinding_forbidden: other: Будь ласка, встановіть пароль для входу до свого облікового запису, перш ніж видалити ім'я користувача. email_or_password_wrong: other: other: Електронна пошта та пароль не збігаються. not_found: other: Користувач не знайдений. suspended: other: Користувач був призупинений. username_invalid: other: Ім'я користувача недійсне. username_duplicate: other: Це ім'я користувача вже використовується. set_avatar: other: Не вдалося встановити аватар. cannot_update_your_role: other: Ви не можете змінити вашу роль. not_allowed_registration: other: На цей час сайт не відкритий для реєстрації. not_allowed_login_via_password: other: Наразі на сайті заборонено вхід за допомогою пароля. access_denied: other: Доступ заборонено page_access_denied: other: Ви не маєте доступу до цієї сторінки. add_bulk_users_format_error: other: "Помилка формату {{.Field}} біля '{{.Content}}' у рядку {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Кількість користувачів, яких ви додаєте одночасно, має бути в діапазоні 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Не вдалося прочитати конфігурацію database: connection_failed: other: Не вдалося встановити з'єднання з базою даних create_table_failed: other: Не вдалося створити таблицю install: create_config_failed: other: Не вдалося створити config.yaml файл. upload: unsupported_file_format: other: Непідтримуваний формат файлу. site_info: config_not_found: other: Конфігурацію сайту не знайдено. badge: object_not_found: other: Об'єкт значка не знайдено reason: spam: name: other: спам desc: other: Це повідомлення є рекламою або вандалізмом. Воно не є корисним або не має відношення до поточної теми. rude_or_abusive: name: other: грубо чи образливо desc: other: "Розумна людина вважатиме такий зміст неприйнятним для ввічливого спілкування." a_duplicate: name: other: дублікат desc: other: Це питання ставилося раніше, і на нього вже є відповідь. placeholder: other: Введіть наявне посилання на питання not_a_answer: name: other: не відповідь desc: other: "Це повідомлення було опубліковане як відповідь, але воно не є спробою відповісти на запитання. Можливо, його слід відредагувати, прокоментувати, поставити інше запитання або взагалі видалити." no_longer_needed: name: other: більше не потрібно desc: other: Цей коментар є застарілим, розмовним або не стосується цієї публікації. something: name: other: інше desc: other: Ця публікація вимагає уваги персоналу з іншої причини, що не вказана вище. placeholder: other: Дайте нам знати, що саме вас турбує community_specific: name: other: причина для спільноти desc: other: Це запитання не відповідає правилам спільноти. not_clarity: name: other: потребує деталей або ясності desc: other: Наразі це запитання містить кілька запитань в одному. Воно має бути зосереджене лише на одній проблемі. looks_ok: name: other: виглядає добре desc: other: Цей допис хороший, як є, і не є низької якості. needs_edit: name: other: потребує редагування, і я це зробив desc: other: Поліпшіть та виправте проблеми з цією публікацією самостійно. needs_close: name: other: потрібно закрити desc: other: Закрите питання не може відповісти, але все ще може редагувати, голосувати і коментувати. needs_delete: name: other: потрібно видалити desc: other: Цей допис буде видалено. question: close: duplicate: name: other: спам desc: other: Це питання ставилося раніше, і на нього вже є відповідь. guideline: name: other: причина для спільноти desc: other: Це запитання не відповідає правилам спільноти. multiple: name: other: потребує деталей або ясності desc: other: Наразі це питання включає кілька запитань в одному. Воно має зосереджуватися лише на одній проблемі. other: name: other: інше desc: other: Для цього допису потрібна інша причина, не зазначена вище. operation_type: asked: other: запитав answered: other: відповів modified: other: змінено deleted_title: other: Видалене питання questions_title: other: Питання tag: tags_title: other: Теґи no_description: other: Тег не має опису. notification: action: update_question: other: оновлене питання answer_the_question: other: питання з відповіддю update_answer: other: оновлена відповідь accept_answer: other: прийнята відповідь comment_question: other: прокоментоване питання comment_answer: other: прокоментована відповідь reply_to_you: other: відповів(-ла) вам mention_you: other: згадав(-ла) вас your_question_is_closed: other: Ваше запитання закрито your_question_was_deleted: other: Ваше запитання видалено your_answer_was_deleted: other: Вашу відповідь видалено your_comment_was_deleted: other: Ваш коментар видалено up_voted_question: other: питання, за яке найбільше проголосували down_voted_question: other: питання, за яке проголосували менше up_voted_answer: other: відповідь, за яку проголосували найбільше down_voted_answer: other: downvoted answer up_voted_comment: other: коментар, за який проголосували invited_you_to_answer: other: запросив(-ла) вас відповісти earned_badge: other: Ви заробили бейдж "{{.BadgeName}}" email_tpl: change_email: title: other: "[{{.SiteName}}] Підтвердіть нову адресу електронної пошти" body: other: "Підтвердьте свою нову адресу електронної пошти для {{.SiteName}} натиснувши на наступне посилання:
\n{{.ChangeEmailUrl}}

\n\nЯкщо ви не запитували цю зміну, будь ласка, ігноруйте цей лист.

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} відповів(-ла) на ваше запитання" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nПереглянути на {{.SiteName}}

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.

\n\nВідписатися" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} запросив(-ла) вас відповісти" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Думаю, ви можете знати відповідь.

\nПереглянути на {{.SiteName}}

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.

\n\nВідписатися" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} прокоментували ваш допис" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nПереглянути на {{.SiteName}}

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.

\n\nВідписатися" new_question: title: other: "[{{.SiteName}}] Нове питання: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Скидання пароля" body: other: "Хтось попросив скинути ваш пароль на {{.SiteName}}.

\n\nЯкщо це не ви, можете сміливо ігнорувати цей лист.

\n\nПерейдіть за наступним посиланням, щоб вибрати новий пароль:
\n{{.PassResetUrl}}\n

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена." register: title: other: "[{{.SiteName}}] Підтвердьте свій новий обліковий запис" body: other: "Ласкаво просимо до {{.SiteName}}!

\n\nПерейдіть за наступним посиланням, щоб підтвердити та активувати свій новий обліковий запис:
\n{{.RegisterUrl}}

\n\nЯкщо наведене вище посилання не відкривається, спробуйте скопіювати і вставити його в адресний рядок вашого веб-браузера.\n

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена." test: title: other: "[{{.SiteName}}] Тестовий електронний лист" body: other: "Це тестовий електронний лист.\n

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена." action_activity_type: upvote: other: підтримати upvoted: other: підтримано downvote: other: голос "проти" downvoted: other: проголосував проти accept: other: прийняти accepted: other: прийнято edit: other: редагувати review: queued_post: other: Допис у черзі flagged_post: other: Відмічений пост suggested_post_edit: other: Запропоновані зміни reaction: tooltip: other: "{{ .Names }} і {{ .Count }} більше..." badge: default_badges: autobiographer: name: other: Автобіограф desc: other: Заповнена інформація про профіль. certified: name: other: Підтверджений desc: other: Завершено наш новий посібник користувача. editor: name: other: Редактор desc: other: Перше редагування посту. first_flag: name: other: Перший прапор desc: other: Спочатку позначено допис. first_upvote: name: other: Перший голос за desc: other: Першим голосував за допис. first_link: name: other: Перше посилання desc: other: First added a link to another post. first_reaction: name: other: Перша реакція desc: other: Першим відреагував на допис. first_share: name: other: Перше поширення desc: other: Перший поділився публікацією. scholar: name: other: Вчений desc: other: Поставив питання і прийняв відповідь. commentator: name: other: Коментатор desc: other: Залиште 5 коментарів. new_user_of_the_month: name: other: Новий користувач місяця desc: other: Видатні внески за їх перший місяць. read_guidelines: name: other: Прочитайте Інструкцію desc: other: Прочитайте [рекомендації для спільноти]. reader: name: other: Читач desc: other: Прочитайте кожну відповідь у темі з більш ніж 10 відповідями. welcome: name: other: Ласкаво просимо desc: other: Отримав голос. nice_share: name: other: Гарне поширення desc: other: Поділилися постом з 25 унікальними відвідувачами. good_share: name: other: Хороше поширення desc: other: Поділилися постом з 300 унікальними відвідувачами. great_share: name: other: Відмінне поширення desc: other: Поділилися постом з 1000 унікальними відвідувачами. out_of_love: name: other: З любові desc: other: Використав 50 голосів «за» за день. higher_love: name: other: Вище кохання desc: other: Використав 50 голосів «за» за день 5 разів. crazy_in_love: name: other: Божевільний в любові desc: other: Використав 50 голосів «за» за день 20 разів. promoter: name: other: Промоутер desc: other: Запросив користувача. campaigner: name: other: Агітатор desc: other: Запрошено 3 основних користувачів. champion: name: other: Чемпіон desc: other: Запросив 5 учасників. thank_you: name: other: Дякую desc: other: Має 20 дописів, за які проголосували, і віддав 10 голосів «за». gives_back: name: other: Дає назад desc: other: Має 100 дописів, за які проголосували, і віддав 100 голосів «за». empathetic: name: other: Емпатичний desc: other: Має 500 дописів, за які проголосували, і віддав 1000 голосів «за». enthusiast: name: other: Ентузіаст desc: other: Відвідано 10 днів поспіль. aficionado: name: other: Шанувальник desc: other: Відвідано 100 днів поспіль. devotee: name: other: Відданий desc: other: Відвідано 365 днів поспіль. anniversary: name: other: Річниця desc: other: Активний учасник на рік, опублікував принаймні один раз. appreciated: name: other: Оцінений desc: other: Отримано 1 голос за 20 дописів. respected: name: other: Шанований desc: other: Отримано 2 голоси за 100 дописів. admired: name: other: Захоплений desc: other: Отримано 5 голосів за 300 дописів. solved: name: other: Вирішено desc: other: Нехай відповідь буде прийнята. guidance_counsellor: name: other: Радник супроводу desc: other: Прийміть 10 відповідей. know_it_all: name: other: Усезнайко desc: other: Було прийнято 50 відповідей. solution_institution: name: other: Інституція рішення desc: other: Було прийнято 150 відповідей. nice_answer: name: other: Чудова відповідь desc: other: Оцінка відповіді на 10 або більше. good_answer: name: other: Гарна відповідь desc: other: Оцінка відповіді на 25 або більше. great_answer: name: other: Чудова відповідь desc: other: Оцінка відповіді на 50 або більше. nice_question: name: other: Гарне питання desc: other: Оцінка питання на 10 або більше. good_question: name: other: Хороше питання desc: other: Оцінка питання на 25 або більше. great_question: name: other: Відмінне питання desc: other: Оцінка питання на 50 або більше. popular_question: name: other: Популярне питання desc: other: Питання з 500 переглядами. notable_question: name: other: Помітне питання desc: other: Питання з 1000 переглядами. famous_question: name: other: Знамените питання desc: other: Питання з 5000 переглядами. popular_link: name: other: Популярне посилання desc: other: Опубліковано зовнішнє посилання з 50 натисканнями. hot_link: name: other: Гаряче посилання desc: other: Опубліковано зовнішнє посилання з 300 натисканнями. famous_link: name: other: Знамените Посилання desc: other: Опубліковано зовнішнє посилання зі 100 натисканнями. default_badge_groups: getting_started: name: other: Початок роботи community: name: other: Спільнота posting: name: other: Публікація # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Як відформатувати desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Назад next: Далі page_title: question: Запитання questions: Запитання tag: Теґ tags: Теґи tag_wiki: тег вікі create_tag: Створити теґ edit_tag: Редагувати теґ ask_a_question: Create Question edit_question: Редагувати запитання edit_answer: Редагувати відповідь search: Пошук posts_containing: Публікації, що містять settings: Налаштування notifications: Сповіщення login: Увійти sign_up: Зареєструватися account_recovery: Відновлення облікового запису account_activation: Активація облікового запису confirm_email: Підтвердити електронну адресу account_suspended: Обліковий запис призупинено admin: Адмін change_email: Змінити електронну адресу install: Встановлення Answer upgrade: Оновлення Answer maintenance: Технічне обслуговування сайту users: Користувачі oauth_callback: Обробка http_404: Помилка HTTP 404 http_50X: Помилка HTTP 500 http_403: Помилка HTTP 403 logout: Вийти posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Сповіщення inbox: Вхідні achievement: Досягнення new_alerts: Нові сповіщення all_read: Позначити все як прочитане show_more: Показати більше someone: Хтось inbox_type: all: Усі posts: Публікації invites: Запрошення votes: Голоси answer: Відповідь question: Запитання badge_award: Значок suspended: title: Ваш обліковий запис було призупинено until_time: "Ваш обліковий запис призупинено до {{ time }}." forever: Цього користувача призупинено назавжди. end: Ви не дотримуєтеся правил спільноти. contact_us: Зв'яжіться з нами editor: blockquote: text: Блок Цитування bold: text: Надійний chart: text: Діаграма flow_chart: Блок-схема sequence_diagram: Діаграма послідовності class_diagram: Діаграма класів state_diagram: Діаграма станів entity_relationship_diagram: Діаграма зв'язків сутностей user_defined_diagram: Визначена користувачем діаграма gantt_chart: Діаграма Ґанта pie_chart: Кругова діаграма code: text: Зразок коду add_code: Додати зразок коду form: fields: code: label: Код msg: empty: Код не може бути порожнім. language: label: Мова placeholder: Автоматичне визначення btn_cancel: Скасувати btn_confirm: Додати formula: text: Формула options: inline: Вбудована формула block: Формула блоку heading: text: Заголовок options: h1: Заголовок 1 h2: Заголовок 2 h3: Заголовок 3 h4: Заголовок 4 h5: Заголовок 5 h6: Заголовок 6 help: text: Допомога hr: text: Горизонтальна лінійка image: text: Зображення add_image: Додати зображення tab_image: Завантажити зображення form_image: fields: file: label: Файл зображення btn: Обрати зображення msg: empty: Файл не може бути порожнім. only_image: Допустимі лише файли зображень. max_size: Розмір файлу не може перевищувати {{size}} МБ. desc: label: Опис tab_url: URL зображення form_url: fields: url: label: URL зображення msg: empty: URL-адреса зображення не може бути пустою. name: label: Опис btn_cancel: Скасувати btn_confirm: Додати uploading: Завантаження indent: text: Абзац outdent: text: Відступ italic: text: Акцент link: text: Гіперпосилання add_link: Додати гіперпосилання form: fields: url: label: URL msg: empty: URL-адреса не може бути пустою. name: label: Опис btn_cancel: Скасувати btn_confirm: Додати ordered_list: text: Нумерований список unordered_list: text: Маркований список table: text: Таблиця heading: Заголовок cell: Клітинка file: text: Прикріпити файли not_supported: "Не підтримується цей тип файлу. Спробуйте ще раз з {{file_type}}." max_size: "Розмір прикріплених файлів не може перевищувати {{size}} МБ." close_modal: title: Я закриваю цей пост, оскільки... btn_cancel: Скасувати btn_submit: Надіслати remark: empty: Не може бути порожнім. msg: empty: Будь ласка, оберіть причину. report_modal: flag_title: Я ставлю відмітку, щоб повідомити про цю публікацію як... close_title: Я закриваю цей пост, оскільки... review_question_title: Переглянути питання review_answer_title: Переглянути відповідь review_comment_title: Переглянути коментар btn_cancel: Скасувати btn_submit: Надіслати remark: empty: Не може бути порожнім. msg: empty: Будь ласка, оберіть причину. not_a_url: Формат URL неправильний. url_not_match: Походження URL не збігається з поточним вебсайтом. tag_modal: title: Створити новий теґ form: fields: display_name: label: Ім'я для відображення msg: empty: Ім'я для відображення не може бути порожнім. range: Ім'я для відображення до 35 символів. slug_name: label: Скорочена URL-адреса desc: Скорочення URL до 35 символів. msg: empty: Скорочення URL не може бути пустим. range: Скорочення URL до 35 символів. character: Скорочення URL містить незадовільний набір символів. desc: label: Опис revision: label: Редакція edit_summary: label: Підсумок редагування placeholder: >- Коротко поясніть ваші зміни (виправлена орфографія, виправлена граматика, покращене форматування) btn_cancel: Скасувати btn_submit: Надіслати btn_post: Опублікувати новий теґ tag_info: created_at: Створено edited_at: Відредаговано history: Історія synonyms: title: Синоніми text: Наступні теги буде змінено на empty: Синонімів не знайдено. btn_add: Додати синонім btn_edit: Редагувати btn_save: Зберегти synonyms_text: Наступні теги буде змінено на delete: title: Видалити цей теґ tip_with_posts: >-

Ми не дозволяємо видаляти тег з дописами.

Передусім, будь ласка, вилучіть цей тег з дописів.

tip_with_synonyms: >-

Ми не дозволяємо видаляти тег із синонімами.

Передусім, будь ласка, вилучіть синоніми з цього тега.

tip: Ви впевнені, що хочете видалити? close: Закрити merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Редагувати теґ default_reason: Редагувати теґ default_first_reason: Додати теґ btn_save_edits: Зберегти зміни btn_cancel: Скасувати dates: long_date: МММ Д long_date_with_year: "МММ Д, РРРР" long_date_with_time: "МММ Д, РРРР [о] ГГ:хв" now: зараз x_seconds_ago: "{{count}}сек назад" x_minutes_ago: "{{count}}хв назад" x_hours_ago: "{{count}}год назад" hour: година day: день hours: годин days: дні month: month months: months year: year reaction: heart: серце smile: посмішка frown: насупився btn_label: додавати або вилучати реакції undo_emoji: скасувати реакцію {{ emoji }} react_emoji: реагувати з {{ emoji }} unreact_emoji: не реагувати з {{ emoji }} comment: btn_add_comment: Додати коментар reply_to: Відповісти на btn_reply: Відповісти btn_edit: Редагувати btn_delete: Видалити btn_flag: Відмітити btn_save_edits: Зберегти зміни btn_cancel: Скасувати show_more: "Ще {{count}} коментарів" tip_question: >- Використовуйте коментарі, щоб попросити більше інформації або запропонувати покращення. Уникайте відповідей на питання в коментарях. tip_answer: >- Використовуйте коментарі, щоб відповідати іншим користувачам або повідомляти їх про зміни. Якщо ви додаєте нову інформацію, відредагуйте свою публікацію, а не коментуйте. tip_vote: Це додає щось корисне до допису edit_answer: title: Редагувати відповідь default_reason: Редагувати відповідь default_first_reason: Додати відповідь form: fields: revision: label: Редакція answer: label: Відповідь feedback: characters: вміст має бути не менше 6 символів. edit_summary: label: Редагувати підсумок placeholder: >- Коротко поясніть ваші зміни (виправлена орфографія, виправлена граматика, покращене форматування) btn_save_edits: Зберегти зміни btn_cancel: Скасувати tags: title: Теґи sort_buttons: popular: Популярне name: Назва newest: Найновіші button_follow: Підписатися button_following: Підписані tag_label: запитання search_placeholder: Фільтрувати за назвою теґу no_desc: Цей теґ не має опису. more: Більше wiki: Вікі ask: title: Create Question edit_title: Редагувати питання default_reason: Редагувати питання default_first_reason: Create question similar_questions: Подібні питання form: fields: revision: label: Редакція title: label: Назва placeholder: What's your topic? Be specific. msg: empty: Назва не може бути порожньою. range: Назва до 150 символів body: label: Тіло msg: empty: Тіло не може бути порожнім. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Теґи msg: empty: Теґи не можуть бути порожніми. answer: label: Відповідь msg: empty: Відповідь не може бути порожньою. edit_summary: label: Редагувати підсумок placeholder: >- Коротко поясніть ваші зміни (виправлена орфографія, виправлена граматика, покращене форматування) btn_post_question: Опублікуйте своє запитання btn_save_edits: Зберегти зміни answer_question: Відповісти на власне питання post_question&answer: Опублікуйте своє запитання і відповідь tag_selector: add_btn: Додати теґ create_btn: Створити новий теґ search_tag: Шукати теґ hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Не знайдено тегів tag_required_text: Обов'язковий тег (принаймні один) header: nav: question: Запитання tag: Теґи user: Користувачі badges: Значки profile: Профіль setting: Налаштування logout: Вийти admin: Адмін review: Огляд bookmark: Закладки moderation: Модерація search: placeholder: Пошук footer: build_on: Powered by <1> Apache Answer upload_img: name: Змінити loading: завантаження... pic_auth_code: title: Капча placeholder: Введіть текст вище msg: empty: Капча не може бути порожньою. inactive: first: >- Ви майже закінчили! Ми надіслали лист для активації на {{mail}}. Будь ласка, дотримуйтесь інструкцій, щоб активувати свій обліковий запис. info: "Якщо він не надійшов, перевірте папку зі спамом." another: >- Ми надіслали вам інший електронний лист для активації на {{mail}}. Це може зайняти кілька хвилин, перш ніж він прибуде; обов'язково перевірте теку зі спамом. btn_name: Повторно надіслати електронний лист для активації change_btn_name: Змінити електронну пошту msg: empty: Не може бути порожнім. resend_email: url_label: Ви впевнені, що бажаєте повторно надіслати електронний лист для активації? url_text: Ви також можете дати користувачеві наведене вище посилання для активації. login: login_to_continue: Увійдіть, щоб продовжити info_sign: Немає облікового запису? <1>Зареєструйтесь info_login: Вже маєте обліковий запис? <1>Увійдіть agreements: Реєструючись, ви погоджуєтеся з <1>політикою конфіденційності та <3>умовами використання. forgot_pass: Забули пароль? name: label: Ім’я msg: empty: Ім'я не може бути порожнім. range: Ім'я повинно мати довжину від 2 до 30 символів. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Електронна пошта msg: empty: Поле електронної пошти не може бути пустим. password: label: Пароль msg: empty: Поле паролю не може бути порожнім. different: Двічі введені паролі є несумісними account_forgot: page_title: Забули свій пароль btn_name: Надішліть мені електронний лист для відновлення send_success: >- Якщо обліковий запис збігається з {{mail}}, незабаром ви отримаєте електронний лист з інструкціями щодо скидання пароля. email: label: Електронна пошта msg: empty: Поле електронної пошти не може бути пустим. change_email: btn_cancel: Скасувати btn_update: Оновити адресу електронної пошти send_success: >- Якщо обліковий запис збігається з {{mail}}, незабаром ви отримаєте електронний лист з інструкціями щодо скидання пароля. email: label: Нова електронна пошта msg: empty: Поле електронної пошти не може бути пустим. oauth: connect: З'єднати з {{ auth_name }} remove: Видалити {{ auth_name }} oauth_bind_email: subtitle: Додайте резервну електронну пошту до свого облікового запису. btn_update: Оновити адресу електронної пошти email: label: Електронна пошта msg: empty: Поле електронної пошти не може бути пустим. modal_title: Електронна адреса вже існує. modal_content: Ця електронна адреса вже зареєстрована. Ви впевнені, що бажаєте підключитися до існуючого облікового запису? modal_cancel: Змінити електронну пошту modal_confirm: Під'єднати до існуючого облікового запису password_reset: page_title: Скинути пароль btn_name: Скинути мій пароль reset_success: >- Ви успішно змінили пароль; вас буде перенаправлено на сторінку входу в систему. link_invalid: >- На жаль, це посилання для зміни пароля більше недійсне. Можливо, ваш пароль уже скинуто? to_login: Продовжити вхід на сторінку password: label: Пароль msg: empty: Поле паролю не може бути порожнім. length: Довжина повинна бути від 8 до 32 символів different: Двічі введені паролі є несумісними password_confirm: label: Підтвердити новий пароль settings: page_title: Налаштування goto_modify: Перейти до зміни nav: profile: Профіль notification: Сповіщення account: Обліковий запис interface: Інтерфейс profile: heading: Профіль btn_name: Зберегти display_name: label: Ім'я для відображення msg: Ім'я для відображення не може бути порожнім. msg_range: Display name must be 2-30 characters in length. username: label: Ім'я користувача caption: Користувачі можуть згадувати вас як "@username". msg: Ім’я користувача не може бути порожнім. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Зображення профілю gravatar: Gravatar gravatar_text: Ви можете змінити зображення на custom: Власне custom_text: Ви можете завантажити своє зображення. default: Системне msg: Будь ласка, завантажте аватар bio: label: Про мене website: label: Вебсайт placeholder: "https://example.com" msg: Неправильний формат вебсайту location: label: Місцезнаходження placeholder: "Місто, Країна" notification: heading: Сповіщення електронною поштою turn_on: Увімкнути inbox: label: Вхідні сповіщення description: Відповіді на ваші запитання, коментарі, запрошення тощо. all_new_question: label: Усі нові запитання description: Отримуйте сповіщення про всі нові питання. До 50 питань на тиждень. all_new_question_for_following_tags: label: Всі нові запитання з наступними тегами description: Отримувати сповіщення про нові запитання з наступними тегами. account: heading: Обліковий запис change_email_btn: Змінити електронну пошту change_pass_btn: Змінити пароль change_email_info: >- Ми надіслали електронний лист на цю адресу. Будь ласка, дотримуйтесь інструкцій для підтвердження. email: label: Нова електронна пошта new_email: label: Нова електронна пошта msg: Нова електронна пошта не може бути порожньою. pass: label: Поточний пароль msg: Комірка паролю не може бути порожньою. password_title: Пароль current_pass: label: Поточний пароль msg: empty: Комірка поточного пароля не може бути порожньою. length: Довжина повинна бути від 8 до 32 символів. different: Два введені паролі не збігаються. new_pass: label: Новий пароль pass_confirm: label: Підтвердити новий пароль interface: heading: Інтерфейс lang: label: Мова інтерфейсу text: Мова інтерфейсу користувача. Зміниться, коли ви оновите сторінку. my_logins: title: Мої логіни label: Увійдіть або зареєструйтеся на цьому сайті, використовуючи ці облікові записи. modal_title: Видалити логін modal_content: Ви впевнені, що хочете видалити цей логін з облікового запису? modal_confirm_btn: Видалити remove_success: Успішно видалено toast: update: успішно оновлено update_password: Пароль успішно змінено. flag_success: Дякую, що відмітили. forbidden_operate_self: Заборонено застосовувати на собі review: Ваша версія з'явиться після перевірки. sent_success: Успішно відправлено related_question: title: Related answers: відповіді linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Люди запитували desc: Виберіть людей, які, на вашу думку, можуть знати відповідь. invite: Запросити відповісти add: Додати людей search: Шукати людей question_detail: action: Дія created: Created Asked: Запитали asked: запитали update: Змінено Edited: Edited edit: відредаговано commented: прокоментовано Views: Переглянуто Follow: Підписатися Following: Підписані follow_tip: Підпишіться на це запитання, щоб отримувати сповіщення answered: дано відповідь closed_in: Зачинено в show_exist: Показати наявне запитання. useful: Корисне question_useful: Це корисно і ясно question_un_useful: Це неясно або некорисно question_bookmark: Додати в закладки це питання answer_useful: Це корисно answer_un_useful: Це некорисно answers: title: Відповіді score: Оцінка newest: Найновіші oldest: Найдавніші btn_accept: Прийняти btn_accepted: Прийнято write_answer: title: Ваша відповідь edit_answer: Редагувати мою чинну відповідь btn_name: Опублікувати свою відповідь add_another_answer: Додати ще одну відповідь confirm_title: Перейти до відповіді continue: Продовжити confirm_info: >-

Ви впевнені, що хочете додати ще одну відповідь?

Натомість ви можете скористатися посиланням редагування, щоб уточнити та покращити вже існуючу відповідь.

empty: Відповідь не може бути порожньою. characters: вміст має бути не менше 6 символів. tips: header_1: Дякуємо за відповідь li1_1: Будь ласка, не забудьте відповісти на запитання. Надайте детальну інформацію та поділіться своїми дослідженнями. li1_2: Підкріплюйте будь-які ваші твердження посиланнями чи особистим досвідом. header_2: Але уникайте... li2_1: Просити про допомогу, шукати роз'яснення або реагувати на інші відповіді. reopen: confirm_btn: Відкрити знову title: Повторно відкрити цей допис content: Ви впевнені, що хочете повторно відкрити? list: confirm_btn: Список title: Показати цей допис content: Ви впевнені, що хочете скласти список? unlist: confirm_btn: Вилучити зі списку title: Вилучити допис зі списку content: Ви впевнені, що хочете вилучити зі списку? pin: title: Закріпити цей допис content: Ви впевнені, що хочете закріпити глобально? Цей допис відображатиметься вгорі всіх списків публікацій. confirm_btn: Закріпити delete: title: Видалити цей допис question: >- Ми не рекомендуємо видаляти питання з відповідями, оскільки це позбавляє майбутніх читачів цих знань.

Повторне видалення запитань із відповідями може призвести до блокування запитів у вашому обліковому записі. Ви впевнені, що хочете видалити? answer_accepted: >-

Ми не рекомендуємо видаляти прийняту відповідь, оскільки це позбавляє майбутніх читачів цих знань.

Повторне видалення прийнятих відповідей може призвести до того, що ваш обліковий запис буде заблоковано для відповідей. Ви впевнені, що хочете видалити? other: Ви впевнені, що хочете видалити? tip_answer_deleted: Ця відповідь була видалена undelete_title: Скасувати видалення цього допису undelete_desc: Ви впевнені, що бажаєте скасувати видалення? btns: confirm: Підтвердити cancel: Скасувати edit: Редагувати save: Зберегти delete: Видалити undelete: Скасувати видалення list: Список unlist: Вилучити зі списку unlisted: Вилучене зі списку login: Увійти signup: Зареєструватися logout: Вийти verify: Підтвердити create: Create approve: Затвердити reject: Відхилити skip: Пропустити discard_draft: Видалити чернетку pinned: Закріплено all: Усі question: Запитання answer: Відповідь comment: Коментар refresh: Оновити resend: Надіслати повторно deactivate: Деактивувати active: Активні suspend: Призупинити unsuspend: Відновити close: Закрити reopen: Відкрити знову ok: ОК light: Світла dark: Темна system_setting: Налаштування системи default: За замовчуванням reset: Скинути tag: Тег post_lowercase: допис filter: Фільтр ignore: Ігнорувати submit: Надіслати normal: Нормальний closed: Закриті deleted: Видалені deleted_permanently: Deleted permanently pending: Очікування more: Більше view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Результати пошуку keywords: Ключові слова options: Параметри follow: Підписатися following: Підписані counts: "{{count}} Результатів" counts_loading: "... Results" more: Більше sort_btns: relevance: Релевантність newest: Найновіші active: Активні score: Оцінка more: Більше tips: title: Підказки щодо розширеного пошуку tag: "<1>[tag] шукати за тегом" user: "<1>користувач:ім'я користувача пошук за автором" answer: "<1>відповіді:0 питання без відповіді" score: "<1>рахунок: 3 записи із 3+ рахунком" question: "<1>є:питання пошукові питання" is_answer: "<1>є:відповідь пошукові відповіді" empty: Ми не змогли нічого знайти.
Спробуйте різні або менш конкретні ключові слова. share: name: Поділитись copy: Копіювати посилання via: Поділитися дописом через... copied: Скопійовано facebook: Поділитись на Facebook twitter: Share to X cannot_vote_for_self: Ви не можете проголосувати за власну публікацію. modal_confirm: title: Помилка... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Ваш новий обліковий запис підтверджено; вас буде перенаправлено на головну сторінку. link: Перейти на головну сторінку oops: Йой! invalid: Посилання, яке ви використовували, більше не працює. confirm_new_email: Вашу адресу електронної пошти було оновлено. confirm_new_email_invalid: >- На жаль, це посилання для підтвердження більше не дійсне. Можливо, ваша електронна пошта вже була змінена? unsubscribe: page_title: Відписатися success_title: Ви успішно відписалися success_desc: Вас успішно вилучено з цього списку підписників, і ви більше не будете отримувати від нас електронні листи. link: Змінити налаштування question: following_tags: Підписки на теги edit: Редагувати save: Зберегти follow_tag_tip: Підпишіться на теги, щоб упорядкувати свій список запитань. hot_questions: Гарячі питання all_questions: Всі питання x_questions: "{{ count }} Питань" x_answers: "{{ count }} відповідей" x_posts: "{{ count }} Posts" questions: Запитання answers: Відповіді newest: Найновіші active: Активні hot: Гаряче frequent: Часто recommend: Рекомендовано score: Оцінка unanswered: Без відповідей modified: змінено answered: дано відповідь asked: запитано closed: закрито follow_a_tag: Підписатися на тег more: Більше personal: overview: Загальний огляд answers: Відповіді answer: відповідь questions: Запитання question: запитання bookmarks: Закладки reputation: Репутація comments: Коментарі votes: Голоси badges: Значки newest: Найновіше score: Оцінка edit_profile: Редагувати профіль visited_x_days: "Відвідано {{ count }} днів" viewed: Переглянуто joined: Приєднано comma: "," last_login: Переглянуто about_me: Про мене about_me_empty: "// Привіт, світ!" top_answers: Найкращі відповіді top_questions: Найкращі запитання stats: Статистика list_empty: Не знайдено жодного допису.
Можливо, ви хочете вибрати іншу вкладку? content_empty: Постів не знайдено. accepted: Прийнято answered: дано відповідь asked: запитано downvoted: проголосовано проти mod_short: MOD mod_long: Модератори x_reputation: репутація x_votes: отримані голоси x_answers: відповіді x_questions: запитання recent_badges: Нещодавні значки install: title: Встановлення next: Далі done: Готово config_yaml_error: Не вдалося створити config.yaml файл. lang: label: Будь ласка, виберіть мову db_type: label: Рушій бази даних db_username: label: Ім'я користувача placeholder: корінь msg: Ім’я користувача не може бути порожнім. db_password: label: Пароль placeholder: корінь msg: Поле паролю не може бути порожнім. db_host: label: Хост бази даних placeholder: "db:3306" msg: Хост бази даних не може бути порожнім. db_name: label: Назва бази даних placeholder: відповідь msg: Назва бази даних не може бути порожня. db_file: label: Файл бази даних placeholder: /data/answer.db msg: Файл бази даних не може бути порожнім. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Створити config.yaml label: Файл config.yaml створено. desc: >- Ви можете створити файл <1>config.yaml вручну в каталозі <1>/var/www/xxx/ і вставити в нього наступний текст. info: Після цього натисніть кнопку "Далі". site_information: Інформація про сайт admin_account: Обліковий запис адміністратора site_name: label: Назва сайту msg: Назва сайту не може бути порожньою. msg_max_length: Назва сайту повинна містити не більше 30 символів. site_url: label: URL сайту text: Адреса вашого сайту. msg: empty: URL-адреса сайту не може бути пустою. incorrect: Неправильний формат URL-адреси сайту. max_length: Максимальна довжина URL-адреси сайту – 512 символів. contact_email: label: Контактна електронна адреса text: Електронна адреса основної контактної особи, відповідальної за цей сайт. msg: empty: Контактна електронна адреса не може бути порожньою. incorrect: Неправильний формат контактної електронної пошти. login_required: label: Приватний switch: Вхід обов'язковий text: Лише авторизовані користувачі можуть отримати доступ до цієї спільноти. admin_name: label: Ім’я msg: Ім'я не може бути порожнім. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Пароль text: >- Вам знадобиться цей пароль для входу. Зберігайте його в надійному місці. msg: Поле паролю не може бути порожнім. msg_min_length: Пароль має бути не менше 8 символів. msg_max_length: Пароль має бути не менше 32 символів. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Електронна пошта text: Вам знадобиться ця електронна адреса для входу. msg: empty: Поле електронної пошти не може бути пустим. incorrect: Невірний формат електронної пошти. ready_title: Ваш сайт готовий ready_desc: >- Якщо ви коли-небудь захочете змінити інші налаштування, відвідайте <1>розділ адміністрування; знайдіть його в меню сайту. good_luck: "Веселіться, і хай щастить!" warn_title: Попередження warn_desc: >- Файл <1>config.yaml вже існує. Якщо вам потрібно скинути будь-який з елементів конфігурації в цьому файлі, будь ласка, спочатку видаліть його. install_now: Ви можете спробувати <1>встановити зараз. installed: Уже встановлено installed_desc: >- Ви, здається, уже встановили. Щоб перевстановити, спочатку очистіть старі таблиці бази даних. db_failed: Не вдалося встановити з'єднання з базою даних db_failed_desc: >- Це означає, що інформація про базу даних у вашому файлі <1>config.yaml невірна або що не вдалося встановити контакт із сервером бази даних. Це може означати, що сервер бази даних вашого хоста не працює. counts: views: перегляди votes: голоси answers: відповіді accepted: Схвалено page_error: http_error: Помилка HTTP {{ code }} desc_403: Ви не маєте дозволу на доступ до цієї сторінки. desc_404: На жаль, такої сторінки не існує. desc_50X: Сервер виявив помилку і не зміг виконати ваш запит. back_home: Повернутися на головну сторінку page_maintenance: desc: "Ми технічно обслуговуємось, ми скоро повернемося." nav_menus: dashboard: Панель contents: Зміст questions: Питання answers: Відповіді users: Користувачі badges: Значки flags: Відмітки settings: Налаштування general: Основне interface: Інтерфейс smtp: SMTP branding: Брендинг legal: Правила та умови write: Написати terms: Terms tos: Умови використання privacy: Приватність seo: SEO customize: Персоналізувати themes: Теми login: Вхід privileges: Привілеї plugins: Плагіни installed_plugins: Встановлені плагіни apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Ласкаво просимо до {{site_name}} user_center: login: Вхід qrcode_login_tip: Будь ласка, використовуйте {{ agentName }}, щоб просканувати QR-код і увійти в систему. login_failed_email_tip: Не вдалося увійти, будь ласка, дозвольте цьому додатку отримати доступ до вашої електронної пошти, перш ніж спробувати ще раз. badges: modal: title: Вітаємо content: Ти отримав новий значок. close: Закрити confirm: Переглянути значки title: Значки awarded: Присвоєно earned_×: Зароблено ×{{ number }} ×_awarded: "Присвоєно {{ number }}" can_earn_multiple: Ви можете заробити це багато разів. earned: Зароблено admin: admin_header: title: Адмін dashboard: title: Панель welcome: Ласкаво просимо до адміністратора! site_statistics: Статистика сайту questions: "Запитання:" resolved: "Вирішено:" unanswered: "Без відповідей:" answers: "Відповіді:" comments: "Коментарі:" votes: "Голоси:" users: "Користувачі:" flags: "Відмітки:" reviews: "Відгуки:" site_health: Стан сайту version: "Версія:" https: "HTTPS:" upload_folder: "Завантажити теку:" run_mode: "Активний режим:" private: Приватний public: Публічний smtp: "SMTP:" timezone: "Часовий пояс:" system_info: Інформація про систему go_version: "Перейти до версії:" database: "База даних:" database_size: "Розмір бази даних:" storage_used: "Використаний обсяг пам’яті:" uptime: "Час роботи:" links: Посилання plugins: Плаґіни github: GitHub blog: Блоґ contact: Контакт forum: Форум documents: Документи feedback: Відгук support: Підтримка review: Огляд config: Конфігурація update_to: Оновити до latest: Останній check_failed: Не вдалося перевірити "yes": "Так" "no": "Ні" not_allowed: Не дозволено allowed: Дозволено enabled: Увімкнено disabled: Вимкнено writable: Записуваний not_writable: Не можна записувати flags: title: Відмітки pending: В очікуванні completed: Завершено flagged: Відмічено flagged_type: Відмічено {{ type }} created: Створені action: Дія review: Огляд user_role_modal: title: Змінити роль користувача на... btn_cancel: Скасувати btn_submit: Надіслати new_password_modal: title: Встановити новий пароль form: fields: password: label: Пароль text: Користувача буде виведено з системи, і йому потрібно буде увійти знову. msg: Пароль повинен мати довжину від 8 до 32 символів. btn_cancel: Скасувати btn_submit: Надіслати edit_profile_modal: title: Редагувати профіль form: fields: display_name: label: Зображуване ім'я msg_range: Display name must be 2-30 characters in length. username: label: Ім'я користувача msg_range: Username must be 2-30 characters in length. email: label: Електронна пошта msg_invalid: Невірна адреса електронної пошти. edit_success: Успішно відредаговано btn_cancel: Скасувати btn_submit: Надіслати user_modal: title: Додати нового користувача form: fields: users: label: Масове додавання користувача placeholder: "Джон Сміт, john@example.com, BUSYopr2\nАліса, alice@example.com, fpDntV8q" text: '“Ім''я, електронну пошту, пароль” розділити комами. Один користувач у рядку.' msg: "Будь ласка, введіть електронну пошту користувача, по одній на рядок." display_name: label: Ім'я для відображення msg: Ім'я для показу повинно мати довжину від 2 до 30 символів. email: label: Електронна пошта msg: Електронна пошта недійсна. password: label: Пароль msg: Пароль повинен мати довжину від 8 до 32 символів. btn_cancel: Скасувати btn_submit: Надіслати users: title: Користувачі name: Ім’я email: Електронна пошта reputation: Репутація created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Статус role: Роль action: Дія change: Зміна all: Усі staff: Персонал more: Більше inactive: Неактивні suspended: Призупинено deleted: Видалено normal: Нормальний Moderator: Модератор Admin: Адмін User: Користувач filter: placeholder: "Фільтр на ім'я, користувач:id" set_new_password: Встановити новий пароль edit_profile: Редагувати профіль change_status: Змінити статус change_role: Змінити роль show_logs: Показати записи журналу add_user: Додати користувача deactivate_user: title: Деактивувати користувача content: Неактивний користувач повинен повторно підтвердити свою електронну адресу. delete_user: title: Видалити цього користувача content: Ви впевнені, що хочете видалити цього користувача? Це назавжди! remove: Вилучити їх вміст label: Видалити всі запитання, відповіді, коментарі тощо. text: Не позначайте цю опцію, якщо ви хочете лише видалити обліковий запис користувача. suspend_user: title: Призупинити цього користувача content: Призупинений користувач не може увійти в систему. label: How long will the user be suspended for? forever: Forever questions: page_title: Запитання unlisted: Вилучене зі списку post: Опублікувати votes: Голоси answers: Відповіді created: Створені status: Статус action: Дія change: Зміна pending: Очікування filter: placeholder: "Фільтр за назвою, питання:id" answers: page_title: Відповіді post: Допис votes: Голоси created: Створено status: Статус action: Дія change: Зміна filter: placeholder: "Фільтр за назвою, відповідь:id" general: page_title: Основне name: label: Назва сайту msg: Назва сайту не може бути порожньою. text: "Назва цього сайту як зазначено у заголовку тегу." site_url: label: URL сайту msg: Url сайту не може бути порожньою. validate: Будь ласка, введіть дійсну URL. text: Адреса вашого сайту. short_desc: label: Короткий опис сайту msg: Короткий опис сайту не може бути пустим. text: "Короткий опис, як використовується в заголовку на головній сторінці." desc: label: Опис сайту msg: Опис сайту не може бути порожнім. text: "Опишіть цей сайт одним реченням, як у тезі метаопису." contact_email: label: Контактна електронна пошта msg: Контактна електронна пошта не може бути порожньою. validate: Контактна електронна пошта недійсна. text: Адреса електронної пошти ключової особи, відповідальної за цей сайт. check_update: label: Оновлення програмного забезпечення text: Автоматично перевіряти оновлення interface: page_title: Інтерфейс language: label: Мова інтерфейсу msg: Мова інтерфейсу не може бути пустою. text: Мова інтерфейсу користувача. Зміниться, коли ви оновите сторінку. time_zone: label: Часовий пояс msg: Часовий пояс не може бути пустим. text: Виберіть місто в тому ж часовому поясі, що й ви. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: З електронної пошти msg: Поле з електронної пошти не може бути пустим. text: Адреса електронної пошти, з якої надсилаються листи. from_name: label: Від імені msg: Поле від імені не може бути пустим. text: Ім'я, з якого надсилаються електронні листи. smtp_host: label: SMTP-хост msg: SMTP хост не може бути порожнім. text: Ваш поштовий сервер. encryption: label: Шифрування msg: Поле шифрування не може бути пустим. text: Для більшості серверів SSL є рекомендованим параметром. ssl: SSL tls: TLS none: Нічого smtp_port: label: SMTP порт msg: SMTP порт має бути числом 1 ~ 65535. text: Порт на ваш поштовий сервер. smtp_username: label: Ім'я користувача SMTP msg: Ім'я користувача SMTP не може бути порожнім. smtp_password: label: Пароль SMTP msg: Пароль до SMTP не може бути порожнім. test_email_recipient: label: Тест отримувачів електронної пошти text: Вкажіть адресу електронної пошти, на яку будуть надходити тестові надсилання. msg: Тест отримувачів електронної пошти не вірний smtp_authentication: label: Увімкнути автентифікацію title: SMTP аутентифікація msg: SMTP аутентифікація не може бути порожньою. "yes": "Так" "no": "Ні" branding: page_title: Брендинг logo: label: Логотип msg: Логотип не може бути порожнім. text: Зображення логотипу у верхньому лівому кутку вашого сайту. Використовуйте широке прямокутне зображення з висотою 56 і співвідношенням сторін більше 3:1. Якщо залишити це поле порожнім, буде показано текст заголовка сайту. mobile_logo: label: Мобільний логотип text: Логотип, що використовується на мобільній версії вашого сайту. Використовуйте широке прямокутне зображення висотою 56. Якщо залишити поле порожнім, буде використано зображення з налаштування "логотип". square_icon: label: Квадратна іконка msg: Квадратна іконка не може бути пустою. text: Зображення, що використовується як основа для іконок метаданих. В ідеалі має бути більшим за 512x512. favicon: label: Favicon text: Іконка для вашого сайту. Для коректної роботи через CDN має бути у форматі png. Буде змінено розмір до 32x32. Якщо залишити порожнім, буде використовуватися "квадратна іконка". legal: page_title: Правила та умови terms_of_service: label: Умови використання text: "Ви можете додати вміст про умови використання тут. Якщо у вас уже є документ, розміщений деінде, надайте тут повну URL-адресу." privacy_policy: label: Політика конфіденційности text: "Ви можете додати вміст політики конфіденційності тут. Якщо у вас уже є документ, розміщений деінде, надайте тут повну URL-адресу." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Відповідь на запис label: Кожен користувач може написати лише одну відповідь на кожне запитання text: "Вимкнути, щоб дозволити користувачам писати кілька відповідей на одне і те ж питання, що може призвести до розфокусування відповідей." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Рекомендовані теги text: "За замовчуванням рекомендовані теги будуть показані у спадному списку." msg: contain_reserved: "рекомендовані теги не можуть містити зарезервовані теги" required_tag: title: Встановіть необхідні теги label: Встановіть “Рекомендовані теги” як необхідні теги text: "Кожне нове питання повинно мати принаймні один рекомендований тег." reserved_tags: label: Зарезервовані теги text: "Зарезервовані теги можуть використовуватися лише модератором." image_size: label: Максимальний розмір зображення (МБ) text: "Максимальний розмір вивантаженого зображення." attachment_size: label: Максимальний розмір вкладення (МБ) text: "Максимальний розмір вкладених файлів для вивантаження." image_megapixels: label: Максимальна кількість мегапікселів зображення text: "Максимальна кількість мегапікселів, дозволена для зображення." image_extensions: label: Дозволені розширення зображень text: "Список розширень файлів, дозволених для показу зображень, через кому." attachment_extensions: label: Авторизовані розширення вкладень text: "Список дозволених для вивантаження розширень файлів, розділених комами. ПОПЕРЕДЖЕННЯ: Дозвіл на вивантаження може спричинити проблеми з безпекою." seo: page_title: SEO permalink: label: Постійне посилання text: Користувацькі структури URL можуть покращити уміння та сумісність з надсиланням посилань. robots: label: robots.txt text: Це назавжди замінить будь-які відповідні налаштування сайту. themes: page_title: Теми themes: label: Теми text: Виберіть наявну тему. color_scheme: label: Схема кольорів navbar_style: label: Navbar background style primary_color: label: Основний колір text: Змінюйте кольори, що використовуються у ваших темах layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS та HTML custom_css: label: Користувацький CSS text: > head: label: Головний text: > header: label: Заголовок text: > footer: label: Низ text: Це вставить перед </body>. sidebar: label: Бічна панель text: Це буде вставлено в бічну панель. login: page_title: Увійти membership: title: Членство label: Дозволити нові реєстрації text: Вимкнути, щоб ніхто не міг створити новий обліковий запис. email_registration: title: Реєстрація за електронною поштою label: Дозволити реєстрацію за електронною поштою text: Вимкніть, щоб запобігти створенню нових облікових записів через електронну пошту. allowed_email_domains: title: Дозволені домени електронної пошти text: Домени електронної пошти, на які користувачі повинні зареєструвати облікові записи. Один домен у рядку. Ігнорується, якщо порожній. private: title: Приватний label: Вхід обов'язковий text: Доступ до цієї спільноти мають лише зареєстровані користувачі. password_login: title: Вхід через пароль label: Дозволити вхід через електронну пошту і пароль text: "ПОПЕРЕДЖЕННЯ: Якщо вимкнути, ви не зможете увійти в систему, якщо раніше не налаштували інший метод входу." installed_plugins: title: Встановлені плагіни plugin_link: Плагіни розширюють і поглиблюють функціональність. Ви можете знайти плагіни у <1>Сховищі плагінів. filter: all: Усі active: Активні inactive: Неактивні outdated: Застарілі plugins: label: Плагіни text: Виберіть наявний плагін. name: Ім’я version: Версія status: Статус action: Дія deactivate: Деактивувати activate: Активувати settings: Налаштування settings_users: title: Користувачі avatar: label: Аватар за замовчуванням text: Для користувачів без аватара власного. gravatar_base_url: label: Основна URL Gravatar text: URL бази API постачальника Gravatar. Ігнорується, якщо порожній. profile_editable: title: Профіль можна редагувати allow_update_display_name: label: Дозволити користувачам змінювати ім'я для відображення allow_update_username: label: Дозволити користувачам змінювати своє ім'я користувача allow_update_avatar: label: Дозволити користувачам змінювати зображення свого профілю allow_update_bio: label: Дозволити користувачам змінювати дані про себе allow_update_website: label: Дозволити користувачам змінювати свій вебсайт allow_update_location: label: Дозволити користувачам змінювати своє місцеперебування privilege: title: Привілеї level: label: Рівень репутації необхідний text: Виберіть репутацію, необхідну для привілеїв msg: should_be_number: введення має бути числом number_larger_1: число має бути рівним або більшим за 1 badges: action: Дія active: Активні activate: Активувати all: Усі awards: Нагороди deactivate: Деактивувати filter: placeholder: Фільтрувати за іменем, значок:id group: Група inactive: Неактивні name: Ім’я show_logs: Показати записи журналу status: Статус title: Значки apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (необов'язково) empty: не може бути порожнім invalid: недійсне btn_submit: Зберегти not_found_props: "Необхідний параметр {{ key }} не знайдено." select: Вибрати page_review: review: Огляд proposed: запропоновано question_edit: Редагування питання answer_edit: Редагування відповіді tag_edit: Редагування тегу edit_summary: Редагувати звіт edit_question: Редагувати питання edit_answer: Редагувати відповідь edit_tag: Редагувати тег empty: Не залишилось завдань огляду. approve_revision_tip: Ви схвалюєте цю редакцію? approve_flag_tip: Ви схвалюєте цю відмітку? approve_post_tip: Ви схвалюєте цей допис? approve_user_tip: Ви схвалюєте цього користувача? suggest_edits: Запропоновані зміни flag_post: Відмітити публікацію flag_user: Відмітити користувача queued_post: Черговий допис queued_user: Черговий користувач filter_label: Тип reputation: репутація flag_post_type: Відмічено цей пост як {{ type }}. flag_user_type: Відмічено цього користувача як {{ type }}. edit_post: Редагувати допис list_post: Додати допис до списку unlist_post: Видалити допис зі списку timeline: undeleted: не видалений deleted: видалений downvote: голос "проти" upvote: голос "за" accept: прийняти cancelled: скасовано commented: прокоментовано rollback: відкат назад edited: відредаговано answered: дано відповідь asked: запитано closed: закрито reopened: знову відкрито created: створено pin: закріплено unpin: відкріплено show: додано до списку hide: не внесено до списку title: "Історія для" tag_title: "Хронологія для" show_votes: "Показати голоси" n_or_a: Н/Д title_for_question: "Хронологія для" title_for_answer: "Часова шкала для відповіді на {{ title }} від {{ author }}" title_for_tag: "Часова шкала для тега" datetime: Дата й час type: Тип by: Від comment: Коментар no_data: "Ми не змогли нічого знайти." users: title: Користувачі users_with_the_most_reputation: Користувачі з найвищою репутацією на цьому тижні users_with_the_most_vote: Користувачі, які голосували за найбільше цього тижня staffs: Персонал нашої спільноти reputation: репутація votes: голоси prompt: leave_page: Ви дійсно хочете покинути сторінку? changes_not_save: Ваші зміни можуть не зберегтися. draft: discard_confirm: Ви дійсно бажаєте скасувати чернетку? messages: post_deleted: Цей допис було видалено. post_cancel_deleted: Цей допис було не видалено. post_pin: Цей допис було закріплено. post_unpin: Цей допис було відкріплено. post_hide_list: Цей допис було приховано зі списку. post_show_list: Цей допис було показано у списку. post_reopen: Цей допис було знову відкрито. post_list: Цей допис було додано до списку. post_unlist: Цей допис було приховано. post_pending: Ваш допис очікує на розгляд. Це попередній перегляд, його буде видно після того, як його буде схвалено. post_closed: Ця публікація була закрита. answer_deleted: Ця відповідь була видалена. answer_cancel_deleted: Ця відповідь була не видалена. change_user_role: Роль цього користувача було змінено. user_inactive: Цей користувач вже неактивний. user_normal: Цей користувач вже нормальний. user_suspended: Цього користувача було відсторонено. user_deleted: Цього користувача було видалено. user_added: User has been added successfully. badge_activated: Цей бейдж було активовано. badge_inactivated: Цей бейдж було деактивовано. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/vi_VN.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: Thành công. unknown: other: Lỗi không xác định. request_format_error: other: Định dạng yêu cầu không hợp lệ. unauthorized_error: other: Chưa được cấp quyền. database_error: other: Lỗi dữ liệu máy chủ. forbidden_error: other: Bị cấm. duplicate_request_error: other: Trùng lặp yêu cầu. action: report: other: Gắn nhãn edit: other: Chỉnh sửa delete: other: Xóa close: other: Đóng reopen: other: Mở lại forbidden_error: other: Bị cấm. pin: other: Ghim hide: other: Gỡ bỏ khỏi danh sách unpin: other: Bỏ ghim show: other: Hiển thị invite_someone_to_answer: other: Chỉnh sửa undelete: other: Khôi phục merge: other: Merge role: name: user: other: Người dùng admin: other: Quản trị viên moderator: other: Người điều hành description: user: other: Mặc định không có quyền truy cập đặc biệt. admin: other: Có toàn quyền truy cập vào trang. moderator: other: Có quyền truy cập vào tất cả bài viết trừ cài đặt quản trị. privilege: level_1: description: other: Cấp độ 1 (yêu cầu danh tiếng thấp cho nhóm riêng, nhóm) level_2: description: other: Cấp độ 2 (yêu cầu danh tiếng cao cho cộng đồng đã phát triển) level_3: description: other: Cấp độ 3 (yêu cầu danh tiếng cao cho cộng đồng đã phát triển) level_custom: description: other: Cấp độ tùy chỉnh rank_question_add_label: other: Đặt câu hỏi rank_answer_add_label: other: Viết câu trả lời rank_comment_add_label: other: Viết bình luận rank_report_add_label: other: Gắn Cờ rank_comment_vote_up_label: other: Bình chọn lên cho bình luận rank_link_url_limit_label: other: Đăng nhiều hơn 2 liên kết cùng một lúc rank_question_vote_up_label: other: Bình chọn lên cho câu hỏi rank_answer_vote_up_label: other: Bình chọn lên cho câu trả lời rank_question_vote_down_label: other: Bình chọn xuống cho câu hỏi rank_answer_vote_down_label: other: Bình chọn xuống cho câu trả lời rank_invite_someone_to_answer_label: other: Mời ai đó trả lời rank_tag_add_label: other: Tạo thẻ mới rank_tag_edit_label: other: Chỉnh sửa mô tả thẻ (cần xem xét) rank_question_edit_label: other: Chỉnh sửa câu hỏi của người khác (cần xem xét) rank_answer_edit_label: other: Chỉnh sửa câu trả lời của người khác (cần xem xét) rank_question_edit_without_review_label: other: Chỉnh sửa câu hỏi của người khác không cần xem xét rank_answer_edit_without_review_label: other: Chỉnh sửa câu trả lời của người khác không cần xem xét rank_question_audit_label: other: Xem xét chỉnh sửa câu hỏi rank_answer_audit_label: other: Xem xét chỉnh sửa câu trả lời rank_tag_audit_label: other: Xem xét chỉnh sửa thẻ rank_tag_edit_without_review_label: other: Chỉnh sửa mô tả thẻ không cần xem xét rank_tag_synonym_label: other: Quản lý từ đồng nghĩa của thẻ email: other: Email e_mail: other: Email password: other: Mật khẩu pass: other: Mật khẩu old_pass: other: Current password original_text: other: Bài viết này email_or_password_wrong_error: other: Email và mật khẩu không trùng khớp. error: common: invalid_url: other: URL không tồn tại. status_invalid: other: Trạng thái không hợp lệ password: space_invalid: other: Mật khẩu không thể tồn tại khoảng trắng. admin: cannot_update_their_password: other: Bạn không thể thay đổi mật khẩu. cannot_edit_their_profile: other: Bạn không thể thay đổi hồ sơ. cannot_modify_self_status: other: Bạn không thể thay đổi trạng thái của mình. email_or_password_wrong: other: Email và mật khẩu không khớp. answer: not_found: other: Không tìm thấy câu trả lời. cannot_deleted: other: Không có quyền xóa. cannot_update: other: Không có quyền cập nhật. question_closed_cannot_add: other: Câu hỏi đã đóng và không thể thêm. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: Không được phép chỉnh sửa bình luận. not_found: other: Không tìm thấy bình luận. cannot_edit_after_deadline: other: Thời gian bình luận đã quá lâu để chỉnh sửa. content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: Email đã được dùng. need_to_be_verified: other: Email cần được xác minh. verify_url_expired: other: URL xác minh email đã hết hạn, vui lòng gửi lại email. illegal_email_domain_error: other: Email không được phép từ miền email đó. Vui lòng sử dụng miền khác. lang: not_found: other: Không tìm thấy file ngôn ngữ. object: captcha_verification_failed: other: Xác minh Captcha thất bại. disallow_follow: other: Bạn không được phép theo dõi. disallow_vote: other: Bạn không được phép bỏ phiếu. disallow_vote_your_self: other: Bạn không thể bỏ phiếu cho bài đăng của chính mình. not_found: other: Đối tượng không tìm thấy. verification_failed: other: Xác thực không thành công. email_or_password_incorrect: other: Email và mật khẩu không trùng khớp. old_password_verification_failed: other: Xác minh mật khẩu cũ thất bại. new_password_same_as_previous_setting: other: Mật khẩu mới giống như cài đặt trước. already_deleted: other: Mật khẩu mới giống như cài đặt trước. meta: object_not_found: other: Đối tượng không tìm thấy question: already_deleted: other: Bài đăng này đã bị xóa. under_review: other: Bài đăng của bạn đang chờ xem xét. Nó sẽ hiển thị sau khi được phê duyệt. not_found: other: Không tìm thấy câu hỏi. cannot_deleted: other: Không có quyền xóa. cannot_close: other: Không có quyền đóng. cannot_update: other: Không có quyền cập nhật. content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Xếp hạng danh tiếng không đạt được điều kiện. vote_fail_to_meet_the_condition: other: Cảm ơn phản hồi của bạn. Bạn cần ít nhất {{.Rank}} danh tiếng để bỏ phiếu. no_enough_rank_to_operate: other: Bạn cần ít nhất {{.Rank}} danh tiếng để làm điều này. report: handle_failed: other: Xử lý báo cáo thất bại. not_found: other: Không tìm thấy báo cáo. tag: already_exist: other: Thẻ đã tồn tại. not_found: other: Không tìm thấy thẻ. recommend_tag_not_found: other: Thẻ đề xuất không tồn tại. recommend_tag_enter: other: Vui lòng nhập ít nhất một thẻ bắt buộc. not_contain_synonym_tags: other: Không nên chứa các thẻ đồng nghĩa. cannot_update: other: Không có quyền cập nhật. is_used_cannot_delete: other: Bạn không thể xóa thẻ đang được sử dụng. cannot_set_synonym_as_itself: other: Bạn không thể đặt từ đồng nghĩa của thẻ hiện tại là chính nó. minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: Tên người gửi không thể là địa chỉ email. theme: not_found: other: Chủ đề không tìm thấy. revision: review_underway: other: Không thể chỉnh sửa hiện tại, có một phiên bản đang trong hàng đợi xem xét. no_permission: other: Không có quyền sửa đổi. user: external_login_missing_user_id: other: Nền tảng bên thứ ba không cung cấp UserID duy nhất, vì vậy bạn không thể đăng nhập, vui lòng liên hệ với quản trị viên trang web. external_login_unbinding_forbidden: other: Vui lòng đặt mật khẩu đăng nhập cho tài khoản của bạn trước khi bạn gỡ bỏ đăng nhập này. email_or_password_wrong: other: other: Email và mật khẩu không khớp. not_found: other: Không tìm thấy người dùng. suspended: other: Người dùng đã bị đình chỉ. username_invalid: other: Tên người dùng không hợp lệ. username_duplicate: other: Tên người dùng đã được sử dụng. set_avatar: other: Thiết lập hình đại diện thất bại. cannot_update_your_role: other: Bạn không thể sửa đổi vai trò của mình. not_allowed_registration: other: Hiện tại trang không mở đăng ký. not_allowed_login_via_password: other: Hiện tại trang không cho phép đăng nhập qua mật khẩu. access_denied: other: Truy cập bị từ chối page_access_denied: other: Bạn không có quyền truy cập trang này. add_bulk_users_format_error: other: "Lỗi định dạng {{.Field}} gần '{{.Content}}' tại dòng {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "Số lượng người dùng bạn thêm cùng một lúc nên nằm trong khoảng từ 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: Đọc cấu hình thất bại database: connection_failed: other: Kết nối cơ sở dữ liệu thất bại create_table_failed: other: Tạo bảng thất bại install: create_config_failed: other: Không thể tạo file config.yaml. upload: unsupported_file_format: other: Định dạng tệp không được hỗ trợ. site_info: config_not_found: other: Không tìm thấy cấu hình trang. badge: object_not_found: other: Đối tượng không tìm thấy reason: spam: name: other: thư rác desc: other: Bài đăng này quảng cáo hoặc phá hoại. Nó không hữu ích hoặc liên quan đến chủ đề hiện tại. rude_or_abusive: name: other: thô lỗ hoặc lạm dụng desc: other: "Một người hợp lý sẽ thấy nội dung này không phù hợp để diễn thuyết một cách tôn trọng." a_duplicate: name: other: một bản sao desc: other: Câu hỏi này đã được hỏi trước đó, đã có câu trả lời. placeholder: other: Nhập liên kết câu hỏi hiện tại not_a_answer: name: other: không phải câu trả lời desc: other: "Điều này đã được đăng dưới dạng câu trả lời nhưng nó không cố gắng trả lời câu hỏi. Nó có thể là một bản chỉnh sửa, một nhận xét, một câu hỏi khác hoặc bị xóa hoàn toàn." no_longer_needed: name: other: không còn cần thiết desc: other: Bình luận này đã lỗi thời, đối thoại hoặc không liên quan đến bài đăng này. something: name: other: điều gì đó khác desc: other: Bài đăng này cần sự chú ý của nhân viên vì một lý do khác không được liệt kê ở trên. placeholder: other: Hãy cho chúng tôi biết cụ thể điều gì bạn quan tâm community_specific: name: other: một lý do cụ thể của cộng đồng desc: other: Câu hỏi này không đáp ứng hướng dẫn của cộng đồng. not_clarity: name: other: cần chi tiết hoặc rõ ràng desc: other: Câu hỏi này hiện bao gồm nhiều câu hỏi trong một. Nó nên tập trung vào một vấn đề duy nhất. looks_ok: name: other: trông ổn desc: other: Bài đăng này tốt như vậy và không kém chất lượng. needs_edit: name: other: cần chỉnh sửa, và tôi đã làm điều đó desc: other: Cải thiện và sửa các vấn đề với bài đăng này bằng chính bạn. needs_close: name: other: cần đóng desc: other: Một câu hỏi đã đóng không thể trả lời, nhưng vẫn có thể chỉnh sửa, bỏ phiếu và bình luận. needs_delete: name: other: cần xóa desc: other: Bài đăng này sẽ bị xóa. question: close: duplicate: name: other: spam desc: other: Câu hỏi này đã được hỏi trước đó và đã có câu trả lời. guideline: name: other: một lý do cụ thể của cộng đồng desc: other: Câu hỏi này không đáp ứng hướng dẫn của cộng đồng. multiple: name: other: cần chi tiết hoặc rõ ràng desc: other: Câu hỏi này hiện bao gồm nhiều câu hỏi trong một. Nó chỉ nên tập trung vào một vấn đề. other: name: other: điều gì đó khác desc: other: Bài đăng này cần một lý do khác không được liệt kê ở trên. operation_type: asked: other: đã hỏi answered: other: đã trả lời modified: other: đã chỉnh sửa deleted_title: other: Câu hỏi đã xóa questions_title: other: Các câu hỏi tag: tags_title: other: Thẻ no_description: other: Thẻ không có mô tả. notification: action: update_question: other: câu hỏi đã cập nhật answer_the_question: other: đã trả lời câu hỏi update_answer: other: câu trả lời đã cập nhật accept_answer: other: câu trả lời đã chấp nhận comment_question: other: đã bình luận câu hỏi comment_answer: other: đã bình luận câu trả lời reply_to_you: other: đã trả lời bạn mention_you: other: đã nhắc đến bạn your_question_is_closed: other: Câu hỏi của bạn đã được đóng your_question_was_deleted: other: Câu hỏi của bạn đã bị xóa your_answer_was_deleted: other: Câu trả lời của bạn đã bị xóa your_comment_was_deleted: other: Bình luận của bạn đã bị xóa up_voted_question: other: câu hỏi đã bình chọn lên down_voted_question: other: câu hỏi đã bình chọn xuống up_voted_answer: other: câu trả lời đã bình chọn lên down_voted_answer: other: câu trả lời đã bình chọn xuống up_voted_comment: other: bình luận đã bình chọn lên invited_you_to_answer: other: đã mời bạn trả lời earned_badge: other: Bạn đã nhận được huy hiệu "{{.BadgeName}}" email_tpl: change_email: title: other: "[{{.SiteName}}] Xác nhận địa chỉ email mới của bạn" body: other: "Xác nhận địa chỉ email mới của bạn cho {{.SiteName}} bằng cách nhấp vào liên kết sau:
\n{{.ChangeEmailUrl}}

\n\nNếu bạn không yêu cầu thay đổi này, vui lòng bỏ qua email này.

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} đã trả lời câu hỏi của bạn" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nXem trên {{.SiteName}}

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời thư này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.

\n\nHủy đăng ký" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} mời bạn trả lời" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Tôi nghĩ bạn có thể biết câu trả lời.

\nXem trên {{.SiteName}}

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời thư này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.

\n\nHủy đăng ký" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} đã bình luận về bài đăng của bạn" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nXem trên {{.SiteName}}

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời thư này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.

\n\nHủy đăng ký" new_question: title: other: "[{{.SiteName}}] Câu hỏi mới: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName}}] Đặt lại mật khẩu" body: other: "Ai đó đã yêu cầu đặt lại mật khẩu của bạn trên {{.SiteName}}.

\n\nNếu người đó không phải là bạn thì bạn có thể yên tâm bỏ qua email này.

\n\nNhấp vào liên kết sau để chọn mật khẩu mới:
\n{{.PassResetUrl}}\n

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn." register: title: other: "[{{.SiteName}}] Xác nhận tài khoản mới của bạn" body: other: "Chào mừng bạn đến với {{.SiteName}}!

\n\nNhấp vào liên kết sau để xác nhận và kích hoạt tài khoản mới của bạn:
\n{{.RegisterUrl}}

\n\nNếu liên kết trên không nhấp vào được, hãy thử sao chép và dán nó vào thanh địa chỉ trình duyệt web của bạn.\n

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn." test: title: other: "[{{.SiteName}}] Email kiểm tra" body: other: "Đây là một email thử nghiệm.\n

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn." action_activity_type: upvote: other: bình chọn lên upvoted: other: đã bình chọn lên downvote: other: bình chọn xuống downvoted: other: đã bình chọn xuống accept: other: chấp nhận accepted: other: đã chấp nhận edit: other: chỉnh sửa review: queued_post: other: Bài đăng trong hàng đợi flagged_post: other: Bài đăng được đánh dấu suggested_post_edit: other: Đề xuất chỉnh sửa reaction: tooltip: other: "{{ .Names }} và {{ .Count }} thêm..." badge: default_badges: autobiographer: name: other: Tác giả tự truyện desc: other: Đã điền thông tin hồ sơ. certified: name: other: Đã xác minh desc: other: Hoàn thành hướng dẫn cho người dùng mới của chúng tôi. editor: name: other: Trình chỉnh sửa desc: other: Chỉnh sửa bài đăng đầu tiên. first_flag: name: other: Cờ đầu tiên desc: other: Lần đầu tiên báo cáo một bài viết. first_upvote: name: other: Lượt thích đầu tiên desc: other: Lần đầu tiên báo cáo một bài viết. first_link: name: other: Liên kết đầu tiên desc: other: First added a link to another post. first_reaction: name: other: Phản ứng đầu tiên desc: other: Phản ứng với bài viết đầu tiên. first_share: name: other: Chia sẻ đầu tiên desc: other: Lần đầu chia sẻ một bài viết. scholar: name: other: Học giả desc: other: Đặt một câu hỏi và chấp nhận một câu trả lời. commentator: name: other: Bình luận viên desc: other: Để lại 5 bình luận. new_user_of_the_month: name: other: Người dùng mới của tháng desc: other: Đóng góp nổi bật trong tháng đầu tiên của họ. read_guidelines: name: other: Đọc hướng dẫn desc: other: Đọc [nguyên tắc cộng đồng]. reader: name: other: Người đọc desc: other: Đọc mọi câu trả lời trong một chủ đề có hơn 10 câu trả lời. welcome: name: other: Xin chào desc: other: Đã nhận được phiếu tán thành. nice_share: name: other: Chia sẻ hay desc: other: Đã chia sẻ một bài đăng với 25 khách truy cập. good_share: name: other: Chia sẻ tốt desc: other: Đã chia sẻ một bài đăng với 300 khách truy cập. great_share: name: other: Chia sẻ tuyệt vời desc: other: Đã chia sẻ một bài đăng với 1000 khách truy cập. out_of_love: name: other: Hết yêu thích desc: other: Đã sử dụng 50 phiếu bầu trong một ngày. higher_love: name: other: Thích cao hơn desc: other: Đã sử dụng 50 phiếu bầu trong một ngày. crazy_in_love: name: other: Thích điên cuồng desc: other: Đã sử dụng 50 phiếu bầu trong một ngày 20 lần. promoter: name: other: Người quảng bá desc: other: Đã mời một người dùng. campaigner: name: other: Chiến dịch desc: other: Đã mời 3 người dùng cơ bản. champion: name: other: Vô địch desc: other: Mời 5 thành viên. thank_you: name: other: Cảm ơn bạn desc: other: Có 20 bài đăng được bình chọn đưa ra 10 phiếu bầu. gives_back: name: other: Trả lại desc: other: Có 100 bài đăng được bình chọn và đưa ra 100 phiếu bầu. empathetic: name: other: Đồng cảm desc: other: Có 500 bài đăng được bình chọn đưa ra 1000 phiếu bầu. enthusiast: name: other: Người nhiệt thành desc: other: Đã truy cập 10 ngày liên tiếp. aficionado: name: other: Người hâm mộ desc: other: Đã truy cập 100 ngày liên tiếp. devotee: name: other: Tín đồ desc: other: Đã truy cập 365 ngày liên tiếp. anniversary: name: other: Kỉ niệm desc: other: Thành viên tích cực trong một năm, đăng ít nhất một lần. appreciated: name: other: Đánh giá cao desc: other: Nhận được 1 lượt bình chọn cho 20 bài viết. respected: name: other: Tôn trọng desc: other: Nhận được 2 lượt bình chọn cho 100 bài viết. admired: name: other: Ngưỡng mộ desc: other: Nhận được 5 lượt bình chọn trên 300 bài đăng. solved: name: other: Đã giải quyết desc: other: Có một câu trả lời được chấp nhận. guidance_counsellor: name: other: Cố vấn hướng dẫn desc: other: Có 10 câu trả lời được chấp nhận. know_it_all: name: other: Biết tất cả desc: other: Có 50 câu trả lời được chấp nhận. solution_institution: name: other: Viện giải pháp desc: other: Có 150 câu trả lời được chấp nhận. nice_answer: name: other: Câu trả lời tốt desc: other: Điểm trả lời từ 10 trở lên. good_answer: name: other: Câu trả lời của bạn desc: other: Điểm trả lời từ 25 trở lên. great_answer: name: other: Câu trả lời tuyệt vời desc: other: Điểm trả lời từ 50 trở lên. nice_question: name: other: Câu trả lời tốt desc: other: Điểm trả lời từ 10 trở lên. good_question: name: other: Câu trả lời tốt desc: other: Điểm trả lời từ 25 trở lên. great_question: name: other: Câu trả lời tốt desc: other: Điểm trả lời từ 50 trở lên. popular_question: name: other: Câu hỏi phổ biến desc: other: Câu hỏi với 500 lượt xem. notable_question: name: other: Câu hỏi đáng chú ý desc: other: Câu hỏi với 1.000 lượt xem. famous_question: name: other: Câu hỏi nổi tiếng desc: other: Câu hỏi với 5.000 lượt xem. popular_link: name: other: Liên kết phổ biến desc: other: Đã đăng một liên kết bên ngoài với 50 lần nhấp chuột. hot_link: name: other: Liên kết nổi bật desc: other: Đã đăng một liên kết bên ngoài với 300 lần nhấp chuột. famous_link: name: other: Liên kết nổi tiếng desc: other: Đã đăng một liên kết bên ngoài với 100 lần nhấp chuột. default_badge_groups: getting_started: name: other: Bắt đầu community: name: other: Cộng đồng posting: name: other: Viết bài thảo luận # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: Cách định dạng desc: >-
  • đề cập đến bài đăng: #post_id

  • để tạo liên kết

    <https://url.com>

    [Title](https://url.com)
  • đặt trả về giữa đoạn văn

  • _italic_ hoặc **in đậm**

  • mã thụt lề 4 dấu cách

  • trích dẫn bằng cách đặt > ở đầu dòng

  • backtick thoát `like _this_`

  • tạo hàng rào mã bằng dấu backticks `

    ```
    mã vào đây
    ```
pagination: prev: Trước next: Tiếp page_title: question: Câu hỏi questions: Các câu hỏi tag: Thẻ tags: Các thẻ tag_wiki: wiki thẻ create_tag: Tạo thẻ edit_tag: Chỉnh sửa thẻ ask_a_question: Create Question edit_question: Chỉnh sửa câu hỏi edit_answer: Chỉnh sửa câu search: Tìm kiếm posts_containing: Bài đăng chứa settings: Cài đặt notifications: Các thông báo login: Đăng nhập sign_up: Đăng ký account_recovery: Khôi phục tài khoản account_activation: Kích hoạt tài khoản confirm_email: Xác nhận Email account_suspended: Tài khoản bị đình chỉ admin: Quản trị change_email: Thay đổi Email install: Cài đặt Answer upgrade: Nâng cấp Answer maintenance: Bảo trì trang web users: Người dùng oauth_callback: Đang xử lý http_404: Lỗi HTTP 404 http_50X: Lỗi HTTP 500 http_403: Lỗi HTTP 403 logout: Đăng xuất posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: Các thông báo inbox: Hộp thư đến achievement: Thành tích new_alerts: Cảnh báo mới all_read: Đánh dấu tất cả đã đọc show_more: Xem thêm someone: Ai đó inbox_type: all: Tất cả posts: Bài đăng invites: Lời mời votes: Bình chọn answer: Câu trả lời question: Câu hỏi badge_award: Huy hiệu suspended: title: Tài khoản của bạn đã bị đình chỉ until_time: "Tài khoản của bạn đã bị đình chỉ cho đến {{ time }}." forever: Người dùng này đã bị đình chỉ vĩnh viễn. end: Bạn không tuân thủ hướng dẫn cộng đồng. contact_us: Liên hệ với chúng tôi editor: blockquote: text: Trích dẫn bold: text: Đậm chart: text: Biểu đồ flow_chart: Biểu đồ luồng sequence_diagram: Sơ đồ trình tự class_diagram: Sơ đồ lớp state_diagram: Sơ đồ trạng thái entity_relationship_diagram: Sơ đồ quan hệ thực thể user_defined_diagram: Sơ đồ do người dùng định nghĩa gantt_chart: Biểu đồ Gantt pie_chart: Biểu đồ tròn code: text: Mẫu code add_code: Thêm code mẫu form: fields: code: label: Mã msg: empty: Mã không thể trống. language: label: Ngôn ngữ placeholder: Phát hiện tự động btn_cancel: Hủy btn_confirm: Thêm formula: text: Công thức options: inline: Công thức nội dòng block: Công thức khối heading: text: Tiêu đề options: h1: Tiêu đề 1 h2: Tiêu đề 2 h3: Tiêu đề 3 h4: Tiêu đề 4 h5: Tiêu đề 5 h6: Tiêu đề 6 help: text: Trợ giúp hr: text: Thước ngang image: text: Hình ảnh add_image: Thêm hình ảnh tab_image: Tải Ảnh lên form_image: fields: file: label: Tệp hình ảnh btn: Chọn hình ảnh msg: empty: Tệp không thể trống. only_image: Chỉ cho phép tệp hình ảnh. max_size: Kích thước tệp không được vượt quá {{size}} MB. desc: label: Mô tả tab_url: URL hình ảnh form_url: fields: url: label: URL hình ảnh msg: empty: URL hình ảnh không thể trống. name: label: Mô tả btn_cancel: Hủy btn_confirm: Thêm uploading: Đang tải lên indent: text: Canh lề outdent: text: Lùi lề italic: text: Nhấn mạnh link: text: Liên kết add_link: Thêm liên kết form: fields: url: label: Đường link url msg: empty: URL không thể trống. name: label: Mô tả btn_cancel: Hủy btn_confirm: Thêm ordered_list: text: Danh sách đánh số unordered_list: text: Danh sách gạch đầu dòng table: text: Bảng heading: Tiêu đề cell: Ô file: text: Đính kèm tập tin not_supported: "Không hỗ trợ loại tệp đó. Hãy thử lại với {{file_type}}." max_size: "Kích thước tệp đính kèm không được vượt quá {{size}} MB." close_modal: title: Tôi đang đóng bài đăng này với lý do... btn_cancel: Hủy btn_submit: Gửi remark: empty: Không thể trống. msg: empty: Vui lòng chọn một lý do. report_modal: flag_title: Tôi đang đánh dấu để báo cáo bài đăng này với lý do... close_title: Tôi đang đóng bài đăng này với lý do... review_question_title: Xem xét câu hỏi review_answer_title: Xem xét câu trả lời review_comment_title: Xem xét bình luận btn_cancel: Hủy btn_submit: Gửi remark: empty: Không thể trống. msg: empty: Vui lòng chọn một lý do. not_a_url: Định dạng URL không chính xác. url_not_match: Nguồn gốc URL không khớp với trang web hiện tại. tag_modal: title: Tạo thẻ mới form: fields: display_name: label: Tên hiển thị msg: empty: Tên hiển thị không thể trống. range: Tên hiển thị tối đa 35 ký tự. slug_name: label: Đường dẫn URL desc: Đường dẫn tối đa 35 ký tự. msg: empty: Đường dẫn URL không thể trống. range: Đường dẫn URL tối đa 35 ký tự. character: Đường dẫn URL chứa bộ ký tự không được phép. desc: label: Mô tả revision: label: Sửa đổi edit_summary: label: Tóm tắt chỉnh sửa placeholder: >- Giải thích ngắn gọn các thay đổi của bạn (sửa chính tả, sửa ngữ pháp, cải thiện định dạng) btn_cancel: Hủy btn_submit: Gửi btn_post: Đăng thẻ mới tag_info: created_at: Đã tạo edited_at: Đã chỉnh sửa history: Lịch sử synonyms: title: Từ đồng nghĩa text: Các thẻ sau sẽ được ánh xạ lại thành empty: Không tìm thấy từ đồng nghĩa. btn_add: Thêm từ đồng nghĩa btn_edit: Chỉnh sửa btn_save: Lưu synonyms_text: Các thẻ sau sẽ được ánh xạ lại thành delete: title: Xóa thẻ này tip_with_posts: >-

Chúng tôi không cho phép xóa thẻ có bài đăng.

Vui lòng xóa thẻ này khỏi các bài đăng trước.

tip_with_synonyms: >-

Chúng tôi không cho phép xóa thẻ có từ đồng nghĩa.

Vui lòng xóa các từ đồng nghĩa khỏi thẻ này trước.

tip: Bạn có chắc chắn muốn xóa không? close: Đóng merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: Submit btn_close: Close edit_tag: title: Chỉnh sửa Thẻ default_reason: Chỉnh sửa thẻ default_first_reason: Thêm thẻ btn_save_edits: Lưu chỉnh sửa btn_cancel: Hủy dates: long_date: MMM D long_date_with_year: "MMM D, YYYY" long_date_with_time: "MMM D, YYYY [at] HH:mm" now: bây giờ x_seconds_ago: "{{count}}giây trước" x_minutes_ago: "{{count}}phút trước" x_hours_ago: "{{count}}giờ trước" hour: giờ day: ngày hours: giờ days: ngày month: month months: months year: year reaction: heart: trái tim smile: nụ cười frown: nhăn mặt btn_label: thêm hoặc loại bỏ phản ứng undo_emoji: bỏ dấu {{ emoji }} phản ứng react_emoji: biểu cảm với {{ emoji }} unreact_emoji: hủy biểu cảm {{ emoji }} comment: btn_add_comment: Thêm bình luận reply_to: Trả lời cho btn_reply: Trả lời btn_edit: Chỉnh sửa btn_delete: Xóa btn_flag: Gắn Cờ btn_save_edits: Lưu chỉnh sửa btn_cancel: Hủy show_more: "{{count}} bình luận khác" tip_question: >- Sử dụng bình luận để yêu cầu thêm thông tin hoặc đề xuất cải tiến. Tránh trả lời câu hỏi trong bình luận. tip_answer: >- Sử dụng bình luận để trả lời cho người dùng khác hoặc thông báo cho họ về các thay đổi. Nếu bạn đang thêm thông tin mới, hãy chỉnh sửa bài đăng của mình thay vì bình luận. tip_vote: Nó thêm điều gì đó hữu ích cho bài đăng edit_answer: title: Chỉnh sửa Câu trả lời default_reason: Chỉnh sửa câu trả lời default_first_reason: Thêm câu trả lời form: fields: revision: label: Sửa đổi answer: label: Câu trả lời feedback: characters: nội dung phải có ít nhất 6 ký tự. edit_summary: label: Tóm tắt chỉnh sửa placeholder: >- Giải thích ngắn gọn các thay đổi của bạn (sửa chính tả, sửa ngữ pháp, cải thiện định dạng) btn_save_edits: Lưu chỉnh sửa btn_cancel: Hủy tags: title: Thẻ sort_buttons: popular: Phổ biến name: Tên newest: Mới nhất button_follow: Theo dõi button_following: Đang theo dõi tag_label: câu hỏi search_placeholder: Lọc theo tên thẻ no_desc: Thẻ không có mô tả. more: Thêm wiki: Wiki ask: title: Create Question edit_title: Chỉnh sửa Câu hỏi default_reason: Chỉnh sửa câu hỏi default_first_reason: Create question similar_questions: Câu hỏi tương tự form: fields: revision: label: Sửa đổi title: label: Tiêu đề placeholder: What's your topic? Be specific. msg: empty: Tiêu đề không thể trống. range: Tiêu đề tối đa 150 ký tự body: label: Nội dung msg: empty: Nội dung không thể trống. hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: Thẻ msg: empty: Thẻ không thể trống. answer: label: Câu trả lời msg: empty: Câu trả lời không thể trống. edit_summary: label: Tóm tắt chỉnh sửa placeholder: >- Giải thích ngắn gọn các thay đổi của bạn (sửa chính tả, sửa ngữ pháp, cải thiện định dạng) btn_post_question: Đăng câu hỏi của bạn btn_save_edits: Lưu chỉnh sửa answer_question: Trả lời câu hỏi của chính bạn post_question&answer: Đăng câu hỏi và câu trả lời của bạn tag_selector: add_btn: Thêm thẻ create_btn: Tạo thẻ mới search_tag: Tìm kiếm thẻ hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: Không có thẻ phù hợp tag_required_text: Thẻ bắt buộc (ít nhất một) header: nav: question: Câu hỏi tag: Thẻ user: Người dùng badges: Danh hiệu profile: Hồ sơ setting: Cài đặt logout: Đăng xuất admin: Quản trị review: Xem xét bookmark: Đánh dấu moderation: Điều hành search: placeholder: Tìm kiếm footer: build_on: Powered by <1> Apache Answer upload_img: name: Thay đổi loading: đang tải... pic_auth_code: title: Mã xác minh placeholder: Nhập văn bản ở trên msg: empty: Captcha không thể trống. inactive: first: >- Bạn gần như đã hoàn tất! Chúng tôi đã gửi một email kích hoạt đến {{mail}}. Vui lòng làm theo hướng dẫn trong email để kích hoạt tài khoản của bạn. info: "Nếu không nhận được, hãy kiểm tra thư mục spam của bạn." another: >- Chúng tôi đã gửi một email kích hoạt khác cho bạn tại {{mail}}. Có thể mất vài phút để nó đến; hãy chắc chắn kiểm tra thư mục thư rác của bạn. btn_name: Gửi lại email kích hoạt change_btn_name: Thay đổi email msg: empty: Không thể để trống mục này. resend_email: url_label: Bạn có chắc chắn muốn gửi lại email kích hoạt không? url_text: Bạn cũng có thể cung cấp liên kết kích hoạt ở trên cho người dùng. login: login_to_continue: Đăng nhập để tiếp tục info_sign: Bạn không có tài khoản? <1>Đăng ký info_login: Bạn đã có tài khoản? <1>Đăng nhập agreements: Bằng cách đăng ký, bạn đồng ý với <1>chính sách bảo mật và <3>điều khoản dịch vụ. forgot_pass: Quên mật khẩu? name: label: Tên msg: empty: Tên không thể trống. range: Tên phải có độ dài từ 2 đến 30 ký tự. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: Email msg: empty: Email không thể trống. password: label: Mật khẩu msg: empty: Mật khẩu không thể trống. different: Mật khẩu nhập vào ở hai bên không nhất quán account_forgot: page_title: Quên mật khẩu btn_name: Gửi email khôi phục cho tôi send_success: >- Nếu một tài khoản khớp với {{mail}}, bạn sẽ sớm nhận được một email với hướng dẫn về cách đặt lại mật khẩu của mình. email: label: Email msg: empty: Email không thể trống. change_email: btn_cancel: Hủy btn_update: Cập nhật địa chỉ email send_success: >- Nếu một tài khoản khớp với {{mail}}, bạn sẽ sớm nhận được một email với hướng dẫn về cách đặt lại mật khẩu của mình. email: label: Email mới msg: empty: Email không thể trống. oauth: connect: Kết nối với {{ auth_name }} remove: Xóa bỏ {{ auth_name }} oauth_bind_email: subtitle: Thêm email khôi phục vào tài khoản của bạn. btn_update: Cập nhật địa chỉ email email: label: Email msg: empty: Email không thể trống. modal_title: Email đã tồn tại. modal_content: Địa chỉ email này đã được đăng ký. Bạn có chắc chắn muốn kết nối với tài khoản hiện tại không? modal_cancel: Thay đổi email modal_confirm: Kết nối với tài khoản hiện tại password_reset: page_title: Đặt lại mật khẩu btn_name: Đặt lại mật khẩu của tôi reset_success: >- Bạn đã thay đổi mật khẩu thành công; bạn sẽ được chuyển hướng đến trang đăng nhập. link_invalid: >- Xin lỗi, liên kết đặt lại mật khẩu này không còn hợp lệ. Có thể mật khẩu của bạn đã được đặt lại? to_login: Tiếp tục đến trang đăng nhập password: label: Mật khẩu msg: empty: Mật khẩu không thể trống. length: Độ dài cần nằm trong khoảng từ 8 đến 32 different: Mật khẩu nhập vào ở hai bên không nhất quán password_confirm: label: Xác nhận mật khẩu mới settings: page_title: Cài đặt goto_modify: Đi đến sửa đổi nav: profile: Hồ sơ notification: Thông báo account: Tài khoản interface: Giao diện profile: heading: Hồ sơ btn_name: Lưu display_name: label: Tên hiển thị msg: Tên hiển thị không thể trống. msg_range: Display name must be 2-30 characters in length. username: label: Tên người dùng caption: Mọi người có thể nhắc đến bạn với "@username". msg: Tên người dùng không thể trống. msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Hình ảnh hồ sơ gravatar: Gravatar gravatar_text: Bạn có thể thay đổi hình ảnh trên custom: Tùy chỉnh custom_text: Bạn có thể tải lên hình ảnh của mình. default: Hệ thống msg: Vui lòng tải lên một hình đại diện bio: label: Giới thiệu về tôi website: label: Website placeholder: "https://example.com" msg: Định dạng website không chính xác location: label: Địa điểm placeholder: "Thành phố, Quốc gia" notification: heading: Thông báo qua Email turn_on: Bật inbox: label: Thông báo hộp thư đến description: Các câu trả lời cho câu hỏi của bạn, bình luận, lời mời và nhiều hơn nữa. all_new_question: label: Tất cả câu hỏi mới description: Nhận thông báo về tất cả các câu hỏi mới. Tối đa 50 câu hỏi mỗi tuần. all_new_question_for_following_tags: label: Tất cả câu hỏi mới cho các thẻ theo dõi description: Nhận thông báo về các câu hỏi mới cho các thẻ đang theo dõi. account: heading: Tài khoản change_email_btn: Thay đổi email change_pass_btn: Thay đổi mật khẩu change_email_info: >- Chúng tôi đã gửi một email đến địa chỉ đó. Vui lòng làm theo hướng dẫn xác nhận. email: label: Email new_email: label: Email mới msg: Email mới không được để trống. pass: label: Mật khẩu hiện tại msg: Mật khẩu không thể trống. password_title: Mật khẩu current_pass: label: Mật khẩu hiện tại msg: empty: Mật khẩu hiện tại không thể trống. length: Độ dài cần nằm trong khoảng từ 8 đến 32. different: Hai mật khẩu nhập vào không khớp. new_pass: label: Mật khẩu mới pass_confirm: label: Xác nhận mật khẩu mới interface: heading: Giao diện lang: label: Ngôn ngữ giao diện text: Ngôn ngữ giao diện người dùng. Nó sẽ thay đổi khi bạn làm mới trang. my_logins: title: Đăng nhập của tôi label: Đăng nhập hoặc đăng ký trên trang này bằng các tài khoản này. modal_title: Xóa đăng nhập modal_content: Bạn có chắc chắn muốn xóa đăng nhập này khỏi tài khoản của bạn không? modal_confirm_btn: Xóa remove_success: Đã xóa thành công toast: update: cập nhật thành công update_password: Mật khẩu đã được thay đổi thành công. flag_success: Cảm ơn bạn đã đánh dấu. forbidden_operate_self: Không được phép thao tác trên chính mình review: Sửa đổi của bạn sẽ được hiển thị sau khi được xem xét. sent_success: Đã gửi thành công related_question: title: Related answers: câu trả lời linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: Mời mọi người desc: Mời những người bạn nghĩ có thể trả lời. invite: Mời trả lời add: Thêm người search: Tìm kiếm người question_detail: action: Hành động created: Created Asked: Đã hỏi asked: đã hỏi update: Đã chỉnh sửa Edited: Edited edit: đã chỉnh sửa commented: đã bình luận Views: Lượt xem Follow: Theo dõi Following: Đang theo dõi follow_tip: Theo dõi câu hỏi này để nhận thông báo answered: đã trả lời closed_in: Đóng trong show_exist: Hiển thị câu hỏi hiện tại. useful: Hữu ích question_useful: Nó hữu ích và rõ ràng question_un_useful: Nó không rõ ràng hoặc không hữu ích question_bookmark: Đánh dấu câu hỏi này answer_useful: Nó hữu ích answer_un_useful: Nó không hữu ích answers: title: Các câu trả lời score: Điểm newest: Mới nhất oldest: Cũ nhất btn_accept: Chấp nhận btn_accepted: Đã chấp nhận write_answer: title: Câu trả lời của bạn edit_answer: Chỉnh sửa câu trả lời hiện tại của tôi btn_name: Đăng câu trả lời của bạn add_another_answer: Thêm câu trả lời khác confirm_title: Tiếp tục trả lời continue: Tiếp tục confirm_info: >-

Bạn có chắc chắn muốn thêm một câu trả lời khác không?

Bạn có thể sử dụng liên kết chỉnh sửa để tinh chỉnh và cải thiện câu trả lời hiện tại của mình, thay vì.

empty: Câu trả lời không thể trống. characters: nội dung phải có ít nhất 6 ký tự. tips: header_1: Cảm ơn câu trả lời của bạn li1_1: Vui lòng chắc chắn trả lời câu hỏi. Cung cấp chi tiết và chia sẻ nghiên cứu của bạn. li1_2: Hỗ trợ bất kỳ tuyên bố nào bạn đưa ra với tài liệu tham khảo hoặc kinh nghiệm cá nhân. header_2: Nhưng tránh ... li2_1: Yêu cầu trợ giúp, yêu cầu làm rõ, hoặc trả lời cho các câu trả lời khác. reopen: confirm_btn: Mở lại title: Mở lại bài đăng này content: Bạn có chắc chắn muốn mở lại không? list: confirm_btn: Danh sách title: Danh sách bài đăng này content: Bạn có chắc chắn muốn liệt kê không? unlist: confirm_btn: Gỡ bỏ khỏi danh sách title: Gỡ bỏ bài đăng này khỏi danh sách content: Bạn có chắc chắn muốn gỡ bỏ không? pin: title: Ghim bài đăng này content: Bạn có chắc chắn muốn ghim toàn cầu không? Bài đăng này sẽ xuất hiện ở đầu tất cả các danh sách bài đăng. confirm_btn: Ghim delete: title: Xóa bài đăng này question: >- Chúng tôi không khuyến khích xóa câu hỏi có câu trả lời vì làm như vậy sẽ tước đoạt kiến thức của độc giả trong tương lai.

Việc xóa liên tục các câu hỏi đã được trả lời có thể dẫn đến việc tài khoản của bạn bị chặn không được phép hỏi. Bạn có chắc chắn muốn xóa không? answer_accepted: >-

Chúng tôi không khuyến khích xóa câu trả lời đã được chấp nhận vì làm như vậy sẽ tước đoạt kiến thức của độc giả trong tương lai.

Việc xóa liên tục các câu trả lời đã được chấp nhận có thể dẫn đến việc tài khoản của bạn bị chặn không được phép trả lời. Bạn có chắc chắn muốn xóa không? other: Bạn có chắc chắn muốn xóa không? tip_answer_deleted: Câu trả lời này đã bị xóa undelete_title: Khôi phục bài đăng này undelete_desc: Bạn có chắc chắn muốn khôi phục không? btns: confirm: Xác nhận cancel: Hủy edit: Chỉnh sửa save: Lưu delete: Xóa undelete: Khôi phục list: Danh sách unlist: Gỡ bỏ khỏi danh sách unlisted: Không được liệt kê login: Đăng nhập signup: Đăng ký logout: Đăng xuất verify: Xác minh create: Create approve: Phê duyệt reject: Từ chối skip: Bỏ qua discard_draft: Hủy bản nháp pinned: Đã ghim all: Tất cả question: Câu hỏi answer: Câu trả lời comment: Bình luận refresh: Làm mới resend: Gửi lại deactivate: Ngừng kích hoạt active: Hoạt động suspend: Tạm ngừng unsuspend: Bỏ vô hiệu hóa close: Đóng reopen: Mở lại ok: Đồng ý light: Phông nền sáng dark: Tối system_setting: Cài đặt hệ thống default: Mặc định reset: Đặt lại tag: Thẻ post_lowercase: bài đăng filter: Lọc ignore: Bỏ qua submit: Gửi normal: Bình thường closed: Đã đóng deleted: Đã xóa deleted_permanently: Deleted permanently pending: Đang chờ xử lý more: Thêm view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: Kết quả tìm kiếm keywords: Từ khóa options: Tùy chọn follow: Theo dõi following: Đang theo dõi counts: "{{count}} Kết quả" counts_loading: "... Results" more: Thêm sort_btns: relevance: Liên quan newest: Mới nhất active: Hoạt động score: Điểm more: Thêm tips: title: Mẹo tìm kiếm nâng cao tag: "<1>[tag] tìm kiếm trong một thẻ" user: "<1>user:username tìm kiếm theo tác giả" answer: "<1>answers:0 câu hỏi chưa có câu trả lời" score: "<1>score:3 bài đăng có điểm 3+" question: "<1>is:question tìm kiếm câu hỏi" is_answer: "<1>is:answer tìm kiếm câu trả lời" empty: Chúng tôi không thể tìm thấy bất cứ thứ gì.
Thử các từ khóa khác hoặc ít cụ thể hơn. share: name: Chia sẻ copy: Sao chép liên kết via: Chia sẻ bài đăng qua... copied: Đã sao chép facebook: Chia sẻ lên Facebook twitter: Share to X cannot_vote_for_self: Bạn không thể bỏ phiếu cho bài đăng của chính mình. modal_confirm: title: Lỗi... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: Tài khoản mới của bạn đã được xác nhận; bạn sẽ được chuyển hướng đến trang chủ. link: Tiếp tục đến trang chủ oops: Rất tiếc! invalid: Liên kết bạn đã dùng không còn hoạt động nữa. confirm_new_email: Email của bạn đã được cập nhật. confirm_new_email_invalid: >- Xin lỗi, liên kết xác nhận này không còn hợp lệ. Có thể email của bạn đã được thay đổi? unsubscribe: page_title: Hủy đăng ký success_title: Hủy đăng ký thành công success_desc: Bạn đã được gỡ bỏ khỏi danh sách người đăng ký này và sẽ không nhận được thêm email từ chúng tôi. link: Thay đổi cài đặt question: following_tags: Thẻ đang theo dõi edit: Chỉnh sửa save: Lưu follow_tag_tip: Theo dõi các thẻ để tùy chỉnh danh sách câu hỏi của bạn. hot_questions: Câu hỏi nổi bật all_questions: Tất cả câu hỏi x_questions: "{{ count }} Câu hỏi" x_answers: "{{ count }} câu trả lời" x_posts: "{{ count }} Posts" questions: Câu hỏi answers: Câu trả lời newest: Mới nhất active: Hoạt động hot: Được nhiều quan tâm frequent: Thường xuyên recommend: Đề xuất score: Điểm unanswered: Chưa được trả lời modified: đã chỉnh sửa answered: đã trả lời asked: đã hỏi closed: đã đóng follow_a_tag: Theo dõi một thẻ more: Thêm personal: overview: Tổng quan answers: Câu trả lời answer: câu trả lời questions: Câu hỏi question: câu hỏi bookmarks: Đánh dấu reputation: Danh tiếng comments: Bình luận votes: Bình chọn badges: Danh hiệu newest: Mới nhất score: Điểm edit_profile: Chỉnh sửa hồ sơ visited_x_days: "Đã truy cập {{ count }} ngày" viewed: Đã xem joined: Tham gia comma: "," last_login: Đã xem about_me: Về tôi about_me_empty: "// Xin chào, Thế giới !" top_answers: Câu trả lời hàng đầu top_questions: Câu hỏi hàng đầu stats: Thống kê list_empty: Không tìm thấy bài đăng.
Có thể bạn muốn chọn một thẻ khác? content_empty: Không tìm thấy bài viết nào. accepted: Đã chấp nhận answered: đã trả lời asked: đã hỏi downvoted: đã bỏ phiếu xuống mod_short: MOD mod_long: Người điều hành x_reputation: danh tiếng x_votes: phiếu bầu nhận được x_answers: câu trả lời x_questions: câu hỏi recent_badges: Huy hiệu gần đây install: title: Cài đặt next: Tiếp theo done: Hoàn thành config_yaml_error: Không thể tạo file config.yaml. lang: label: Vui lòng chọn một ngôn ngữ db_type: label: Hệ quản trị cơ sở dữ liệu db_username: label: Tên người dùng placeholder: root msg: Tên người dùng không thể trống. db_password: label: Mật khẩu placeholder: root msg: Mật khẩu không thể trống. db_host: label: Máy chủ cơ sở dữ liệu placeholder: "db:3306" msg: Máy chủ cơ sở dữ liệu không thể trống. db_name: label: Tên cơ sở dữ liệu placeholder: câu trả lời msg: Tên cơ sở dữ liệu không thể trống. db_file: label: Tệp tin Database placeholder: /data/answer.db msg: Tệp cơ sở dữ liệu không thể trống. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: Tạo config.yaml label: Tệp config.yaml đã được tạo. desc: >- Bạn có thể tạo tệp <1>config.yaml thủ công trong thư mục <1>/var/wwww/xxx/ và dán văn bản sau vào đó. info: Sau khi bạn đã làm xong, nhấp vào nút "Tiếp theo". site_information: Thông tin trang admin_account: Tài khoản quản trị site_name: label: Tên trang msg: Tên trang không thể trống. msg_max_length: Tên trang phải có tối đa 30 ký tự. site_url: label: URL trang text: Địa chỉ của trang của bạn. msg: empty: URL trang không thể trống. incorrect: Định dạng URL trang không chính xác. max_length: URL trang phải có tối đa 512 ký tự. contact_email: label: Email liên hệ text: Địa chỉ email của người liên hệ chính phụ trách trang này. msg: empty: Email liên hệ không thể trống. incorrect: Định dạng email liên hệ không chính xác. login_required: label: Riêng tư switch: Yêu cầu đăng nhập text: Chỉ người dùng đã đăng nhập mới có thể truy cập cộng đồng này. admin_name: label: Tên msg: Tên không thể trống. character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: Mật khẩu text: >- Bạn sẽ cần mật khẩu này để đăng nhập. Vui lòng lưu trữ nó ở một nơi an toàn. msg: Mật khẩu không thể trống. msg_min_length: Mật khẩu phải có ít nhất 8 ký tự. msg_max_length: Mật khẩu phải có tối đa 32 ký tự. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: Email text: Bạn sẽ cần email này để đăng nhập. msg: empty: Email không thể trống. incorrect: Định dạng email không chính xác. ready_title: Trang web của bạn đã sẵn sàng ready_desc: >- Nếu bạn cảm thấy muốn thay đổi thêm cài đặt nào đó, hãy truy cập <1>mục quản trị; tìm nó trong menu trang. good_luck: "Chúc bạn vui vẻ và may mắn!" warn_title: Cảnh báo warn_desc: >- Tệp <1>config.yaml đã tồn tại. Nếu bạn cần đặt lại bất kỳ mục cấu hình nào trong tệp này, vui lòng xóa nó trước. install_now: Bạn có thể thử <1>cài đặt ngay bây giờ. installed: Đã cài đặt installed_desc: >- Có vẻ như bạn đã cài đặt rồi. Để cài đặt lại, vui lòng xóa các bảng cơ sở dữ liệu cũ trước. db_failed: Kết nối cơ sở dữ liệu thất bại db_failed_desc: >- Điều này có thể có nghĩa là thông tin cơ sở dữ liệu trong tệp <1>config.yaml của bạn không chính xác hoặc không thể thiết lập liên lạc với máy chủ cơ sở dữ liệu. Điều này có thể có nghĩa là máy chủ cơ sở dữ liệu của máy chủ của bạn đang bị tắt. counts: views: lượt xem votes: bình chọn answers: câu trả lời accepted: Đã chấp nhận page_error: http_error: Lỗi HTTP {{ code }} desc_403: Bạn không có quyền truy cập trang này. desc_404: Thật không may, trang này không tồn tại. desc_50X: Máy chủ đã gặp sự cố và không thể hoàn thành yêu cầu của bạn. back_home: Quay lại trang chủ page_maintenance: desc: "Chúng tôi đang bảo trì, chúng tôi sẽ trở lại sớm." nav_menus: dashboard: Bảng điều khiển contents: Nội dung questions: Câu hỏi answers: Câu trả lời users: Người dùng badges: Huy hiệu flags: Cờ settings: Cài đặt general: Chung interface: Giao diện smtp: SMTP branding: Thương hiệu legal: Pháp lý write: Viết terms: Terms tos: Điều khoản dịch vụ privacy: Quyền riêng tư seo: SEO customize: Tùy chỉnh themes: Chủ đề login: Đăng nhập privileges: Đặc quyền plugins: Plugins installed_plugins: Plugin đã cài đặt apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Chào mừng bạn đến với {{site_name}} user_center: login: Đăng nhập qrcode_login_tip: Vui lòng sử dụng {{ agentName }} để quét mã QR và đăng nhập. login_failed_email_tip: Đăng nhập thất bại, vui lòng cho phép ứng dụng này truy cập thông tin email của bạn trước khi thử lại. badges: modal: title: Chúc mừng content: Bạn đã nhận được huy hiệu mới. close: Đóng confirm: Xem huy hiệu title: Huy hiệu awarded: Giải Thưởng earned_×: Nhận được ×{{ number }} ×_awarded: "{{ number }} được trao tặng" can_earn_multiple: Bạn có thể kiếm được nhiều lần. earned: Đã nhận admin: admin_header: title: Quản trị dashboard: title: Bảng điều khiển welcome: Chào mừng bạn đến với Answer Admin! site_statistics: Thống kê trang questions: "Câu hỏi:" resolved: "Đã giải quyết:" unanswered: "Chưa được trả lời:" answers: "Câu trả lời:" comments: "Bình luận:" votes: "Phiếu bầu:" users: "Người dùng:" flags: "Cờ:" reviews: "Đánh giá:" site_health: Sức khỏe trang version: "Phiên bản:" https: "HTTPS:" upload_folder: "Thư mục tải lên:" run_mode: "Chế độ hoạt động:" private: Riêng tư public: Công cộng smtp: "SMTP:" timezone: "Múi giờ:" system_info: Thông tin hệ thống go_version: "Phiên bản Go:" database: "Database:" database_size: "Tệp tin Database:" storage_used: "Bộ nhớ đã sử dụng:" uptime: "Thời gian hoạt động:" links: Links plugins: Plugin github: GitHub blog: Blog contact: Liên hệ forum: Diễn đàn documents: Tài liệu feedback: Phản hồi support: Hỗ trợ review: Đánh giá config: Cấu hình update_to: Cập nhật lên latest: Mới nhất check_failed: Kiểm tra thất bại "yes": "Có" "no": "Không" not_allowed: Không được phép allowed: Được phép enabled: Đã bật disabled: Đã tắt writable: Có thể chỉnh sửa not_writable: Không thể ghi flags: title: Cờ pending: Đang chờ xử lý completed: Hoàn thành flagged: Đã đánh dấu flagged_type: Đã đánh dấu {{ type }} created: Đã tạo action: Hành động review: Đánh giá user_role_modal: title: Thay đổi vai trò người dùng thành... btn_cancel: Hủy btn_submit: Gửi new_password_modal: title: Đặt mật khẩu mới form: fields: password: label: Mật khẩu text: Người dùng sẽ bị đăng xuất và cần đăng nhập lại. msg: Mật khẩu phải có độ dài từ 8 đến 32 ký tự. btn_cancel: Hủy btn_submit: Gửi edit_profile_modal: title: Chỉnh sửa hồ sơ form: fields: display_name: label: Tên hiển thị msg_range: Display name must be 2-30 characters in length. username: label: Tên người dùng msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Địa chỉ email không hợp lệ. edit_success: Chỉnh Sửa Thành Công btn_cancel: Hủy btn_submit: Gửi user_modal: title: Thêm người dùng mới form: fields: users: label: Thêm người dùng hàng loạt placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Tách "tên, email, mật khẩu" bằng dấu phẩy. Một người dùng mỗi dòng. msg: "Vui lòng nhập email của người dùng, một dòng mỗi người." display_name: label: Tên hiển thị msg: Tên hiển thị phải dài từ 2-30 ký tự. email: label: Email msg: Email không hợp lệ. password: label: Mật khẩu msg: Mật khẩu phải có từ 8 đến 32 ký tự. btn_cancel: Hủy btn_submit: Gửi users: title: Người dùng name: Tên email: Email reputation: Danh tiếng created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: Trạng thái role: Vai trò action: Hành động change: Thay đổi all: Tất cả staff: Nhân viên more: Thêm inactive: Không hoạt động suspended: Bị tạm ngưng deleted: Đã xóa normal: Bình thường Moderator: Người điều hành Admin: Quản trị viên User: Người dùng filter: placeholder: "Lọc theo tên, user:id" set_new_password: Đặt mật khẩu mới edit_profile: Chỉnh sửa hồ sơ change_status: Thay đổi trạng thái change_role: Thay đổi vai trò show_logs: Hiển thị nhật ký add_user: Thêm người dùng deactivate_user: title: Ngừng kích hoạt người dùng content: Người dùng không hoạt động phải xác nhận lại email của họ. delete_user: title: Xóa người dùng này content: Bạn có chắc chắn muốn xóa người dùng này không? Điều này là vĩnh viễn! remove: Xóa nội dung của họ label: Xóa tất cả các câu hỏi, câu trả lời, bình luận, vv. text: Không chọn điều này nếu bạn chỉ muốn xóa tài khoản của người dùng. suspend_user: title: Đình chỉ người dùng này content: Người dùng bị đình chỉ không thể đăng nhập. label: How long will the user be suspended for? forever: Forever questions: page_title: Câu hỏi unlisted: Không được liệt kê post: Bài đăng votes: Phiếu bầu answers: Câu trả lời created: Đã tạo status: Trạng thái action: Hành động change: Thay đổi pending: Đang chờ xử lý filter: placeholder: "Lọc theo tiêu đề, question:id" answers: page_title: Câu trả lời post: Bài đăng votes: Phiếu bầu created: Đã tạo status: Trạng thái action: Hành động change: Thay đổi filter: placeholder: "Lọc theo tiêu đề, answer:id" general: page_title: Chung name: label: Tên trang msg: Tên trang không thể trống. text: "Tên của trang này, được sử dụng trong thẻ tiêu đề." site_url: label: URL trang msg: Url trang không thể trống. validate: Vui lòng nhập URL hợp lệ. text: Địa chỉ của trang của bạn. short_desc: label: Mô tả ngắn của trang msg: Mô tả ngắn của trang không thể trống. text: "Mô tả ngắn, được sử dụng trong thẻ tiêu đề trên trang chủ." desc: label: Mô tả trang msg: Mô tả trang không thể trống. text: "Mô tả trang này trong một câu, được sử dụng trong thẻ mô tả meta." contact_email: label: Email liên hệ msg: Email liên hệ không thể trống. validate: Định dạng email liên hệ không hợp lệ. text: Địa chỉ email của người liên hệ chính phụ trách trang này. check_update: label: Cập nhật phần mềm text: Tự động kiểm tra cập nhật interface: page_title: Giao diện language: label: Ngôn ngữ giao diện msg: Ngôn ngữ giao diện không thể trống. text: Ngôn ngữ giao diện người dùng. Nó sẽ thay đổi khi bạn làm mới trang. time_zone: label: Múi giờ msg: Múi giờ không thể trống. text: Chọn một thành phố cùng múi giờ với bạn. avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: Email gửi từ msg: Email gửi từ không thể trống. text: Địa chỉ email mà các email được gửi từ đó. from_name: label: Tên gửi từ msg: Tên gửi từ không thể trống. text: Tên mà các email được gửi từ đó. smtp_host: label: Máy chủ SMTP msg: Máy chủ SMTP không thể trống. text: Máy chủ thư của bạn. encryption: label: Mã hóa msg: Mã hóa không thể trống. text: Đối với hầu hết các máy chủ, SSL là tùy chọn được khuyến nghị. ssl: SSL tls: TLS none: Không smtp_port: label: Cổng SMTP msg: Cổng SMTP phải là số từ 1 đến 65535. text: Cổng đến máy chủ thư của bạn. smtp_username: label: Tên người dùng SMTP msg: Tên người dùng SMTP không thể trống. smtp_password: label: Mật khẩu SMTP msg: Mật khẩu SMTP không thể trống. test_email_recipient: label: Người nhận email kiểm tra text: Cung cấp địa chỉ email sẽ nhận email kiểm tra. msg: Người nhận email kiểm tra không hợp lệ smtp_authentication: label: Bật xác thực title: Xác thực SMTP msg: Xác thực SMTP không thể trống. "yes": "Có" "no": "Không" branding: page_title: Thương hiệu logo: label: Logo msg: Logo không thể trống. text: Hình ảnh logo ở góc trên bên trái của trang của bạn. Sử dụng hình ảnh hình chữ nhật rộng với chiều cao 56 và tỷ lệ khung hình lớn hơn 3:1. Nếu để trống, văn bản tiêu đề trang sẽ được hiển thị. mobile_logo: label: Logo di động text: Logo được sử dụng trên phiên bản di động của trang của bạn. Sử dụng hình ảnh hình chữ nhật rộng với chiều cao 56. Nếu để trống, hình ảnh từ cài đặt "logo" sẽ được sử dụng. square_icon: label: Biểu tượng vuông msg: Biểu tượng vuông không thể trống. text: Hình ảnh được sử dụng làm cơ sở cho các biểu tượng siêu dữ liệu. Nên lớn hơn 512x512. favicon: label: Favicon text: Favicon cho trang của bạn. Để hoạt động chính xác trên một CDN, nó phải là png. Sẽ được thay đổi kích thước thành 32x32. Nếu để trống, "biểu tượng vuông" sẽ được sử dụng. legal: page_title: Pháp lý terms_of_service: label: Điều khoản dịch vụ text: "Bạn có thể thêm nội dung điều khoản dịch vụ ở đây. Nếu bạn đã có một tài liệu được lưu trữ ở nơi khác, cung cấp URL đầy đủ ở đây." privacy_policy: label: Chính sách bảo mật text: "Bạn có thể thêm nội dung chính sách bảo mật ở đây. Nếu bạn đã có một tài liệu được lưu trữ ở nơi khác, cung cấp URL đầy đủ ở đây." external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Câu trả lời chỉnh sửa label: Each user can only write one answer for each question text: "Tắt để cho phép người dùng viết nhiều câu trả lời cho cùng một câu hỏi, điều này có thể khiến các câu trả lời bị mất trọng tâm." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Thẻ được đề xuất text: "Các thẻ gợi ý sẽ hiển thị trong danh sách thả xuống theo mặc định." msg: contain_reserved: "các thẻ được đề xuất không được chứa thẻ dự bị" required_tag: title: Đặt thẻ cần thiết label: Đặt thẻ được đề xuất là bắt buộc text: "Mỗi câu hỏi mới phải có ít nhất một thẻ được đề xuất." reserved_tags: label: Thẻ dành riêng text: "Thẻ dành riêng chỉ có thể được thêm vào một bài đăng bởi điều hành viên." image_size: label: Kích thước hình ảnh tối đa (MB) text: "Kích thước tải lên hình ảnh tối đa." attachment_size: label: Kích thước tệp đính kèm tối đa (MB) text: "Kích thước tải lên tệp đính kèm tối đa." image_megapixels: label: Megapixel hình ảnh tối đa text: "Số megapixel tối đa được phép cho một hình ảnh." image_extensions: label: Tiện ích mở rộng hình ảnh được ủy quyền text: "Danh sách đuôi file được phép hiển thị hình ảnh, phân cách bằng dấu phẩy." attachment_extensions: label: Các loại tệp đính kèm được phép tải lên text: "Danh sách các đuôi file được phép tải lên, phân cách bằng dấu phẩy. CẢNH BÁO: Cho phép tải lên có thể gây ra vấn đề bảo mật." seo: page_title: SEO permalink: label: Liên kết cố định text: Cấu trúc URL tùy chỉnh có thể cải thiện khả năng sử dụng và khả năng tương thích về sau của liên kết của bạn. robots: label: robots.txt text: Điều này sẽ ghi đè vĩnh viễn bất kỳ cài đặt trang web liên quan nào. themes: page_title: Giao diện themes: label: Giao diện text: Chọn một chủ đề hiện có. color_scheme: label: Sơ đồ màu navbar_style: label: Navbar background style primary_color: label: Màu chính text: Thay đổi các màu sắc được sử dụng bởi chủ đề của bạn layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS và HTML custom_css: label: CSS tùy chỉnh text: > head: label: Đầu text: > header: label: Đầu trang text: > footer: label: Cuối trang text: Điều này sẽ chèn trước </body>. sidebar: label: Thanh bên text: Điều này sẽ chèn vào thanh bên. login: page_title: Đăng nhập membership: title: Thành viên label: Cho phép đăng ký mới text: Tắt để ngăn ai đó tạo tài khoản mới. email_registration: title: Đăng ký qua email label: Cho phép đăng ký qua email text: Tắt để ngăn ai đó tạo tài khoản mới thông qua email. allowed_email_domains: title: Miền email được phép text: Miền email mà người dùng phải đăng ký tài khoản. Một miền mỗi dòng. Bỏ qua khi trống. private: title: Riêng tư label: Yêu cầu đăng nhập text: Chỉ người dùng đã đăng nhập mới có thể truy cập cộng đồng này. password_login: title: Đăng nhập bằng mật khẩu label: Cho phép đăng nhập bằng email và mật khẩu text: "CẢNH BÁO: Nếu tắt, bạn có thể không thể đăng nhập nếu bạn chưa cấu hình phương thức đăng nhập khác trước đó." installed_plugins: title: Plugin đã cài đặt plugin_link: Plugin mở rộng và mở rộng chức năng của trang web. Bạn có thể tìm thấy plugin trong <1>Kho Plugin Answer. filter: all: Tất cả active: Đang hoạt động inactive: Không hoạt động outdated: Quá hạn plugins: label: Plugin text: Chọn một plugin hiện có. name: Tên version: Phiên bản status: Trạng thái action: Hành động deactivate: Vô hiệu hóa activate: Kích hoạt settings: Cài đặt settings_users: title: Người dùng avatar: label: Hình đại diện mặc định text: Dành cho người dùng không có hình đại diện tùy chỉnh của riêng họ. gravatar_base_url: label: Gravatar Base URL text: URL của nhà cung cấp API Gravatar. Bỏ qua khi trống. profile_editable: title: Hồ sơ có thể chỉnh sửa allow_update_display_name: label: Cho phép người dùng thay đổi tên hiển thị của họ allow_update_username: label: Cho phép người dùng thay đổi tên người dùng của họ allow_update_avatar: label: Cho phép người dùng thay đổi hình ảnh hồ sơ của họ allow_update_bio: label: Cho phép người dùng thay đổi giới thiệu về mình allow_update_website: label: Cho phép người dùng thay đổi trang web của họ allow_update_location: label: Cho phép người dùng thay đổi vị trí của họ privilege: title: Đặc quyền level: label: Mức độ danh tiếng yêu cầu text: Chọn mức danh tiếng yêu cầu cho các đặc quyền msg: should_be_number: dữ liệu đầu vào phải là kiểu số number_larger_1: số phải bằng hoặc lớn hơn 1 badges: action: Hành động active: Hoạt động activate: Kích hoạt all: Tất cả awards: Giải Thưởng deactivate: Ngừng kích hoạt filter: placeholder: Lọc theo tên, user:id group: Nhóm inactive: Không hoạt động name: Tên show_logs: Hiển thị nhật ký status: Trạng thái title: Danh hiệu apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (tùy chọn) empty: không thể trống invalid: không hợp lệ btn_submit: Lưu not_found_props: "Không tìm thấy thuộc tính bắt buộc {{ key }}." select: Chọn page_review: review: Xem xét proposed: đề xuất question_edit: Chỉnh sửa câu hỏi answer_edit: Câu trả lời chỉnh sửa tag_edit: Chỉnh sửa thẻ edit_summary: Tóm tắt chỉnh sửa edit_question: Chỉnh sửa câu hỏi edit_answer: Chỉnh sửa câu trả lời edit_tag: Chỉnh sửa thẻ empty: Không còn nhiệm vụ xem xét nào. approve_revision_tip: Bạn có chấp nhận sửa đổi này không? approve_flag_tip: Bạn có chấp nhận cờ này không? approve_post_tip: Bạn có chấp nhận bài đăng này không? approve_user_tip: Bạn có chấp nhận người dùng này không? suggest_edits: Đề xuất chỉnh sửa flag_post: Đánh dấu bài đăng flag_user: Đánh dấu người dùng queued_post: Bài đăng trong hàng đợi queued_user: Người dùng trong hàng đợi filter_label: Loại reputation: danh tiếng flag_post_type: Đánh dấu bài đăng này là {{ type }}. flag_user_type: Đánh dấu người dùng này là {{ type }}. edit_post: Chỉnh sửa bài đăng list_post: Liệt kê bài đăng unlist_post: Gỡ bỏ bài đăng khỏi danh sách timeline: undeleted: đã khôi phục deleted: đã xóa downvote: bỏ phiếu xuống upvote: bỏ phiếu lên accept: chấp nhận cancelled: đã hủy commented: đã bình luận rollback: quay lại edited: đã chỉnh sửa answered: đã trả lời asked: đã hỏi closed: đã đóng reopened: đã mở lại created: đã tạo pin: đã ghim unpin: bỏ ghim show: được liệt kê hide: không được liệt kê title: "Lịch sử cho" tag_title: "Dòng thời gian cho" show_votes: "Hiển thị phiếu bầu" n_or_a: N/A title_for_question: "Dòng thời gian cho" title_for_answer: "Dòng thời gian cho câu trả lời của {{ title }} bởi {{ author }}" title_for_tag: "Dòng thời gian cho thẻ" datetime: Ngày giờ type: Loại by: Bởi comment: Bình luận no_data: "Chúng tôi không thể tìm thấy bất cứ thứ gì." users: title: Người dùng users_with_the_most_reputation: Người dùng có điểm danh tiếng cao nhất trong tuần này users_with_the_most_vote: Người dùng đã bỏ phiếu nhiều nhất trong tuần này staffs: Nhân viên cộng đồng của chúng tôi reputation: danh tiếng votes: phiếu bầu prompt: leave_page: Bạn có chắc chắn muốn rời khỏi trang không? changes_not_save: Các thay đổi của bạn có thể không được lưu. draft: discard_confirm: Bạn có chắc chắn muốn hủy bản nháp của mình không? messages: post_deleted: Bài đăng này đã bị xóa. post_cancel_deleted: Bài đăng này đã được phục hồi. post_pin: Bài đăng này đã được ghim. post_unpin: Bài đăng này đã bị bỏ ghim. post_hide_list: Bài đăng này đã được ẩn khỏi danh sách. post_show_list: Bài đăng này đã được hiển thị trên danh sách. post_reopen: Bài đăng này đã được mở lại. post_list: Bài đăng này đã được liệt kê. post_unlist: Bài đăng này đã được gỡ bỏ khỏi danh sách. post_pending: Bài đăng của bạn đang chờ xem xét. Đây là bản xem trước, nó sẽ được hiển thị sau khi được phê duyệt. post_closed: Bài đăng này đã bị đóng. answer_deleted: Câu trả lời này đã bị xóa. answer_cancel_deleted: Câu trả lời này đã được phục hồi. change_user_role: Vai trò của người dùng này đã được thay đổi. user_inactive: Người dùng này đã không hoạt động. user_normal: Người dùng này đã bình thường. user_suspended: Người dùng này đã bị đình chỉ. user_deleted: Người dùng này đã bị xóa. user_added: User has been added successfully. badge_activated: Huy hiệu này đã được kích hoạt. badge_inactivated: Huy hiệu này đã bị vô hiệu hóa. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: i18n/zh_CN.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: 成功。 unknown: other: 未知错误。 request_format_error: other: 请求格式错误。 unauthorized_error: other: 未授权。 database_error: other: 数据服务器错误。 forbidden_error: other: 禁止访问。 duplicate_request_error: other: 重复提交。 action: report: other: 举报 edit: other: 编辑 delete: other: 删除 close: other: 关闭 reopen: other: 重新打开 forbidden_error: other: 禁止访问。 pin: other: 置顶 hide: other: 列表隐藏 unpin: other: 取消置顶 show: other: 列表显示 invite_someone_to_answer: other: 编辑 undelete: other: 撤消删除 merge: other: 合并 role: name: user: other: 用户 admin: other: 管理员 moderator: other: 版主 description: user: other: 默认没有特殊权限。 admin: other: 拥有管理网站的全部权限。 moderator: other: 拥有除访问后台管理以外的所有权限。 privilege: level_1: description: other: 级别 1(少量声望要求,适合私有团队、群组) level_2: description: other: 级别 2(低声望要求,适合初启动的社区) level_3: description: other: 级别 3(高声望要求,适合成熟的社区) level_custom: description: other: 自定义等级 rank_question_add_label: other: 提问 rank_answer_add_label: other: 写答案 rank_comment_add_label: other: 写评论 rank_report_add_label: other: 举报 rank_comment_vote_up_label: other: 点赞评论 rank_link_url_limit_label: other: 每次发布超过 2 个链接 rank_question_vote_up_label: other: 点赞问题 rank_answer_vote_up_label: other: 点赞答案 rank_question_vote_down_label: other: 点踩问题 rank_answer_vote_down_label: other: 点踩答案 rank_invite_someone_to_answer_label: other: 邀请回答 rank_tag_add_label: other: 创建新标签 rank_tag_edit_label: other: 编辑标签描述(需要审核) rank_question_edit_label: other: 编辑别人的问题(需要审核) rank_answer_edit_label: other: 编辑别人的答案(需要审核) rank_question_edit_without_review_label: other: 编辑别人的问题无需审核 rank_answer_edit_without_review_label: other: 编辑别人的答案无需审核 rank_question_audit_label: other: 审核问题编辑 rank_answer_audit_label: other: 审核回答编辑 rank_tag_audit_label: other: 审核标签编辑 rank_tag_edit_without_review_label: other: 编辑标签描述无需审核 rank_tag_synonym_label: other: 管理标签同义词 email: other: 邮箱 e_mail: other: 邮箱 password: other: 密码 pass: other: 密码 old_pass: other: 当前密码 original_text: other: 本帖 email_or_password_wrong_error: other: 邮箱和密码不匹配。 error: common: invalid_url: other: 无效的 URL。 status_invalid: other: 无效状态。 password: space_invalid: other: 密码不得含有空格。 admin: cannot_update_their_password: other: 你无法修改自己的密码。 cannot_edit_their_profile: other: 您不能修改您的个人资料。 cannot_modify_self_status: other: 你无法修改自己的状态。 email_or_password_wrong: other: 邮箱和密码不匹配。 answer: not_found: other: 没有找到答案。 cannot_deleted: other: 没有删除权限。 cannot_update: other: 没有更新权限。 question_closed_cannot_add: other: 问题已关闭,无法添加。 content_cannot_empty: other: 回答内容不能为空。 comment: edit_without_permission: other: 不允许编辑评论。 not_found: other: 评论未找到。 cannot_edit_after_deadline: other: 评论时间太久,无法修改。 content_cannot_empty: other: 评论内容不能为空。 email: duplicate: other: 邮箱已存在。 need_to_be_verified: other: 邮箱需要验证。 verify_url_expired: other: 邮箱验证的网址已过期,请重新发送邮件。 illegal_email_domain_error: other: 此邮箱不在允许注册的邮箱域中。请使用其他邮箱尝试。 lang: not_found: other: 语言文件未找到。 object: captcha_verification_failed: other: 验证码错误。 disallow_follow: other: 你不能关注。 disallow_vote: other: 你不能投票。 disallow_vote_your_self: other: 你不能为自己的帖子投票。 not_found: other: 对象未找到。 verification_failed: other: 验证失败。 email_or_password_incorrect: other: 邮箱和密码不匹配。 old_password_verification_failed: other: 旧密码验证失败。 new_password_same_as_previous_setting: other: 新密码和旧密码相同。 already_deleted: other: 该帖子已被删除。 meta: object_not_found: other: Meta 对象未找到 question: already_deleted: other: 该帖子已被删除。 under_review: other: 您的帖子正在等待审核。它将在它获得批准后可见。 not_found: other: 问题未找到。 cannot_deleted: other: 没有删除权限。 cannot_close: other: 没有关闭权限。 cannot_update: other: 没有更新权限。 content_cannot_empty: other: 内容不能为空。 content_less_than_minimum: other: 输入的内容不足。 rank: fail_to_meet_the_condition: other: 声望值未达到要求。 vote_fail_to_meet_the_condition: other: 感谢投票。你至少需要 {{.Rank}} 声望才能投票。 no_enough_rank_to_operate: other: 你至少需要 {{.Rank}} 声望才能执行此操作。 report: handle_failed: other: 报告处理失败。 not_found: other: 报告未找到。 tag: already_exist: other: 标签已存在。 not_found: other: 标签未找到。 recommend_tag_not_found: other: 推荐标签不存在。 recommend_tag_enter: other: 请选择至少一个必选标签。 not_contain_synonym_tags: other: 不应包含同义词标签。 cannot_update: other: 没有更新权限。 is_used_cannot_delete: other: 你不能删除这个正在使用的标签。 cannot_set_synonym_as_itself: other: 你不能将当前标签设为自己的同义词。 minimum_count: other: 没有输入足够的标签。 smtp: config_from_name_cannot_be_email: other: 发件人名称不能是邮箱地址。 theme: not_found: other: 主题未找到。 revision: review_underway: other: 目前无法编辑,有一个版本在审阅队列中。 no_permission: other: 无权限修改。 user: external_login_missing_user_id: other: 第三方平台没有提供唯一的 UserID,所以你不能登录,请联系网站管理员。 external_login_unbinding_forbidden: other: 请在移除此登录之前为你的账户设置登录密码。 email_or_password_wrong: other: other: 邮箱和密码不匹配。 not_found: other: 用户未找到。 suspended: other: 用户已被封禁。 username_invalid: other: 用户名无效。 username_duplicate: other: 用户名已被使用。 set_avatar: other: 头像设置错误。 cannot_update_your_role: other: 你不能修改自己的角色。 not_allowed_registration: other: 该网站暂未开放注册。 not_allowed_login_via_password: other: 该网站暂不支持密码登录。 access_denied: other: 拒绝访问 page_access_denied: other: 您没有权限访问此页面。 add_bulk_users_format_error: other: "发生错误,{{.Field}} 格式错误,在 '{{.Content}}' 行数 {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "一次性添加的用户数量应在 1-{{.MaxAmount}} 之间。" status_suspended_forever: other: "该用户已被永久封禁。该用户不符合社区准则。" status_suspended_until: other: "该用户已被封禁至 {{.SuspendedUntil}}。该用户不符合社区准则。" status_deleted: other: "该用户已被删除。" status_inactive: other: "该用户未激活。" config: read_config_failed: other: 读取配置失败 database: connection_failed: other: 数据库连接失败 create_table_failed: other: 创建表失败 install: create_config_failed: other: 无法创建 config.yaml 文件。 upload: unsupported_file_format: other: 不支持的文件格式。 site_info: config_not_found: other: 未找到网站的该配置信息。 badge: object_not_found: other: 没有找到徽章对象 reason: spam: name: other: 垃圾信息 desc: other: 这个帖子是一个广告,或是破坏性行为。它对当前的主题无帮助或无关。 rude_or_abusive: name: other: 粗鲁或辱骂的 desc: other: "一个有理智的人都会认为这种内容不适合进行尊重性的讨论。" a_duplicate: name: other: 重复内容 desc: other: 该问题有人问过,而且已经有了答案。 placeholder: other: 输入已有的问题链接 not_a_answer: name: other: 不是答案 desc: other: "该帖是作为答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑、评论、另一个问题或者需要被删除。" no_longer_needed: name: other: 不再需要 desc: other: 该评论已过时,对话性质或与此帖子无关。 something: name: other: 其他原因 desc: other: 此帖子需要工作人员注意,因为是上述所列以外的其他理由。 placeholder: other: 让我们具体知道你关心的什么 community_specific: name: other: 社区特定原因 desc: other: 该问题不符合社区准则。 not_clarity: name: other: 需要细节或澄清 desc: other: 该问题目前涵盖多个问题。它应该侧重在一个问题上。 looks_ok: name: other: 看起来没问题 desc: other: 这个帖子是好的,不是低质量。 needs_edit: name: other: 需要编辑,我已做了修改。 desc: other: 改进和纠正你自己帖子中的问题。 needs_close: name: other: 需要关闭 desc: other: 关闭的问题不能回答,但仍然可以编辑、投票和评论。 needs_delete: name: other: 需要删除 desc: other: 该帖子将被删除。 question: close: duplicate: name: other: 垃圾信息 desc: other: 此问题以前就有人问过,而且已经有了答案。 guideline: name: other: 社区特定原因 desc: other: 该问题不符合社区准则。 multiple: name: other: 需要细节或澄清 desc: other: 该问题目前涵盖多个问题。它应该只集中在一个问题上。 other: name: other: 其他原因 desc: other: 该帖子存在上面没有列出的另一个原因。 operation_type: asked: other: 提问于 answered: other: 回答于 modified: other: 修改于 deleted_title: other: 删除的问题 questions_title: other: 问题 tag: tags_title: other: 标签 no_description: other: 此标签没有描述。 notification: action: update_question: other: 更新了问题 answer_the_question: other: 回答了问题 update_answer: other: 更新了答案 accept_answer: other: 采纳了答案 comment_question: other: 评论了问题 comment_answer: other: 评论了答案 reply_to_you: other: 回复了你 mention_you: other: 提到了你 your_question_is_closed: other: 你的问题已被关闭 your_question_was_deleted: other: 你的问题已被删除 your_answer_was_deleted: other: 你的答案已被删除 your_comment_was_deleted: other: 你的评论已被删除 up_voted_question: other: 点赞问题 down_voted_question: other: 点踩问题 up_voted_answer: other: 点赞答案 down_voted_answer: other: 点踩回答 up_voted_comment: other: 点赞评论 invited_you_to_answer: other: 邀请你回答 earned_badge: other: 你获得 "{{.BadgeName}}" 徽章 email_tpl: change_email: title: other: "[{{.SiteName}}] 确认你的新邮箱地址" body: other: "请点击以下链接确认你在 {{.SiteName}} 上的新邮箱地址:
\n{{.ChangeEmailUrl}}

\n\n如果你没有请求此更改,请忽略此邮件。\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到

" new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} 回答了你的问题" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到

\n\n取消订阅" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
我想你可能知道答案。

\n在 {{.SiteName}} 上查看

\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到

\n\n取消订阅" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} 评论了你的帖子" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到

\n\n取消订阅" new_question: title: other: "[{{.SiteName}}] 新问题: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}

\n{{.Tags}}

\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到

\n\n取消订阅" pass_reset: title: other: "[{{.SiteName }}] 重置密码" body: other: "有人要求在 [{{.SiteName}}] 上重置你的密码。

\n\n如果这不是你的操作,请安心忽略此电子邮件。

\n\n请点击以下链接设置一个新密码:
\n{{.PassResetUrl}}\n\n如果你没有请求此更改,请忽略此邮件。\n" register: title: other: "[{{.SiteName}}] 确认你的新账户" body: other: "欢迎加入 {{.SiteName}}!

\n\n请点击以下链接确认并激活你的新账户:
\n{{.RegisterUrl}}

\n\n如果上面的链接不能点击,请将其复制并粘贴到你的浏览器地址栏中。\n

\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到" test: title: other: "[{{.SiteName}}] 测试邮件" body: other: "这是测试电子邮件。\n

\n\n-
\n注意:这是一个自动的系统电子邮件, 请不要回复此消息,因为您的回复将不会被看到。" action_activity_type: upvote: other: 点赞 upvoted: other: 点赞 downvote: other: 点踩 downvoted: other: 点踩 accept: other: 采纳 accepted: other: 已采纳 edit: other: 编辑 review: queued_post: other: 排队的帖子 flagged_post: other: 举报的帖子 suggested_post_edit: other: 建议的编辑 reaction: tooltip: other: "{{ .Names }} 以及另外 {{ .Count }} 个..." badge: default_badges: autobiographer: name: other: 自传作者 desc: other: 填写了 个人资料 信息。 certified: name: other: 已认证 desc: other: 完成了我们的新用户教程。 editor: name: other: 编辑者 desc: other: 首次帖子编辑。 first_flag: name: other: 第一次举报 desc: other: 第一次举报一个帖子 first_upvote: name: other: 第一次投票 desc: other: 第一次投票了一个帖子。 first_link: name: other: 第一个链接 desc: other: 第一次添加了一个链接到另一个帖子。 first_reaction: name: other: 第一个响应 desc: other: 第一次表情回应帖子 first_share: name: other: 首次分享 desc: other: 首次分享了一个帖子。 scholar: name: other: 学者 desc: other: 问了一个问题并接受了一个答案。 commentator: name: other: 评论员 desc: other: 留下5条评论。 new_user_of_the_month: name: other: 月度用户 desc: other: 本月杰出用户 read_guidelines: name: other: 阅读指南 desc: other: 阅读[社区准则]。 reader: name: other: 读者 desc: other: 用10个以上的答案在主题中阅读每个答案。 welcome: name: other: 欢迎 desc: other: 获得一个点赞投票 nice_share: name: other: 好分享 desc: other: 分享了一个拥有25个唯一访客的帖子。 good_share: name: other: 好分享 desc: other: 分享了一个拥有300个唯一访客的帖子。 great_share: name: other: 优秀的分享 desc: other: 分享了一个拥有1000个唯一访客的帖子。 out_of_love: name: other: 失去爱好 desc: other: 一天内使用了 50 个赞。 higher_love: name: other: 更高的爱好 desc: other: 一天内使用了 50 个赞 5 次。 crazy_in_love: name: other: 爱情疯狂的 desc: other: 一天内使用了 50 个赞 20 次。 promoter: name: other: 推荐人 desc: other: 邀请用户。 campaigner: name: other: 宣传者 desc: other: 邀请了3个基本用户。 champion: name: other: 冠军 desc: other: 邀请了5个成员。 thank_you: name: other: 谢谢 desc: other: 有 20 个赞成票的帖子,并投了 10 个赞成票。 gives_back: name: other: 返回 desc: other: 拥有100个投票赞成的职位并放弃了100个投票。 empathetic: name: other: 情随境迁 desc: other: 拥有500个投票赞成的职位并放弃了1000个投票。 enthusiast: name: other: 狂热 desc: other: 连续访问10天。 aficionado: name: other: Aficionado desc: other: 连续访问100天。 devotee: name: other: Devotee desc: other: 连续访问365天。 anniversary: name: other: 周年纪念日 desc: other: 活跃成员一年至少发布一次。 appreciated: name: other: 欣赏 desc: other: 在 20 个帖子中获得 1个投票 respected: name: other: 尊敬 desc: other: 100个员额获得2次补票。 admired: name: other: 仰慕 desc: other: 300个员额获得5次补票。 solved: name: other: 已解决 desc: other: 接受答案。 guidance_counsellor: name: other: 指导顾问 desc: other: 接受答案。 know_it_all: name: other: 万事通 desc: other: 接受50个答案。 solution_institution: name: other: 解决方案机构 desc: other: 有150个答案被接受。 nice_answer: name: other: 好答案 desc: other: 回答得分为10或以上。 good_answer: name: other: 好答案 desc: other: 回答得分为25或更多。 great_answer: name: other: 优秀答案 desc: other: 回答得分为50或以上。 nice_question: name: other: 好问题 desc: other: 问题得分为10或以上。 good_question: name: other: 好问题 desc: other: 问题得分为25或更多。 great_question: name: other: 很棒的问题 desc: other: 问题得分为50或更多。 popular_question: name: other: 热门问题 desc: other: 问题有 500 个浏览量。 notable_question: name: other: 值得关注问题 desc: other: 问题有 1,000 个浏览量。 famous_question: name: other: 著名的问题 desc: other: 问题有 5,000 个浏览量。 popular_link: name: other: 热门链接 desc: other: 发布了一个带有50个点击的外部链接。 hot_link: name: other: 热门链接 desc: other: 发布了一个带有300个点击的外部链接。 famous_link: name: other: 著名链接 desc: other: 发布了一个带有100个点击的外部链接。 default_badge_groups: getting_started: name: other: 完成初始化 community: name: other: Community 专题 posting: name: other: 发帖 # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: 如何排版 desc: >-
  • 引用问题或答案: #4

  • 添加链接

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 **粗体**

  • 使用 个空格缩进代码

  • 在行首添加 > 表示引用

  • 反引号进行转义 `像 _这样_`

  • 使用 ``` 创建代码块

    ```
    这是代码块
    ```
pagination: prev: 上一页 next: 下一页 page_title: question: 问题 questions: 问题 tag: 标签 tags: 标签 tag_wiki: 标签维基 create_tag: 创建标签 edit_tag: 编辑标签 ask_a_question: 创建问题 edit_question: 编辑问题 edit_answer: 编辑回答 search: 搜索 posts_containing: 帖子包含 settings: 设置 notifications: 通知 login: 登录 sign_up: 注册 account_recovery: 账号恢复 account_activation: 账号激活 confirm_email: 确认电子邮件 account_suspended: 账号已被封禁 admin: 后台管理 change_email: 修改邮箱 install: Answer 安装 upgrade: Answer 升级 maintenance: 网站维护 users: 用户 oauth_callback: 处理中 http_404: HTTP 错误 404 http_50X: HTTP 错误 500 http_403: HTTP 错误 403 logout: 退出 posts: 帖子 ai_assistant: AI 助手 ai_assistant: description: 有问题?问它并获得答案、观点和建议。 recent_conversations: 新对话 show_more: 显示更多 new: 新聊天 ai_generate: 来自帖子的 AI,可能不准确。 copy: 复制 ask_a_follow_up: 提出后续问题 ask_placeholder: 提问 notifications: title: 通知 inbox: 收件箱 achievement: 成就 new_alerts: 新通知 all_read: 全部标记为已读 show_more: 显示更多 someone: 有人 inbox_type: all: 全部 posts: 帖子 invites: 邀请 votes: 投票 answer: 回答 question: 问题 badge_award: 徽章 suspended: title: 你的账号账号已被封禁 until_time: "你的账号被封禁直到 {{ time }}。" forever: 你的账号已被永久封禁。 end: 你违反了我们的社区准则。 contact_us: 联系我们 editor: blockquote: text: 引用 bold: text: 粗体 chart: text: 图表 flow_chart: 流程图 sequence_diagram: 时序图 class_diagram: 类图 state_diagram: 状态图 entity_relationship_diagram: 实体关系图 user_defined_diagram: 用户自定义图表 gantt_chart: 甘特图 pie_chart: 饼图 code: text: 代码块 add_code: 添加代码块 form: fields: code: label: 代码块 msg: empty: 代码块不能为空 language: label: 语言 placeholder: 自动识别 btn_cancel: 取消 btn_confirm: 添加 formula: text: 公式 options: inline: 行内公式 block: 块级公式 heading: text: 标题 options: h1: 标题 1 h2: 标题 2 h3: 标题 3 h4: 标题 4 h5: 标题 5 h6: 标题 6 help: text: 帮助 hr: text: 水平线 image: text: 图片 add_image: 添加图片 tab_image: 上传图片 form_image: fields: file: label: 图像文件 btn: 选择图片 msg: empty: 请选择图片文件。 only_image: 只能上传图片文件。 max_size: 文件大小不能超过 {{size}} MB。 desc: label: 描述 tab_url: 图片地址 form_url: fields: url: label: 图片地址 msg: empty: 图片地址不能为空 name: label: 描述 btn_cancel: 取消 btn_confirm: 添加 uploading: 上传中 indent: text: 缩进 outdent: text: 减少缩进 italic: text: 斜体 link: text: 超链接 add_link: 添加超链接 form: fields: url: label: 链接 msg: empty: 链接不能为空。 name: label: 描述 btn_cancel: 取消 btn_confirm: 添加 ordered_list: text: 有序列表 unordered_list: text: 无序列表 table: text: 表格 heading: 表头 cell: 单元格 file: text: 附件 not_supported: "不支持的文件类型。请尝试上传其他类型的文件如: {{file_type}}。" max_size: "上传文件超过 {{size}} MB。" close_modal: title: 关闭原因是... btn_cancel: 取消 btn_submit: 提交 remark: empty: 不能为空。 msg: empty: 请选择一个原因。 report_modal: flag_title: 我举报这篇帖子的原因是... close_title: 我关闭这篇帖子的原因是... review_question_title: 审查问题 review_answer_title: 审查回答 review_comment_title: 审查评论 btn_cancel: 取消 btn_submit: 提交 remark: empty: 不能为空 msg: empty: 请选择一个原因。 not_a_url: URL 格式不正确。 url_not_match: URL 来源与当前网站不匹配。 tag_modal: title: 创建新标签 form: fields: display_name: label: 显示名称 msg: empty: 显示名称不能为空。 range: 显示名称不能超过 35 个字符。 slug_name: label: URL 固定链接 desc: URL 固定链接不能超过 35 个字符。 msg: empty: URL 固定链接不能为空。 range: URL 固定链接不能超过 35 个字符。 character: URL 固定链接包含非法字符。 desc: label: 描述 revision: label: 编辑历史 edit_summary: label: 编辑备注 placeholder: >- 简单描述更改原因(更正拼写、修复语法、改进格式) btn_cancel: 取消 btn_submit: 提交 btn_post: 发布新标签 tag_info: created_at: 创建于 edited_at: 编辑于 history: 历史 synonyms: title: 同义词 text: 以下标签将被重置到 empty: 此标签目前没有同义词。 btn_add: 添加同义词 btn_edit: 编辑 btn_save: 保存 synonyms_text: 以下标签将被重置到 delete: title: 删除标签 tip_with_posts: >-

我们不允许 删除带有帖子的标签

请先从帖子中移除此标签。

tip_with_synonyms: >-

我们不允许 删除带有同义词的标签

请先从此标签中删除同义词。

tip: 确定要删除吗? close: 关闭 merge: title: 合并标签 source_tag_title: 源标签 source_tag_description: 源标签及其相关数据将重新映射到目标标签。 target_tag_title: 目标标签 target_tag_description: 合并后将在这两个标签之间将创建一个同义词。 no_results: 没有匹配的标签 btn_submit: 提交 btn_close: 关闭 edit_tag: title: 编辑标签 default_reason: 编辑标签 default_first_reason: 添加标签 btn_save_edits: 保存更改 btn_cancel: 取消 dates: long_date: MM 月 DD 日 long_date_with_year: "YYYY 年 MM 月 DD 日" long_date_with_time: "YYYY 年 MM 月 DD 日 HH:mm" now: 刚刚 x_seconds_ago: "{{count}} 秒前" x_minutes_ago: "{{count}} 分钟前" x_hours_ago: "{{count}} 小时前" hour: 小时 day: 天 hours: 小时 days: 日 month: 月 months: 月 year: 年 reaction: heart: 爱心 smile: 微笑 frown: 愁 btn_label: 添加或删除回应。 undo_emoji: 撤销 {{ emoji }} 回应 react_emoji: 用 {{ emoji }} 回应 unreact_emoji: 撤销 {{ emoji }} comment: btn_add_comment: 添加评论 reply_to: 回复 btn_reply: 回复 btn_edit: 编辑 btn_delete: 删除 btn_flag: 举报 btn_save_edits: 保存更改 btn_cancel: 取消 show_more: "{{count}} 条剩余评论" tip_question: >- 使用评论提问更多信息或者提出改进意见。避免在评论里回答问题。 tip_answer: >- 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 tip_vote: 它给帖子添加了一些有用的内容 edit_answer: title: 编辑回答 default_reason: 编辑回答 default_first_reason: 添加答案 form: fields: revision: label: 编辑历史 answer: label: 回答内容 feedback: characters: 内容长度至少 6 个字符 edit_summary: label: 编辑摘要 placeholder: >- 简单描述更改原因(更正拼写、修复语法、改进格式) btn_save_edits: 保存更改 btn_cancel: 取消 tags: title: 标签 sort_buttons: popular: 热门 name: 名称 newest: 最新 button_follow: 关注 button_following: 已关注 tag_label: 个问题 search_placeholder: 通过标签名称过滤 no_desc: 此标签无描述。 more: 更多 wiki: 维基 ask: title: 创建问题 edit_title: 编辑问题 default_reason: 编辑问题 default_first_reason: 创建问题 similar_questions: 相似问题 form: fields: revision: label: 修订版本 title: label: 标题 placeholder: 你的主题是什么?请具体说明。 msg: empty: 标题不能为空。 range: 标题最多 150 个字符 body: label: 内容 msg: empty: 内容不能为空。 hint: optional_body: 描述这个问题是什么。 minimum_characters: "详细描述这个问题,至少需要 {{min_content_length}} 字符。" tags: label: 标签 msg: empty: 必须选择一个标签 answer: label: 回答内容 msg: empty: 回答内容不能为空 edit_summary: label: 编辑备注 placeholder: >- 简单描述更改原因(更正拼写、修复语法、改进格式) btn_post_question: 提交问题 btn_save_edits: 保存更改 answer_question: 回答自己的问题 post_question&answer: 提交问题和回答 tag_selector: add_btn: 添加标签 create_btn: 创建新标签 search_tag: 搜索标签 hint: 描述您的内容是关于什么,至少需要一个标签。 hint_zero_tags: 描述您的内容与什么有关。 hint_more_than_one_tag: "描述您的内容是关于什么,至少需要{{min_tags_number}}个标签。" no_result: 没有匹配的标签 tag_required_text: 必选标签(至少一个) header: nav: question: 问题 tag: 标签 user: 用户 badges: 徽章 profile: 用户主页 setting: 账号设置 logout: 退出 admin: 后台管理 review: 审查 bookmark: 收藏夹 moderation: 管理 search: placeholder: 搜索 footer: build_on: Powered by <1> Apache Answer upload_img: name: 更改 loading: 加载中... pic_auth_code: title: 验证码 placeholder: 输入图片中的文字 msg: empty: 验证码不能为空。 inactive: first: >- 就差一步!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活你的账户。 info: "如果没有收到,请检查你的垃圾邮件文件夹。" another: >- 我们向你的邮箱 {{mail}} 发送了另一封激活电子邮件。可能需要几分钟才能到达;请务必检查您的垃圾邮件箱。 btn_name: 重新发送激活邮件 change_btn_name: 更改邮箱 msg: empty: 不能为空。 resend_email: url_label: 确定要重新发送激活邮件吗? url_text: 你也可以将上面的激活链接给该用户。 login: login_to_continue: 登录以继续 info_sign: 没有账户?<1>注册 info_login: 已经有账户?<1>登录 agreements: 登录即表示您同意<1>隐私政策和<3>服务条款。 forgot_pass: 忘记密码? name: label: 名字 msg: empty: 名字不能为空 range: 名称长度必须在 2 至 30 个字符之间。 character: '只能由 "a-z", "0-9", " - . _" 组成' email: label: 邮箱 msg: empty: 邮箱不能为空 password: label: 密码 msg: empty: 密码不能为空 different: 两次输入密码不一致 account_forgot: page_title: 忘记密码 btn_name: 发送恢复邮件 send_success: >- 如果存在邮箱为 {{mail}} 账户,你将很快收到一封重置密码的说明邮件。 email: label: 邮箱 msg: empty: 邮箱不能为空 change_email: btn_cancel: 取消 btn_update: 更新电子邮件地址 send_success: >- 如果存在邮箱为 {{mail}} 的账户,你将很快收到一封重置密码的说明邮件。 email: label: 新的电子邮件地址 msg: empty: 邮箱不能为空。 oauth: connect: 连接到 {{ auth_name }} remove: 移除 {{ auth_name }} oauth_bind_email: subtitle: 向你的账户添加恢复邮件地址。 btn_update: 更新电子邮件地址 email: label: 邮箱 msg: empty: 邮箱不能为空。 modal_title: 邮箱已经存在。 modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗? modal_cancel: 更改邮箱 modal_confirm: 连接到已有账户 password_reset: page_title: 密码重置 btn_name: 重置我的密码 reset_success: >- 你已经成功更改密码;你将被重定向到登录页面。 link_invalid: >- 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? to_login: 前往登录页面 password: label: 密码 msg: empty: 密码不能为空。 length: 密码长度在8-32个字符之间 different: 两次输入密码不一致 password_confirm: label: 确认新密码 settings: page_title: 设置 goto_modify: 前往修改 nav: profile: 我的资料 notification: 通知 account: 账号 interface: 界面 profile: heading: 个人资料 btn_name: 保存 display_name: label: 显示名称 msg: 昵称不能为空。 msg_range: 显示名称长度必须为 2-30 个字符。 username: label: 用户名 caption: 用户可以通过 "@用户名" 来提及你。 msg: 用户名不能为空 msg_range: 显示名称长度必须为 2-30 个字符。 character: '只能由 "a-z", "0-9", " - . _" 组成' avatar: label: 头像 gravatar: Gravatar gravatar_text: 你可以更改图像在 custom: 自定义 custom_text: 你可以上传你的图片。 default: 系统 msg: 请上传头像 bio: label: 关于我 website: label: 网站 placeholder: "https://example.com" msg: 网址格式不正确 location: label: 位置 placeholder: "城市,国家" notification: heading: 邮件通知 turn_on: 开启 inbox: label: 收件箱通知 description: 你的提问有新的回答,评论,邀请回答和其他。 all_new_question: label: 所有新问题 description: 获取所有新问题的通知。每周最多有50个问题。 all_new_question_for_following_tags: label: 所有关注标签的新问题 description: 获取关注的标签下新问题通知。 account: heading: 账号 change_email_btn: 更改邮箱 change_pass_btn: 更改密码 change_email_info: >- 邮件已发送。请根据指引完成验证。 email: label: 电子邮件地址 new_email: label: 新的电子邮件地址 msg: 新邮箱不能为空。 pass: label: 当前密码 msg: 密码不能为空。 password_title: 密码 current_pass: label: 当前密码 msg: empty: 当前密码不能为空 length: 密码长度必须在 8 至 32 之间 different: 两次输入的密码不匹配 new_pass: label: 新密码 pass_confirm: label: 确认新密码 interface: heading: 界面 lang: label: 界面语言 text: 设置用户界面语言,在刷新页面后生效。 my_logins: title: 我的登录 label: 使用这些账户登录或注册本网站。 modal_title: 移除登录 modal_content: 你确定要从账户里移除该登录? modal_confirm_btn: 移除 remove_success: 移除成功 toast: update: 更新成功 update_password: 密码更新成功。 flag_success: 感谢标记。 forbidden_operate_self: 禁止对自己执行操作 review: 您的修订将在审阅通过后显示。 sent_success: 发送成功 related_question: title: 相似 answers: 个回答 linked_question: title: 关联 description: 帖子关联到 no_linked_question: 没有与之关联的贴子。 invite_to_answer: title: 受邀人 desc: 邀请你认为可能知道答案的人。 invite: 邀请回答 add: 添加人员 search: 搜索人员 question_detail: action: 操作 created: 创建于 Asked: 提问于 asked: 提问于 update: 修改于 Edited: 编辑于 edit: 编辑于 commented: 评论 Views: 阅读次数 Follow: 关注此问题 Following: 已关注 follow_tip: 关注此问题以接收通知 answered: 回答于 closed_in: 关闭于 show_exist: 查看类似问题。 useful: 有用的 question_useful: 它是有用和明确的 question_un_useful: 它不明确或没用的 question_bookmark: 收藏该问题 answer_useful: 这是有用的 answer_un_useful: 它是没有用的 answers: title: 个回答 score: 评分 newest: 最新 oldest: 最旧 btn_accept: 采纳 btn_accepted: 已被采纳 write_answer: title: 你的回答 edit_answer: 编辑我的回答 btn_name: 提交你的回答 add_another_answer: 添加另一个回答 confirm_title: 继续回答 continue: 继续 confirm_info: >-

你确定要提交一个新的回答吗?

作为替代,你可以通过编辑来完善和改进之前的回答。

empty: 回答内容不能为空。 characters: 内容长度至少 6 个字符。 tips: header_1: 感谢你的回答 li1_1: 请务必确定在 回答问题。提供详细信息并分享你的研究。 li1_2: 用参考资料或个人经历来支持你所做的任何陈述。 header_2: 但是 请避免... li2_1: 请求帮助,寻求澄清,或答复其他答案。 reopen: confirm_btn: 重新打开 title: 重新打开这个帖子 content: 确定要重新打开吗? list: confirm_btn: 列表显示 title: 列表中显示这个帖子 content: 确定要列表中显示这个帖子吗? unlist: confirm_btn: 列表隐藏 title: 从列表中隐藏这个帖子 content: 确定要从列表中隐藏这个帖子吗? pin: title: 置顶该帖子 content: 你确定要全局置顶吗?这个帖子将出现在所有帖子列表的顶部。 confirm_btn: 置顶 delete: title: 删除 question: >- 我们不建议 删除有回答的帖子。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? answer_accepted: >-

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? other: 你确定要删除? tip_answer_deleted: 该回答已被删除 undelete_title: 撤销删除本帖 undelete_desc: 你确定你要撤销删除吗? btns: confirm: 确认 cancel: 取消 edit: 编辑 save: 保存 delete: 删除 undelete: 撤消删除 list: 列表显示 unlist: 列表隐藏 unlisted: 已隐藏 login: 登录 signup: 注册 logout: 退出 verify: 验证 create: 创建 approve: 批准 reject: 拒绝 skip: 跳过 discard_draft: 丢弃草稿 pinned: 已置顶 all: 全部 question: 问题 answer: 回答 comment: 评论 refresh: 刷新 resend: 重新发送 deactivate: 取消激活 active: 激活 suspend: 封禁 unsuspend: 解禁 close: 关闭 reopen: 重新打开 ok: 确定 light: 浅色 dark: 深色 system_setting: 跟随系统 default: 默认 reset: 重置 tag: 标签 post_lowercase: 帖子 filter: 筛选 ignore: 忽略 submit: 提交 normal: 正常 closed: 已关闭 deleted: 已删除 deleted_permanently: 永久删除 pending: 等待处理 more: 更多 view: 浏览量 card: 卡片 compact: 紧凑 display_below: 在下方显示 always_display: 总是显示 or: 或者 back_sites: 返回网站 search: title: 搜索结果 keywords: 关键词 options: 选项 follow: 关注 following: 已关注 counts: "{{count}} 个结果" counts_loading: "... 个结果" more: 更多 sort_btns: relevance: 相关性 newest: 最新的 active: 活跃的 score: 评分 more: 更多 tips: title: 高级搜索提示 tag: "<1>[tag] 在指定标签中搜索" user: "<1>user:username 根据作者搜索" answer: "<1>answers:0 搜索未回答的问题" score: "<1>score:3 评分 3+ 的帖子" question: "<1>is:question 搜索问题" is_answer: "<1>is:answer 搜索回答" empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 share: name: 分享 copy: 复制链接 via: 分享到... copied: 已复制 facebook: 分享到 Facebook twitter: 分享到 X cannot_vote_for_self: 你不能给自己的帖子投票。 modal_confirm: title: 发生错误... delete_permanently: title: 永久删除 content: 您确定要永久删除吗? account_result: success: 你的账号已通过验证,即将返回首页。 link: 返回首页 oops: 糟糕! invalid: 您使用的链接不再有效。 confirm_new_email: 你的电子邮箱已更新 confirm_new_email_invalid: >- 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? unsubscribe: page_title: 退订 success_title: 退订成功 success_desc: 您已成功退订,并且将不会再收到我们的邮件。 link: 更改设置 question: following_tags: 已关注的标签 edit: 编辑 save: 保存 follow_tag_tip: 关注标签来筛选你的问题列表。 hot_questions: 热门问题 all_questions: 全部问题 x_questions: "{{ count }} 个问题" x_answers: "{{ count }} 个回答" x_posts: "{{ count }} 个帖子" questions: 问题 answers: 回答 newest: 最新 active: 活跃 hot: 热门 frequent: 频繁的 recommend: 推荐 score: 评分 unanswered: 未回答 modified: 更新于 answered: 回答于 asked: 提问于 closed: 已关闭 follow_a_tag: 关注一个标签 more: 更多 personal: overview: 概览 answers: 回答 answer: 回答 questions: 问题 question: 问题 bookmarks: 收藏 reputation: 声望 comments: 评论 votes: 得票 badges: 徽章 newest: 最新 score: 评分 edit_profile: 编辑资料 visited_x_days: "已访问 {{ count }} 天" viewed: 浏览次数 joined: 加入于 comma: "," last_login: 上次登录 about_me: 关于我 about_me_empty: "// Hello, World!" top_answers: 高分回答 top_questions: 高分问题 stats: 状态 list_empty: 没有找到相关的内容。
试试看其他选项卡? content_empty: 未找到帖子。 accepted: 已采纳 answered: 回答于 asked: 提问于 downvoted: 点踩 mod_short: 版主 mod_long: 版主 x_reputation: 声望 x_votes: 得票 x_answers: 个回答 x_questions: 个问题 recent_badges: 最近的徽章 install: title: 安装 next: 下一步 done: 完成 config_yaml_error: 无法创建 config.yaml 文件。 lang: label: 请选择一种语言 db_type: label: 数据库引擎 db_username: label: 用户名 placeholder: root msg: 用户名不能为空 db_password: label: 密码 placeholder: root msg: 密码不能为空 db_host: label: 数据库主机 placeholder: "db:3306" msg: 数据库地址不能为空 db_name: label: 数据库名 placeholder: 回答 msg: 数据库名称不能为空。 db_file: label: 数据库文件 placeholder: /data/answer.db msg: 数据库文件不能为空。 ssl_enabled: label: 启用 SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL 模式 ssl_root_cert: placeholder: sslrootcert文件路径 msg: sslrootcert 文件的路径不能为空 ssl_cert: placeholder: sslcert文件路径 msg: sslcert 文件的路径不能为空 ssl_key: placeholder: sslkey 文件路径 msg: sslcert 文件的路径不能为空 config_yaml: title: 创建 config.yaml label: 已创建 config.yaml 文件。 desc: >- 你可以手动在 <1>/var/wwww/xxx/ 目录中创建 <1>config.yaml 文件并粘贴以下文本。 info: 完成后,点击“下一步”按钮。 site_information: 站点信息 admin_account: 管理员账号 site_name: label: 站点名称 msg: 站点名称不能为空。 msg_max_length: 站点名称长度不得超过 30 个字符。 site_url: label: 网站网址 text: 此网站的网址。 msg: empty: 网址不能为空。 incorrect: 网址格式不正确。 max_length: 网址长度不得超过 512 个字符。 contact_email: label: 联系邮箱 text: 负责本网站的主要联系人的电子邮件地址。 msg: empty: 联系人邮箱不能为空。 incorrect: 联系人邮箱地址不正确。 login_required: label: 私有的 switch: 需要登录 text: 只有登录用户才能访问这个社区。 admin_name: label: 名字 msg: 名字不能为空。 character: '只能由 "a-z", "0-9", " - . _" 组成' msg_max_length: 名称长度必须在 2 至 30 个字符之间。 admin_password: label: 密码 text: >- 您需要此密码才能登录。请将其存储在一个安全的位置。 msg: 密码不能为空。 msg_min_length: 密码必须至少 8 个字符长。 msg_max_length: 密码长度不能超过 32 个字符。 admin_confirm_password: label: "确认密码" text: "请重新输入您的密码以确认。" msg: "确认密码不一致。" admin_email: label: 邮箱 text: 您需要此电子邮件才能登录。 msg: empty: 邮箱不能为空。 incorrect: 邮箱格式不正确。 ready_title: 您的网站已准备好 ready_desc: >- 如果你想改变更多的设置,请访问 <1>管理区域;在网站菜单中找到它。 good_luck: "玩得愉快,祝你好运!" warn_title: 警告 warn_desc: >- 文件 <1>config.yaml 已存在。如果你要重置该文件中的任何配置项,请先删除它。 install_now: 您可以尝试 <1>现在安装。 installed: 已安裝 installed_desc: >- 你似乎已经安装过了。如果要重新安装,请先清除旧的数据库表。 db_failed: 数据连接异常! db_failed_desc: >- 这或者意味着数据库信息在 <1>config.yaml 文件不正确,或者无法与数据库服务器建立联系。这可能意味着你的主机数据库服务器故障。 counts: views: 次浏览 votes: 个点赞 answers: 个回答 accepted: 已被采纳 page_error: http_error: HTTP 错误 {{ code }} desc_403: 您无权访问此页面。 desc_404: 很抱歉,此页面不存在。 desc_50X: 服务器遇到了一个错误,无法完成你的请求。 back_home: 返回首页 page_maintenance: desc: "我们正在进行维护,我们将很快回来。" nav_menus: dashboard: 后台管理 contents: 内容管理 questions: 问题 answers: 回答 users: 用户管理 badges: 徽章 flags: 举报管理 settings: 站点设置 general: 一般 interface: 界面 smtp: SMTP branding: 品牌 legal: 法律条款 write: 撰写 terms: 服务条款 tos: 服务条款 privacy: 隐私政策 seo: SEO customize: 自定义 themes: 主题 login: 登录 privileges: 特权 plugins: 插件 installed_plugins: 已安装插件 apperance: 外观 community: 社区 advanced: 高级选项 tags: 标签 rules: 规则 policies: 政策 security: 安全 files: 文件 apikeys: API 密钥 intelligence: 智力 ai_assistant: AI 助手 ai_settings: AI 设置 mcp: MCP website_welcome: 欢迎来到 {{site_name}} user_center: login: 登录 qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码并登录。 login_failed_email_tip: 登录失败,请允许此应用访问您的邮箱信息,然后重试。 badges: modal: title: 恭喜 content: 你赢得了一个新徽章。 close: 关闭 confirm: 查看徽章 title: 徽章 awarded: 授予 earned_×: 以获得 ×{{ number }} ×_awarded: "{{ number }} 得到" can_earn_multiple: 你可以多次获得 earned: 获得 admin: admin_header: title: 后台管理 dashboard: title: 后台管理 welcome: 欢迎来到管理后台! site_statistics: 站点统计 questions: "问题:" resolved: "已解决:" unanswered: "未回答:" answers: "回答:" comments: "评论:" votes: "投票:" users: "用户:" flags: "举报:" reviews: "审查:" site_health: 网站健康 version: "版本" https: "HTTPS:" upload_folder: "上传文件夹:" run_mode: "运行模式:" private: 私有 public: 公开 smtp: "SMTP:" timezone: "时区:" system_info: 系统信息 go_version: "Go版本:" database: "数据库:" database_size: "数据库大小:" storage_used: "已用存储空间:" uptime: "运行时间:" links: 链接 plugins: 插件 github: GitHub blog: 博客 contact: 联系 forum: 论坛 documents: 文档 feedback: 用户反馈 support: 帮助 review: 审查 config: 配置 update_to: 更新到 latest: 最新版本 check_failed: 校验失败 "yes": "是" "no": "否" not_allowed: 拒绝 allowed: 允许 enabled: 已启用 disabled: 停用 writable: 可写 not_writable: 不可写 flags: title: 举报 pending: 等待处理 completed: 已完成 flagged: 被举报内容 flagged_type: 标记了 {{ type }} created: 创建于 action: 操作 review: 审查 user_role_modal: title: 更改用户状态为... btn_cancel: 取消 btn_submit: 提交 new_password_modal: title: 设置新密码 form: fields: password: label: 密码 text: 用户将被退出,需要再次登录。 msg: 密码的长度必须是8-32个字符。 btn_cancel: 取消 btn_submit: 提交 edit_profile_modal: title: 编辑资料 form: fields: display_name: label: 显示名称 msg_range: 显示名称长度必须为 2-30 个字符。 username: label: 用户名 msg_range: 用户名长度必须为 2-30 个字符。 email: label: 电子邮件地址 msg_invalid: 无效的邮箱地址 edit_success: 修改成功 btn_cancel: 取消 btn_submit: 提交 user_modal: title: 添加新用户 form: fields: users: label: 批量添加用户 placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: 用逗号分隔“name, email, password”,每行一个用户。 msg: "请输入用户的邮箱,每行一个。" display_name: label: 显示名称 msg: 显示名称长度必须为 2-30 个字符 email: label: 邮箱 msg: 邮箱无效。 password: label: 密码 msg: 密码的长度必须是8-32个字符。 btn_cancel: 取消 btn_submit: 提交 users: title: 用户 name: 名称 email: 邮箱 reputation: 声望 created_at: 创建时间 delete_at: 删除时间 suspend_at: 封禁时间 suspend_until: 封禁到期 status: 状态 role: 角色 action: 操作 change: 更改 all: 全部 staff: 工作人员 more: 更多 inactive: 不活跃 suspended: 已封禁 deleted: 已删除 normal: 正常 Moderator: 版主 Admin: 管理员 User: 用户 filter: placeholder: "按名称筛选,用户:id" set_new_password: 设置新密码 edit_profile: 编辑资料 change_status: 更改状态 change_role: 更改角色 show_logs: 显示日志 add_user: 添加用户 deactivate_user: title: 停用用户 content: 未激活的用户必须重新验证他们的邮箱。 delete_user: title: 删除此用户 content: 确定要删除此用户?此操作无法撤销! remove: 移除内容 label: 删除所有问题、 答案、 评论等 text: 如果你只想删除用户账户,请不要选中此项。 suspend_user: title: 挂起此用户 content: 被封禁的用户将无法登录。 label: 用户将被封禁多长时间? forever: 永久 questions: page_title: 问题 unlisted: 已隐藏 post: 标题 votes: 得票数 answers: 回答数 created: 创建于 status: 状态 action: 操作 change: 更改 pending: 等待处理 filter: placeholder: "按标题过滤,问题:id" answers: page_title: 回答 post: 标题 votes: 得票数 created: 创建于 status: 状态 action: 操作 change: 更改 filter: placeholder: "按标题筛选,答案:id" general: page_title: 一般 name: label: 站点名称 msg: 不能为空 text: "站点的名称,作为站点的标题。" site_url: label: 网站网址 msg: 网站网址不能为空。 validate: 请输入一个有效的 URL。 text: 此网站的地址。 short_desc: label: 简短站点描述 msg: 简短网站描述不能为空。 text: "简短的标语,作为网站主页的标题(Html 的 title 标签)。" desc: label: 站点描述 msg: 网站描述不能为空。 text: "使用一句话描述本站,作为网站的描述(Html 的 meta 标签)。" contact_email: label: 联系邮箱 msg: 联系人邮箱不能为空。 validate: 联系人邮箱无效。 text: 本网站的主要联系邮箱地址。 check_update: label: 软件更新 text: 自动检查软件更新 interface: page_title: 界面 language: label: 界面语言 msg: 不能为空 text: 设置用户界面语言,在刷新页面后生效。 time_zone: label: 时区 msg: 时区不能为空。 text: 选择一个与您相同时区的城市。 avatar: label: 默认头像 text: 没有自定义头像的用户。 gravatar_base_url: label: Gravatar 根路径 URL text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。 smtp: page_title: SMTP from_email: label: 发件人邮箱 msg: 发件人邮箱不能为空。 text: 用于发送邮件的地址。 from_name: label: 发件人 msg: 不能为空 text: 发件人的名字。 smtp_host: label: SMTP 主机 msg: 不能为空 text: 邮件服务器 encryption: label: 加密 msg: 不能为空 text: 对于大多数服务器而言,SSL 是推荐开启的。 ssl: SSL tls: TLS none: 无加密 smtp_port: label: SMTP 端口 msg: SMTP 端口必须在 1 ~ 65535 之间。 text: 邮件服务器的端口号。 smtp_username: label: SMTP 用户名 msg: 不能为空 smtp_password: label: SMTP 密码 msg: 不能为空 test_email_recipient: label: 测试收件邮箱 text: 提供用于接收测试邮件的邮箱地址。 msg: 测试收件邮箱无效 smtp_authentication: label: 启用身份验证 title: SMTP 身份验证 msg: 不能为空 "yes": "是" "no": "否" branding: page_title: 品牌 logo: label: 网站标志(Logo) msg: 图标不能为空。 text: 在你的网站左上方的Logo图标。使用一个高度为56,长宽比大于3:1的宽长方形图像。如果留空,将显示网站标题文本。 mobile_logo: label: 移动端 Logo text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空,将使用 "Logo"设置中的图像。 square_icon: label: 方形图标 msg: 方形图标不能为空。 text: 用作元数据图标的基础的图像。最好是大于512x512。 favicon: label: 收藏夹图标 text: 网站的图标。要在 CDN 正常工作,它必须是 png。 将调整大小到32x32。如果留空,将使用“方形图标”。 legal: page_title: 法律条款 terms_of_service: label: 服务条款 text: "您可以在此添加服务内容的条款。如果您已经在别处托管了文档,请在这里提供完整的URL。" privacy_policy: label: 隐私政策 text: "您可以在此添加隐私政策内容。如果您已经在别处托管了文档,请在这里提供完整的URL。" external_content_display: label: 外部内容 text: "内容包括从外部网站嵌入的图像、视频和媒体。" always_display: 总是显示外部内容 ask_before_display: 在显示外部内容之前询问 write: page_title: 文件 min_content: label: 最小问题长度 text: 最小允许的问题内容长度(字符)。 restrict_answer: title: 回答编辑 label: 每个用户对于每个问题只能有一个回答 text: "用户可以使用编辑按钮优化已有的回答" min_tags: label: "问题的最少标签数" text: "一个问题所需标签的最小数量。" recommend_tags: label: 推荐标签 text: "推荐标签将默认显示在下拉列表中。" msg: contain_reserved: "推荐标签不能包含保留标签" required_tag: title: 设置必填标签 label: 设置“推荐标签”为必需的标签 text: "每个新问题必须至少有一个推荐标签。" reserved_tags: label: 保留标签 text: "只有版主才能使用保留的标签。" image_size: label: 最大图像大小 (MB) text: "最大图像上传大小." attachment_size: label: 最大附件大小 (MB) text: "最大附件文件上传大小。" image_megapixels: label: 最大图像兆像素 text: "允许图像的最大兆位数。" image_extensions: label: 允许的图像后缀 text: "允许图像显示的文件扩展名的列表,用英文逗号分隔。" attachment_extensions: label: 允许的附件后缀 text: "允许上传的文件扩展名列表与英文逗号分开。警告:允许上传可能会导致安全问题。" seo: page_title: 搜索引擎优化 permalink: label: 固定链接 text: 自定义URL结构可以提高可用性,以及你的链接的向前兼容性。 robots: label: robots.txt text: 这将永久覆盖任何相关的网站设置。 themes: page_title: 主题 themes: label: 主题 text: 选择一个现有主题。 color_scheme: label: 配色方案 navbar_style: label: 导航栏背景样式 primary_color: label: 主色调 text: 修改您主题使用的颜色 layout: label: 布局 full_width: 全宽度 fixed_width: 固定宽度 css_and_html: page_title: CSS 与 HTML custom_css: label: 自定义 CSS text: > head: label: 头部 text: > header: label: 页眉 text: > footer: label: 页脚 text: 这将在 </body> 之前插入。 sidebar: label: 侧边栏 text: 这将插入侧边栏中。 login: page_title: 登录 membership: title: 会员 label: 允许新注册 text: 关闭以防止任何人创建新账户。 email_registration: title: 邮箱注册 label: 允许邮箱注册 text: 关闭以阻止任何人通过邮箱创建新账户。 allowed_email_domains: title: 允许的邮箱域 text: 允许注册账户的邮箱域。每行一个域名。留空时忽略。 private: title: 非公开的 label: 需要登录 text: 只有登录用户才能访问这个社区。 password_login: title: 密码登录 label: 允许使用邮箱和密码登录 text: "警告:如果您未配置过其他登录方式,关闭密码登录后您则可能无法登录。" installed_plugins: title: 已安装插件 plugin_link: 插件扩展功能。您可以在<1>插件仓库中找到插件。 filter: all: 全部 active: 已启用 inactive: 未启用 outdated: 已过期 plugins: label: 插件 text: 选择一个现有的插件。 name: 名称 version: 版本 status: 状态 action: 操作 deactivate: 停用 activate: 启用 settings: 设置 settings_users: title: 用户 avatar: label: 默认头像 text: 没有自定义头像的用户。 gravatar_base_url: label: Gravatar 根路径 URL text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。 profile_editable: title: 个人资料可编辑 allow_update_display_name: label: 允许用户修改显示名称 allow_update_username: label: 允许用户修改用户名 allow_update_avatar: label: 允许用户修改个人头像 allow_update_bio: label: 允许用户修改个人介绍 allow_update_website: label: 允许用户修改个人主页网址 allow_update_location: label: 允许用户更改位置 privilege: title: 特权 level: label: 级别所需声望 text: 选择特权所需的声望值 msg: should_be_number: 输入必须是数字 number_larger_1: 数字应该大于等于 1 badges: action: 操作 active: 活跃的 activate: 启用 all: 全部 awards: 奖项 deactivate: 取消激活 filter: placeholder: 按名称筛选,或使用 badge:id group: 组 inactive: 未启用 name: 名字 show_logs: 显示日志 status: 状态 title: 徽章 apikeys: title: API 密钥 add_api_key: 添加 API 密钥 desc: 描述 scope: 范围 key: 密钥 created: 创建于 last_used: 最后使用 add_or_edit_modal: add_title: 添加 API 密钥 edit_title: 编辑 API 密钥 description: 描述 description_required: 请输入描述。 scope: 范围 global: 全局 read-only: 只读 created_modal: title: 已创建API密钥 api_key: API 密钥 description: 此密钥将不会再次显示。请确保您在继续之前拿到一份副本。 delete_modal: title: 删除API密钥 content: 任何使用此密钥的应用程序或脚本都将无法访问API。这是永久性的! ai_settings: enabled: label: AI 已启用 check: 启用AI功能 text: AI 模型必须正确配置才能使用。 provider: label: 提供商 api_host: label: API 主机 msg: API 主机是必需的 api_key: label: API 密钥 check: 检查 check_success: "连接成功。" msg: API 密钥是必填项 model: label: 模型 msg: 模型是必需的 add_success: AI 设置更新成功。 conversations: topic: 主题 helpful: 有帮助 unhelpful: 没有帮助 created: 创建于 action: 操作 empty: 没有找到会话 delete_modal: title: 删除对话 content: 您确定要删除此对话吗?这是永久性的! delete_success: 对话删除成功。 mcp: mcp_server: label: MCP服务器 switch: 已启用 type: label: 类型 url: label: 链接 http_header: label: HTTP Header text: 请将 {key} 替换为 API 密钥。 form: optional: (选填) empty: 不能为空 invalid: 是无效的 btn_submit: 保存 not_found_props: "所需属性 {{ key }} 未找到。" select: 选择 page_review: review: 评论 proposed: 提案 question_edit: 问题编辑 answer_edit: 回答编辑 tag_edit: '标签管理: 编辑标签' edit_summary: 编辑备注 edit_question: 编辑问题 edit_answer: 编辑回答 edit_tag: 编辑标签 empty: 没有剩余的审核任务。 approve_revision_tip: 您是否批准此修订? approve_flag_tip: 您是否批准此举报? approve_post_tip: 您是否批准此帖子? approve_user_tip: 您是否批准此修订? suggest_edits: 建议的编辑 flag_post: 举报帖子 flag_user: 举报用户 queued_post: 排队的帖子 queued_user: 排队用户 filter_label: 类型 reputation: 声望值 flag_post_type: 举报这个帖子的类型是 {{ type }} flag_user_type: 举报这个用户的类型是 {{ type }} edit_post: 编辑帖子 list_post: 文章列表 unlist_post: 隐藏的帖子 timeline: undeleted: 取消删除 deleted: 删除 downvote: 反对 upvote: 点赞 accept: 采纳 cancelled: 已取消 commented: '评论:' rollback: 回滚 edited: 最后编辑于 answered: 回答于 asked: 提问于 closed: 关闭 reopened: 重新开启 created: 创建于 pin: 已置顶 unpin: 取消置頂 show: 已显示 hide: 已隐藏 title: "历史记录" tag_title: "时间线" show_votes: "显示投票" n_or_a: N/A title_for_question: "时间线" title_for_answer: "{{ title }} 的 {{ author }} 回答时间线" title_for_tag: "时间线" datetime: 日期时间 type: 类型 by: 由 comment: 评论 no_data: "空空如也" users: title: 用户 users_with_the_most_reputation: 本周声望最高的用户 users_with_the_most_vote: 本周投票最多的用户 staffs: 我们的社区工作人员 reputation: 声望值 votes: 投票 prompt: leave_page: 确定要离开此页面? changes_not_save: 您的更改尚未保存 draft: discard_confirm: 您确定要丢弃您的草稿吗? messages: post_deleted: 该帖子已被删除。 post_cancel_deleted: 此帖子已被删除 post_pin: 该帖子已被置顶。 post_unpin: 该帖子已被取消置顶。 post_hide_list: 此帖子已经从列表中隐藏。 post_show_list: 该帖子已显示到列表中。 post_reopen: 这个帖子已被重新打开. post_list: 这个帖子已经被显示 post_unlist: 这个帖子已经被隐藏 post_pending: 您的帖子正在等待审核。它将在它获得批准后可见。 post_closed: 此帖已关闭。 answer_deleted: 该回答已被删除. answer_cancel_deleted: 此答案已取消删除。 change_user_role: 此用户的角色已被更改。 user_inactive: 此用户已经处于未激活状态。 user_normal: 此用户已经是正常的。 user_suspended: 此用户已被封禁。 user_deleted: 此用户已被删除 user_added: 用户添加成功。 badge_activated: 此徽章已被激活。 badge_inactivated: 此徽章已被禁用。 users_deleted: 这些用户已被删除。 posts_deleted: 这些问题已被删除。 answers_deleted: 这些答案已被删除。 copy: 复制到剪贴板 copied: 已复制 external_content_warning: 外部图像/媒体未显示。 ================================================ FILE: i18n/zh_TW.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The following fields are used for back-end backend: base: success: other: 成功。 unknown: other: 未知的錯誤。 request_format_error: other: 要求格式錯誤。 unauthorized_error: other: 未授權。 database_error: other: 資料伺服器錯誤。 forbidden_error: other: 已拒絕存取。 duplicate_request_error: other: 重複送出。 action: report: other: 檢舉 edit: other: 編輯 delete: other: 删除 close: other: 關閉 reopen: other: 再次開啟。 forbidden_error: other: 已拒絕存取。 pin: other: 置頂 hide: other: 不公開 unpin: other: 取消置頂 show: other: 清單 invite_someone_to_answer: other: 編輯 undelete: other: 還原 merge: other: 合併 role: name: user: other: 使用者 admin: other: 管理員 moderator: other: 版主 description: user: other: 預設沒有特別閱讀權限。 admin: other: 擁有存取此網站的全部權限。 moderator: other: 可以存取除了管理員設定以外的所有貼文。 privilege: level_1: description: other: Level 1 (less reputation required for private team, group) level_2: description: other: Level 2 (low reputation required for startup community) level_3: description: other: Level 3 (high reputation required for mature community) level_custom: description: other: Custom Level rank_question_add_label: other: Ask question rank_answer_add_label: other: Write answer rank_comment_add_label: other: 寫留言 rank_report_add_label: other: Flag rank_comment_vote_up_label: other: Upvote comment rank_link_url_limit_label: other: Post more than 2 links at a time rank_question_vote_up_label: other: Upvote question rank_answer_vote_up_label: other: Upvote answer rank_question_vote_down_label: other: Downvote question rank_answer_vote_down_label: other: Downvote answer rank_invite_someone_to_answer_label: other: Invite someone to answer rank_tag_add_label: other: Create new tag rank_tag_edit_label: other: Edit tag description (need to review) rank_question_edit_label: other: Edit other's question (need to review) rank_answer_edit_label: other: Edit other's answer (need to review) rank_question_edit_without_review_label: other: Edit other's question without review rank_answer_edit_without_review_label: other: Edit other's answer without review rank_question_audit_label: other: Review question edits rank_answer_audit_label: other: Review answer edits rank_tag_audit_label: other: Review tag edits rank_tag_edit_without_review_label: other: Edit tag description without review rank_tag_synonym_label: other: Manage tag synonyms email: other: 電子郵件 e_mail: other: 電子郵件 password: other: 密碼 pass: other: 密碼 old_pass: other: 目前密碼 original_text: other: 此貼文 email_or_password_wrong_error: other: 電郵和密碼不匹配。 error: common: invalid_url: other: URL 無效。 status_invalid: other: 無效狀態。 password: space_invalid: other: 密碼不能包含空白字元。 admin: cannot_update_their_password: other: 你不能修改自己的密码。 cannot_edit_their_profile: other: You cannot modify your profile. cannot_modify_self_status: other: You cannot modify your status. email_or_password_wrong: other: 電郵和密碼不匹配。 answer: not_found: other: 未發現答案。 cannot_deleted: other: 沒有刪除權限。 cannot_update: other: 沒有更新權限。 question_closed_cannot_add: other: Questions are closed and cannot be added. content_cannot_empty: other: Answer content cannot be empty. comment: edit_without_permission: other: 不允許編輯留言。 not_found: other: 未發現留言。 cannot_edit_after_deadline: other: 這則留言時間過久,無法修改。 content_cannot_empty: other: Comment content cannot be empty. email: duplicate: other: 這個電子郵件地址已被使用。 need_to_be_verified: other: 需驗證電子郵件地址。 verify_url_expired: other: 電子郵件地址驗證網址已過期,請重寄電子郵件。 illegal_email_domain_error: other: Email is not allowed from that email domain. Please use another one. lang: not_found: other: 未找到語言檔。 object: captcha_verification_failed: other: 驗證碼錯誤。 disallow_follow: other: 你不被允許追蹤。 disallow_vote: other: 你無法投票。 disallow_vote_your_self: other: 你不能為自己的貼文投票。 not_found: other: 找不到物件。 verification_failed: other: 驗證失敗。 email_or_password_incorrect: other: 電子郵件地址和密碼不匹配。 old_password_verification_failed: other: 舊密碼驗證失敗 new_password_same_as_previous_setting: other: 新密碼與先前的一樣。 already_deleted: other: 這則貼文已被刪除。 meta: object_not_found: other: Meta object not found question: already_deleted: other: This post has been deleted. under_review: other: Your post is awaiting review. It will be visible after it has been approved. not_found: other: 找不到問題。 cannot_deleted: other: 無刪除權限。 cannot_close: other: 無關閉權限。 cannot_update: other: 無更新權限。 content_cannot_empty: other: Content cannot be empty. content_less_than_minimum: other: Not enough content entered. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. vote_fail_to_meet_the_condition: other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. no_enough_rank_to_operate: other: You need at least {{.Rank}} reputation to do this. report: handle_failed: other: 報告處理失敗。 not_found: other: 找不到報告。 tag: already_exist: other: Tag already exists. not_found: other: 找不到標籤。 recommend_tag_not_found: other: Recommend tag is not exist. recommend_tag_enter: other: 請輸入至少一個必需的標籤。 not_contain_synonym_tags: other: 不應包含同義詞標籤。 cannot_update: other: 沒有權限更新。 is_used_cannot_delete: other: You cannot delete a tag that is in use. cannot_set_synonym_as_itself: other: 你不能將目前標籤的同義詞設定為本身。 minimum_count: other: Not enough tags were entered. smtp: config_from_name_cannot_be_email: other: The from name cannot be a email address. theme: not_found: other: 未找到主題。 revision: review_underway: other: 目前無法編輯,有一個版本在審查佇列中。 no_permission: other: No permission to revise. user: external_login_missing_user_id: other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. external_login_unbinding_forbidden: other: Please set a login password for your account before you remove this login. email_or_password_wrong: other: other: 電子郵箱和密碼不匹配。 not_found: other: 未找到使用者。 suspended: other: 該使用者已被停權。 username_invalid: other: 使用者名稱無效。 username_duplicate: other: 使用者名稱已被使用。 set_avatar: other: 大頭照設定錯誤。 cannot_update_your_role: other: 您不能修改自己的角色。 not_allowed_registration: other: Currently the site is not open for registration. not_allowed_login_via_password: other: Currently the site is not allowed to login via password. access_denied: other: Access denied page_access_denied: other: You do not have access to this page. add_bulk_users_format_error: other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" add_bulk_users_amount_error: other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." status_suspended_forever: other: "This user was suspended forever. This user doesn't meet a community guideline." status_suspended_until: other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." status_deleted: other: "This user was deleted." status_inactive: other: "This user is inactive." config: read_config_failed: other: 讀取組態失敗 database: connection_failed: other: 資料庫連線失敗 create_table_failed: other: 表建立失敗 install: create_config_failed: other: 無法建立 config.yaml 檔。 upload: unsupported_file_format: other: 不支援的檔案格式。 site_info: config_not_found: other: Site config not found. badge: object_not_found: other: Badge object not found reason: spam: name: other: 垃圾訊息 desc: other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. rude_or_abusive: name: other: rude or abusive desc: other: "A reasonable person would find this content inappropriate for respectful discourse." a_duplicate: name: other: a duplicate desc: other: This question has been asked before and already has an answer. placeholder: other: Enter the existing question link not_a_answer: name: other: not an answer desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." no_longer_needed: name: other: no longer needed desc: other: This comment is outdated, conversational or not relevant to this post. something: name: other: something else desc: other: This post requires staff attention for another reason not listed above. placeholder: other: Let us know specifically what you are concerned about community_specific: name: other: a community-specific reason desc: other: This question doesn't meet a community guideline. not_clarity: name: other: needs details or clarity desc: other: This question currently includes multiple questions in one. It should focus on one problem only. looks_ok: name: other: looks OK desc: other: This post is good as-is and not low quality. needs_edit: name: other: needs edit, and I did it desc: other: Improve and correct problems with this post yourself. needs_close: name: other: 需關閉 desc: other: A closed question can't answer, but still can edit, vote and comment. needs_delete: name: other: needs delete desc: other: This post will be deleted. question: close: duplicate: name: other: 垃圾訊息 desc: other: 此問題以前就有人問過,而且已經有了答案。 guideline: name: other: 一个社群特定原因 desc: other: 此問題不符合社群準則。 multiple: name: other: 需要細節或明晰 desc: other: This question currently includes multiple questions in one. It should focus on one problem only. other: name: other: 其他原因 desc: other: 這個帖子需要上面沒有列出的另一個原因。 operation_type: asked: other: 提問於 answered: other: 回答於 modified: other: 修改於 deleted_title: other: Deleted question questions_title: other: Questions tag: tags_title: other: Tags no_description: other: The tag has no description. notification: action: update_question: other: 更新了問題 answer_the_question: other: 回答了問題 update_answer: other: 更新了答案 accept_answer: other: 已接受的回答 comment_question: other: 留言了問題 comment_answer: other: 留言了答案 reply_to_you: other: 回覆了你 mention_you: other: 提到了你 your_question_is_closed: other: 你的問題已被關閉 your_question_was_deleted: other: 你的問題已被刪除 your_answer_was_deleted: other: 你的答案已被刪除 your_comment_was_deleted: other: 你的留言已被刪除 up_voted_question: other: upvoted question down_voted_question: other: downvoted question up_voted_answer: other: upvoted answer down_voted_answer: other: downvoted answer up_voted_comment: other: upvoted comment invited_you_to_answer: other: invited you to answer earned_badge: other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: other: "[{{.SiteName}}] Confirm your new email address" body: other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." new_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" invited_you_to_answer: title: other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" new_question: title: other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" body: other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Password reset" body: other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." register: title: other: "[{{.SiteName}}] Confirm your new account" body: other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." test: title: other: "[{{.SiteName}}] Test Email" body: other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." action_activity_type: upvote: other: upvote upvoted: other: upvoted downvote: other: downvote downvoted: other: downvoted accept: other: 採納 accepted: other: 已採納 edit: other: 編輯 review: queued_post: other: Queued post flagged_post: other: Flagged post suggested_post_edit: other: Suggested edits reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: default_badges: autobiographer: name: other: Autobiographer desc: other: Filled out profile information. certified: name: other: Certified desc: other: Completed our new user tutorial. editor: name: other: 編輯者 desc: other: First post edit. first_flag: name: other: First Flag desc: other: First flagged a post. first_upvote: name: other: First Upvote desc: other: First up voted a post. first_link: name: other: 首個連結 desc: other: First added a link to another post. first_reaction: name: other: First Reaction desc: other: First reacted to the post. first_share: name: other: First Share desc: other: First shared a post. scholar: name: other: Scholar desc: other: Asked a question and accepted an answer. commentator: name: other: Commentator desc: other: Leave 5 comments. new_user_of_the_month: name: other: New User of the Month desc: other: Outstanding contributions in their first month. read_guidelines: name: other: Read Guidelines desc: other: Read the [community guidelines]. reader: name: other: 閱讀者 desc: other: Read every answers in a topic with more than 10 answers. welcome: name: other: 歡迎 desc: other: Received a up vote. nice_share: name: other: Nice Share desc: other: Shared a post with 25 unique visitors. good_share: name: other: Good Share desc: other: Shared a post with 300 unique visitors. great_share: name: other: Great Share desc: other: Shared a post with 1000 unique visitors. out_of_love: name: other: Out of Love desc: other: Used 50 up votes in a day. higher_love: name: other: Higher Love desc: other: Used 50 up votes in a day 5 times. crazy_in_love: name: other: Crazy in Love desc: other: Used 50 up votes in a day 20 times. promoter: name: other: Promoter desc: other: Invited a user. campaigner: name: other: Campaigner desc: other: Invited 3 basic users. champion: name: other: Champion desc: other: Invited 5 members. thank_you: name: other: 感謝 desc: other: Has 20 up voted posts and gave 10 up votes. gives_back: name: other: Gives Back desc: other: Has 100 up voted posts and gave 100 up votes. empathetic: name: other: Empathetic desc: other: Has 500 up voted posts and gave 1000 up votes. enthusiast: name: other: Enthusiast desc: other: Visited 10 consecutive days. aficionado: name: other: Aficionado desc: other: Visited 100 consecutive days. devotee: name: other: Devotee desc: other: Visited 365 consecutive days. anniversary: name: other: Anniversary desc: other: Active member for a year, posted at least once. appreciated: name: other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: name: other: Respected desc: other: Received 2 up votes on 100 posts. admired: name: other: Admired desc: other: Received 5 up votes on 300 posts. solved: name: other: Solved desc: other: Have an answer be accepted. guidance_counsellor: name: other: Guidance Counsellor desc: other: Have 10 answers be accepted. know_it_all: name: other: Know-it-All desc: other: Have 50 answers be accepted. solution_institution: name: other: Solution Institution desc: other: Have 150 answers be accepted. nice_answer: name: other: Nice Answer desc: other: Answer score of 10 or more. good_answer: name: other: Good Answer desc: other: Answer score of 25 or more. great_answer: name: other: Great Answer desc: other: Answer score of 50 or more. nice_question: name: other: Nice Question desc: other: Question score of 10 or more. good_question: name: other: Good Question desc: other: Question score of 25 or more. great_question: name: other: Great Question desc: other: Question score of 50 or more. popular_question: name: other: Popular Question desc: other: Question with 500 views. notable_question: name: other: Notable Question desc: other: Question with 1,000 views. famous_question: name: other: Famous Question desc: other: Question with 5,000 views. popular_link: name: other: Popular Link desc: other: Posted an external link with 50 clicks. hot_link: name: other: Hot Link desc: other: Posted an external link with 300 clicks. famous_link: name: other: Famous Link desc: other: Posted an external link with 100 clicks. default_badge_groups: getting_started: name: other: Getting Started community: name: other: Community posting: name: other: Posting # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: 如何設定文字格式 desc: >-
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: 上一頁 next: 下一頁 page_title: question: 問題 questions: 問題 tag: 標籤 tags: 標籤 tag_wiki: 標籤 wiki create_tag: Create Tag edit_tag: 編輯標籤 ask_a_question: Create Question edit_question: 編輯問題 edit_answer: 編輯回答 search: 搜尋 posts_containing: 包含的貼文 settings: 設定 notifications: 通知 login: 登入 sign_up: 註冊 account_recovery: 帳號恢復 account_activation: 帳號啟用 confirm_email: 確認電子郵件 account_suspended: 帳號已被停權 admin: 後台管理 change_email: 修改電子郵件 install: Answer 安裝 upgrade: Answer 升級 maintenance: 網站維護 users: 使用者 oauth_callback: Processing http_404: HTTP 錯誤 404 http_50X: HTTP 錯誤 500 http_403: HTTP 錯誤 403 logout: 登出 posts: Posts ai_assistant: AI Assistant ai_assistant: description: Got a question? Ask it and get answers, perspectives, and recommendations. recent_conversations: Recent Conversations show_more: Show more new: New chat ai_generate: AI-generated from posts and may not be accurate. copy: Copy ask_a_follow_up: Ask a follow-up ask_placeholder: Ask a question notifications: title: 通知 inbox: 收件夾 achievement: 成就 new_alerts: New alerts all_read: 全部標記為已讀 show_more: 顯示更多 someone: Someone inbox_type: all: 所有 posts: Posts invites: Invites votes: Votes answer: Answer question: Question badge_award: Badge suspended: title: 您的帳號已被停權 until_time: "你的帳號被停權至{{ time }}。" forever: 你的帳號已被永久停權。 end: 違反了我們的社群準則。 contact_us: Contact us editor: blockquote: text: 引用 bold: text: 粗體 chart: text: 圖表 flow_chart: 流程圖 sequence_diagram: 時序圖 class_diagram: 類圖 state_diagram: 狀態圖 entity_relationship_diagram: 實體關係圖 user_defined_diagram: 用戶自定義圖表 gantt_chart: 甘特圖 pie_chart: 圓餅圖 code: text: 代碼示例 add_code: 添加代碼示例 form: fields: code: label: 代碼塊 msg: empty: 代碼不能為空 language: label: 語言 placeholder: 自動偵測 btn_cancel: 取消 btn_confirm: 添加 formula: text: 公式 options: inline: 內聯公式 block: 公式塊 heading: text: 標題 options: h1: 標題 1 h2: 標題 2 h3: 標題 3 h4: 標題 4 h5: 標題 5 h6: 標題 6 help: text: 幫助 hr: text: Horizontal rule image: text: 圖片 add_image: 添加圖片 tab_image: 上傳圖片 form_image: fields: file: label: 圖檔 btn: 選擇圖片 msg: empty: 文件不能為空。 only_image: 只能上傳圖片文件。 max_size: File size cannot exceed {{size}} MB. desc: label: 圖片描述 tab_url: 圖片地址 form_url: fields: url: label: 圖片地址 msg: empty: 圖片地址不能為空 name: label: 圖片描述 btn_cancel: 取消 btn_confirm: 添加 uploading: 上傳中... indent: text: 增加縮排 outdent: text: 減少縮排 italic: text: 斜體 link: text: 超連結 add_link: 添加超連結 form: fields: url: label: 連結 msg: empty: 連結不能為空。 name: label: 描述 btn_cancel: 取消 btn_confirm: 添加 ordered_list: text: Numbered list unordered_list: text: Bulleted list table: text: 表格 heading: 表頭 cell: 單元格 file: text: Attach files not_supported: "Don’t support that file type. Try again with {{file_type}}." max_size: "Attach files size cannot exceed {{size}} MB." close_modal: title: 關閉原因是... btn_cancel: 取消 btn_submit: 提交 remark: empty: 不能為空。 msg: empty: 請選擇一個原因。 report_modal: flag_title: 報告為... close_title: 關閉原因是... review_question_title: 審核問題 review_answer_title: 審核回答 review_comment_title: 審核評論 btn_cancel: 取消 btn_submit: 提交 remark: empty: 不能為空 msg: empty: 請選擇一個原因。 not_a_url: URL format is incorrect. url_not_match: URL origin does not match the current website. tag_modal: title: 創建新標籤 form: fields: display_name: label: Display name msg: empty: 顯示名稱不能為空。 range: 顯示名稱不能超過 35 個字符。 slug_name: label: URL slug desc: URL slug up to 35 characters. msg: empty: URL 固定連結不能為空。 range: URL 固定連結不能超過 35 個字元。 character: URL 固定連結包含非法字元。 desc: label: 描述 revision: label: Revision edit_summary: label: Edit summary placeholder: >- Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) btn_cancel: 取消 btn_submit: 提交 btn_post: Post new tag tag_info: created_at: 創建於 edited_at: 編輯於 history: 歷史 synonyms: title: 同義詞 text: 以下標籤等同於 empty: 此標籤目前沒有同義詞。 btn_add: 添加同義詞 btn_edit: 編輯 btn_save: 儲存 synonyms_text: 以下標籤等同於 delete: title: 刪除標籤 tip_with_posts: >-

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

tip: 你確定要刪除嗎? close: 關閉 merge: title: Merge tag source_tag_title: Source tag source_tag_description: The source tag and its associated data will be remapped to the target tag. target_tag_title: Target tag target_tag_description: A synonym between these two tags will be created after merging. no_results: No tags matched btn_submit: 送出 btn_close: 關閉 edit_tag: title: 編輯標籤 default_reason: 編輯標籤 default_first_reason: Add tag btn_save_edits: 儲存更改 btn_cancel: 取消 dates: long_date: MM月DD日 long_date_with_year: "YYYY年MM月DD日" long_date_with_time: "YYYY 年 MM 月 DD 日 HH:mm" now: 剛剛 x_seconds_ago: "{{count}} 秒前" x_minutes_ago: "{{count}} 分鐘前" x_hours_ago: "{{count}} 小時前" hour: 小時 day: 天 hours: hours days: days month: month months: months year: year reaction: heart: heart smile: smile frown: frown btn_label: add or remove reactions undo_emoji: undo {{ emoji }} reaction react_emoji: react with {{ emoji }} unreact_emoji: unreact with {{ emoji }} comment: btn_add_comment: 添加評論 reply_to: 回復 btn_reply: 回復 btn_edit: 編輯 btn_delete: 刪除 btn_flag: 舉報 btn_save_edits: 保存 btn_cancel: 取消 show_more: "{{count}} 條剩餘評論" tip_question: >- 通过評論询问更多问题或提出改進建議。避免在評論中回答問題。 tip_answer: >- 使用評論回復其他用戶或通知他們进行更改。如果你要添加新的信息,請編輯你的帖子,而不是發表評論。 tip_vote: It adds something useful to the post edit_answer: title: 編輯回答 default_reason: 編輯回答 default_first_reason: Add answer form: fields: revision: label: 編輯歷史 answer: label: 回答內容 feedback: characters: 內容必須至少6個字元長度。 edit_summary: label: Edit summary placeholder: >- 簡單描述更改原因 (錯別字、文字表達、格式等等) btn_save_edits: 儲存更改 btn_cancel: 取消 tags: title: 標籤 sort_buttons: popular: 熱門 name: 名稱 newest: Newest button_follow: 關注 button_following: 已關注 tag_label: 個問題 search_placeholder: 通過標籤名過濾 no_desc: 此標籤無描述。 more: 更多 wiki: Wiki ask: title: Create Question edit_title: 編輯問題 default_reason: 編輯問題 default_first_reason: Create question similar_questions: 相似的問題 form: fields: revision: label: 編輯歷史 title: label: 標題 placeholder: What's your topic? Be specific. msg: empty: 標題不能為空 range: 標題最多 150 個字元 body: label: 正文 msg: empty: 正文不能爲空。 hint: optional_body: Describe what the question is about. minimum_characters: "Describe what the question is about, at least {{min_content_length}} characters are required." tags: label: 標籤 msg: empty: 標籤不能為空 answer: label: 回答內容 msg: empty: 回答內容不能為空 edit_summary: label: Edit summary placeholder: >- 簡單描述更改原因 (錯別字、文字表達、格式等等) btn_post_question: 提出問題 btn_save_edits: 儲存更改 answer_question: 回答您自己的問題 post_question&answer: 發布您的問題和答案 tag_selector: add_btn: 建立標籤 create_btn: 建立新標籤 search_tag: 搜尋標籤 hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: 沒有匹配的標籤 tag_required_text: 必填標籤 (至少一個) header: nav: question: 問題 tag: 標籤 user: 用戶 badges: Badges profile: 用戶主頁 setting: 帳號設置 logout: 登出 admin: 後台管理 review: 審查 bookmark: Bookmarks moderation: Moderation search: placeholder: 搜尋 footer: build_on: Powered by <1> Apache Answer upload_img: name: 更改 loading: 讀取中... pic_auth_code: title: 驗證碼 placeholder: 輸入上面的文字 msg: empty: 验证码不能為空 inactive: first: >- 就差一步!我們寄送了一封啟用電子郵件到 {{mail}}。請按照郵件中的說明啟用您的帳戶。 info: "如果沒有收到,請檢查您的垃圾郵件文件夾。" another: >- 我們向您發送了另一封啟用電子郵件,地址為 {{mail}}。它可能需要幾分鐘才能到達;請務必檢查您的垃圾郵件文件夾。 btn_name: 重新發送啟用郵件 change_btn_name: 更改郵箱 msg: empty: 不能為空 resend_email: url_label: Are you sure you want to resend the activation email? url_text: You can also give the activation link above to the user. login: login_to_continue: 登入以繼續 info_sign: 沒有帳戶?<1>註冊 info_login: 已經有一個帳號?<1>登入 agreements: 登入即表示您同意<1>隱私政策和<3>服務條款。 forgot_pass: 忘記密碼? name: label: 名稱 msg: empty: 名稱不能為空 range: Name must be between 2 to 30 characters in length. character: 'Must use the character set "a-z", "0-9", " - . _"' email: label: 郵箱 msg: empty: 郵箱不能為空 password: label: 密碼 msg: empty: 密碼不能為空 different: 兩次輸入密碼不一致 account_forgot: page_title: 忘記密碼 btn_name: 向我發送恢復郵件 send_success: >- 如果帳號與{{mail}}相符,您應該很快就會收到一封電子郵件,說明如何重置您的密碼。 email: label: 郵箱 msg: empty: 郵箱不能為空 change_email: btn_cancel: 取消 btn_update: 更新電子郵件地址 send_success: >- 如果帳號與{{mail}}相符,您應該很快就會收到一封電子郵件,說明如何重置您的密碼。 email: label: New email msg: empty: 郵箱不能為空 oauth: connect: Connect with {{ auth_name }} remove: Remove {{ auth_name }} oauth_bind_email: subtitle: Add a recovery email to your account. btn_update: Update email address email: label: Email msg: empty: Email cannot be empty. modal_title: Email already existes. modal_content: This email address already registered. Are you sure you want to connect to the existing account? modal_cancel: Change email modal_confirm: Connect to the existing account password_reset: page_title: 密碼重置 btn_name: 重置我的密碼 reset_success: >- 你已經成功更改密碼,將返回登入頁面 link_invalid: >- 抱歉,此密碼重置連結已失效。也許是你已經重置過密碼了? to_login: 前往登入頁面 password: label: 密碼 msg: empty: 密碼不能為空 length: 密碼長度在8-32個字元之間 different: 兩次輸入密碼不一致 password_confirm: label: Confirm new password settings: page_title: 設置 goto_modify: Go to modify nav: profile: 我的資料 notification: 通知 account: 帳號 interface: 界面 profile: heading: 個人資料 btn_name: 保存 display_name: label: Display name msg: 顯示名稱不能為空。 msg_range: Display name must be 2-30 characters in length. username: label: 用戶名 caption: 用戶之間可以通過 "@用戶名" 進行交互。 msg: 用戶名不能為空 msg_range: Username must be 2-30 characters in length. character: 'Must use the character set "a-z", "0-9", "- . _"' avatar: label: Profile image gravatar: 頭像 gravatar_text: You can change image on custom: 自定義 custom_text: 您可以上傳您的圖片。 default: 系統 msg: 請上傳頭像 bio: label: About me website: label: 網站 placeholder: "https://example.com" msg: 網站格式不正確 location: label: 位置 placeholder: "城市, 國家" notification: heading: 通知 turn_on: Turn on inbox: label: Email notifications description: Answers to your questions, comments, invites, and more. all_new_question: label: All new questions description: Get notified of all new questions. Up to 50 questions per week. all_new_question_for_following_tags: label: All new questions for following tags description: Get notified of new questions for following tags. account: heading: 帳號 change_email_btn: 更改郵箱 change_pass_btn: 更改密碼 change_email_info: >- 我們已經寄出一封郵件至此電子郵件地址,請遵照說明進行確認。 email: label: 電子郵件地址 new_email: label: 新電子郵件地址 msg: 新電子郵件地址不能為空白。 pass: label: 目前密碼 msg: Password cannot be empty. password_title: 密碼 current_pass: label: Current password msg: empty: Current password cannot be empty. length: 密碼長度必須在 8 至 32 之間 different: 兩次輸入的密碼不匹配 new_pass: label: New password pass_confirm: label: 確認新密碼 interface: heading: 介面 lang: label: 介面語言 text: 設定使用者介面語言,在重新整裡頁面後生效。 my_logins: title: 我的登入 label: 使用這些帳號登入或註冊此網站。 modal_title: 移除登入 modal_content: Are you sure you want to remove this login from your account? modal_confirm_btn: Remove remove_success: Removed successfully toast: update: 更新成功 update_password: 更改密碼成功。 flag_success: 感謝您的標記 forbidden_operate_self: 禁止自己操作 review: 您的修訂將在審核通過後顯示。 sent_success: Sent successfully related_question: title: Related answers: 個回答 linked_question: title: Linked description: Posts linked to no_linked_question: No contents linked from this content. invite_to_answer: title: People Asked desc: Invite people who you think might know the answer. invite: Invite to answer add: Add people search: Search people question_detail: action: Action created: Created Asked: 提問於 asked: 提問於 update: 修改於 Edited: Edited edit: 最後編輯於 commented: commented Views: 閱讀次數 Follow: 關注 Following: 已關注 follow_tip: Follow this question to receive notifications answered: 回答於 closed_in: 關閉於 show_exist: 顯示現有問題。 useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: title: 個回答 score: 評分 newest: 最新 oldest: Oldest btn_accept: 採納 btn_accepted: 已被採納 write_answer: title: 你的回答 edit_answer: Edit my existing answer btn_name: 提交你的回答 add_another_answer: 添加另一個答案 confirm_title: 繼續回答 continue: 繼續 confirm_info: >-

您確定要添加一個新的回答嗎?

您可以使用编辑链接来完善和改进您现有的答案。

empty: 回答內容不能為空。 characters: 內容必須至少6個字元長度。 tips: header_1: Thanks for your answer li1_1: Please be sure to answer the question. Provide details and share your research. li1_2: Back up any statements you make with references or personal experience. header_2: But avoid ... li2_1: Asking for help, seeking clarification, or responding to other answers. reopen: confirm_btn: Reopen title: 重新打開這個貼文 content: 確定要重新打開嗎? list: confirm_btn: List title: List this post content: Are you sure you want to list? unlist: confirm_btn: Unlist title: Unlist this post content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. confirm_btn: Pin delete: title: 刪除此貼 question: >- 我們不建議刪除有回答的貼文。因為這樣做會使得後來的讀者無法從該問題中獲得幫助。

如果刪除過多有回答的貼文,你的帳號將會被禁止提問。你確定要刪除嗎? answer_accepted: >-

我們不建議刪除被採納的回答。因為這樣做會使得後來的讀者無法從該回答中獲得幫助。

如果刪除過多被採納的貼文,你的帳號將會被禁止回答任何提問。你確定要刪除嗎? other: 你確定要刪除? tip_answer_deleted: 此回答已被刪除 undelete_title: Undelete this post undelete_desc: Are you sure you wish to undelete? btns: confirm: 確認 cancel: 取消 edit: 編輯 save: 儲存 delete: 刪除 undelete: 還原 list: 清單 unlist: Unlist unlisted: Unlisted login: 登入 signup: 註冊 logout: 登出 verify: 驗證 create: 建立 approve: 核准 reject: 拒絕 skip: 略過 discard_draft: Discard draft pinned: Pinned all: All question: Question answer: Answer comment: Comment refresh: Refresh resend: Resend deactivate: Deactivate active: Active suspend: Suspend unsuspend: Unsuspend close: Close reopen: Reopen ok: OK light: Light dark: Dark system_setting: System setting default: Default reset: Reset tag: Tag post_lowercase: post filter: Filter ignore: Ignore submit: Submit normal: Normal closed: Closed deleted: Deleted deleted_permanently: Deleted permanently pending: Pending more: More view: View card: Card compact: Compact display_below: Display below always_display: Always display or: or back_sites: Back to sites search: title: 搜尋結果 keywords: 關鍵詞 options: 選項 follow: 追蹤 following: 已關注 counts: "{{count}} 個結果" counts_loading: "... Results" more: 更多 sort_btns: relevance: 相關性 newest: 最新的 active: 活躍的 score: 評分 more: 更多 tips: title: 高級搜尋提示 tag: "<1>[tag] 在指定標籤中搜尋" user: "<1>user:username 根據作者搜尋" answer: "<1>answers:0 搜尋未回答的問題" score: "<1>score:3 得分為 3+ 的帖子" question: "<1>is:question 只搜尋問題" is_answer: "<1>is:answer 只搜尋回答" empty: 找不到任何相關的內容。
請嘗試其他關鍵字,或者減少查找內容的長度。 share: name: 分享 copy: 複製連結 via: 分享在... copied: 已複製 facebook: 分享到 Facebook twitter: Share to X cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: 發生錯誤... delete_permanently: title: Delete permanently content: Are you sure you want to delete permanently? account_result: success: 你的帳號已通過驗證,即將返回首頁。 link: 繼續訪問主頁 oops: Oops! invalid: The link you used no longer works. confirm_new_email: 你的電子郵箱已更新 confirm_new_email_invalid: >- 抱歉,此驗證連結已失效。也許是你的郵箱已經成功更改了? unsubscribe: page_title: 退訂 success_title: 取消訂閱成功 success_desc: 您已成功從訂閱者清單中移除且不會在收到任何來自我們的郵件。 link: 更改設置 question: following_tags: 已關注的標籤 edit: 編輯 save: 儲存 follow_tag_tip: 按照標籤整理您的問題列表。 hot_questions: 熱門問題 all_questions: 全部問題 x_questions: "{{ count }} 個問題" x_answers: "{{ count }} 個回答" x_posts: "{{ count }} Posts" questions: 個問題 answers: 回答 newest: 最新的 active: 活躍的 hot: Hot frequent: Frequent recommend: Recommend score: 評分 unanswered: 未回答 modified: 修改於 answered: 回答於 asked: 提問於 closed: 已關閉 follow_a_tag: 關注一個標籤 more: 更多 personal: overview: 概覽 answers: 回答 answer: 回答 questions: 問題 question: 問題 bookmarks: 書籤 reputation: 聲望 comments: 評論 votes: 得票 badges: Badges newest: 最新 score: 評分 edit_profile: Edit profile visited_x_days: "已造訪 {{ count }} 天" viewed: 閱讀次數 joined: 加入於 comma: "," last_login: 出現時間 about_me: 關於我 about_me_empty: "// 你好, 世界 !" top_answers: 熱門回答 top_questions: 熱門問題 stats: 狀態 list_empty: 沒有找到相關的內容。
試試看其他標籤? content_empty: No posts found. accepted: 已採納 answered: 回答於 asked: 提問於 downvoted: downvoted mod_short: MOD mod_long: 管理員 x_reputation: 聲望 x_votes: 得票 x_answers: 個回答 x_questions: 個問題 recent_badges: Recent Badges install: title: Installation next: 下一步 done: 完成 config_yaml_error: 無法建立 config.yaml 檔。 lang: label: Please choose a language db_type: label: Database engine db_username: label: 用戶名 placeholder: 根 msg: 用戶名不能為空 db_password: label: 密碼 placeholder: root msg: 密碼不能為空 db_host: label: Database host placeholder: "db: 3306" msg: Database host cannot be empty. db_name: label: Database name placeholder: 回答 msg: Database name cannot be empty. db_file: label: Database file placeholder: /data/answer.db msg: Database file cannot be empty. ssl_enabled: label: Enable SSL ssl_enabled_on: label: On ssl_enabled_off: label: Off ssl_mode: label: SSL Mode ssl_root_cert: placeholder: sslrootcert file path msg: Path to sslrootcert file cannot be empty ssl_cert: placeholder: sslcert file path msg: Path to sslcert file cannot be empty ssl_key: placeholder: sslkey file path msg: Path to sslkey file cannot be empty config_yaml: title: 創建 config.yaml label: 已創建 config.yaml 文件。 desc: >- 您可以手動在 <1>/var/wwww/xxx/ 目錄中創建<1>config.yaml 文件並粘貼以下文本。 info: 完成後點擊"下一步"按鈕。 site_information: 網站資訊 admin_account: 管理員帳戶 site_name: label: Site name msg: Site name cannot be empty. msg_max_length: Site name must be at maximum 30 characters in length. site_url: label: 網站 URL text: 此網站的地址。 msg: empty: 網站URL不能為空。 incorrect: 網站URL格式不正確。 max_length: Site URL must be at maximum 512 characters in length. contact_email: label: Contact email text: 負責本網站的主要聯絡人的電子郵件地址。 msg: empty: Contact email cannot be empty. incorrect: Contact email incorrect format. login_required: label: Private switch: Login required text: Only logged in users can access this community. admin_name: label: 暱稱 msg: 暱稱不能為空。 character: 'Must use the character set "a-z", "0-9", " - . _"' msg_max_length: Name must be between 2 to 30 characters in length. admin_password: label: 密碼 text: >- 您需要此密碼才能登入。請將其儲存在一個安全的位置。 msg: 密碼不能為空。 msg_min_length: Password must be at least 8 characters in length. msg_max_length: Password must be at maximum 32 characters in length. admin_confirm_password: label: "Confirm Password" text: "Please re-enter your password to confirm." msg: "Confirm password does not match." admin_email: label: 郵箱 text: 您需要此電子郵件才能登入。 msg: empty: 郵箱不能為空。 incorrect: 郵箱格式不正確。 ready_title: Your site is ready ready_desc: >- 如果你想改變更多的設定,請瀏覽<1>管理員部分;在網站選單中找到它。 good_luck: "玩得愉快,祝您好運!" warn_title: 警告 warn_desc: >- 檔案<1>config.yaml已存在。如果您需要重置此文件中的任何配置項,請先刪除它。 install_now: 您可以嘗試<1>現在安裝。 installed: 已安裝 installed_desc: >- 您似乎已經安裝過了。要重新安裝,請先清除舊的資料庫表。 db_failed: 資料連接異常! db_failed_desc: >- This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. counts: views: 觀看 votes: 得票 answers: 回答 accepted: 已採納 page_error: http_error: HTTP 错误 {{ code }} desc_403: You don't have permission to access this page. desc_404: Unfortunately, this page doesn't exist. desc_50X: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: desc: "我們正在維護中,很快就會回來。" nav_menus: dashboard: 後台管理 contents: 內容 questions: 問題 answers: 回答 users: 使用者管理 badges: Badges flags: 檢舉 settings: 設定 general: 一般 interface: 介面 smtp: SMTP branding: 品牌 legal: 法律條款 write: 撰寫 terms: Terms tos: 服務條款 privacy: 隱私政策 seo: SEO customize: 自定義 themes: 主題 login: 登入 privileges: Privileges plugins: Plugins installed_plugins: Installed Plugins apperance: Appearance community: Community advanced: Advanced tags: Tags rules: Rules policies: Policies security: Security files: Files apikeys: API Keys intelligence: Intelligence ai_assistant: AI Assistant ai_settings: AI Settings mcp: MCP website_welcome: Welcome to {{site_name}} user_center: login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: modal: title: Congratulations content: You've earned a new badge. close: Close confirm: View badges title: Badges awarded: Awarded earned_×: Earned ×{{ number }} ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned admin: admin_header: title: 後台管理 dashboard: title: 後台管理 welcome: Welcome to Admin! site_statistics: Site statistics questions: "問題:" resolved: "Resolved:" unanswered: "Unanswered:" answers: "回答:" comments: "評論:" votes: "投票:" users: "Users:" flags: "檢舉:" reviews: "Reviews:" site_health: Site health version: "版本" https: "HTTPS:" upload_folder: "Upload folder:" run_mode: "Running mode:" private: Private public: Public smtp: "SMTP:" timezone: "時區:" system_info: System info go_version: "Go version:" database: "Database:" database_size: "Database size:" storage_used: "已用儲存空間:" uptime: "運行時間:" links: Links plugins: Plugins github: GitHub blog: Blog contact: Contact forum: Forum documents: 文件 feedback: 用戶反饋 support: 支持 review: 審核 config: 配置 update_to: 更新到 latest: 最新版本 check_failed: 校驗失敗 "yes": "是" "no": "否" not_allowed: 不允許 allowed: 允許 enabled: 已啟用 disabled: 停用 writable: Writable not_writable: Not writable flags: title: 檢舉 pending: 等待處理 completed: 已完成 flagged: 已標記 flagged_type: Flagged {{ type }} created: 創建於 action: 操作 review: 審核 user_role_modal: title: 更改用戶狀態為... btn_cancel: 取消 btn_submit: 提交 new_password_modal: title: Set new password form: fields: password: label: Password text: The user will be logged out and need to login again. msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit edit_profile_modal: title: Edit profile form: fields: display_name: label: Display name msg_range: Display name must be 2-30 characters in length. username: label: Username msg_range: Username must be 2-30 characters in length. email: label: Email msg_invalid: Invalid Email Address. edit_success: Edited successfully btn_cancel: Cancel btn_submit: Submit user_modal: title: Add new user form: fields: users: label: Bulk add user placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" text: Separate “name, email, password” with commas. One user per line. msg: "Please enter the user's email, one per line." display_name: label: Display name msg: Display name must be 2-30 characters in length. email: label: Email msg: Email is not valid. password: label: Password msg: Password must be at 8-32 characters in length. btn_cancel: Cancel btn_submit: Submit users: title: 用戶 name: 名稱 email: 郵箱 reputation: 聲望 created_at: Created time delete_at: Deleted time suspend_at: Suspended time suspend_until: Suspend until status: 狀態 role: 角色 action: 操作 change: 更改 all: 全部 staff: 工作人員 more: More inactive: 不活躍 suspended: 已停權 deleted: 已刪除 normal: 正常 Moderator: 版主 Admin: 管理員 User: 用戶 filter: placeholder: "按名稱篩選,用戶:id" set_new_password: 設置新密碼 edit_profile: Edit profile change_status: 更改狀態 change_role: 更改角色 show_logs: 顯示日誌 add_user: 新增使用者 deactivate_user: title: Deactivate user content: An inactive user must re-validate their email. delete_user: title: Delete this user content: Are you sure you want to delete this user? This is permanent! remove: Remove their content label: Remove all questions, answers, comments, etc. text: Don’t check this if you wish to only delete the user’s account. suspend_user: title: Suspend this user content: A suspended user can't log in. label: How long will the user be suspended for? forever: Forever questions: page_title: 問題 unlisted: Unlisted post: 標題 votes: 得票數 answers: 回答 created: 創建於 status: 狀態 action: 操作 change: 更改 pending: Pending filter: placeholder: "按標題過濾,問題:id" answers: page_title: 回答 post: 發布 votes: 得票數 created: 創建於 status: 狀態 action: 操作 change: 更改 filter: placeholder: "按名稱篩選,answer:id" general: page_title: 一般 name: label: Site name msg: 不能為空 text: "網站的名稱,如標題標籤中所用。" site_url: label: 網站網址 msg: 網站網址不能為空。 validate: 請輸入一個有效的 URL。 text: 此網站的網址。 short_desc: label: Short site description msg: 網站簡短描述不能為空。 text: "簡短的描述,如主頁上的標題標籤所使用的那样。" desc: label: Site description msg: 網站描述不能為空。 text: "使用一句話描述本站,作為網站的描述(Html 的 meta 標籤)。" contact_email: label: Contact email msg: 聯絡人信箱不能為空。 validate: 聯絡人信箱無效。 text: 負責本網站的主要聯絡人的電子郵件信箱。 check_update: label: Software updates text: Automatically check for updates interface: page_title: 介面 language: label: Interface language msg: 界面語言不能為空 text: 設置用戶界面語言,在刷新頁面后生效。 time_zone: label: 時區 msg: 時區不能為空。 text: 選擇一個與您相同時區的城市。 avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar base URL text: URL of the Gravatar provider's API base. Ignored when empty. smtp: page_title: SMTP from_email: label: From email msg: 發件人電子郵件不能为空。 text: 發送郵件的郵箱地址 from_name: label: From name msg: 發件人名稱不能为空。 text: 發件人的名稱 smtp_host: label: SMTP host msg: SMTP 主機名稱不能為空。 text: 郵件服務器 encryption: label: 加密 msg: 加密不能為空。 text: 對於大多數服務器,SSL 是推薦的選項。 ssl: SSL tls: TLS none: 無 smtp_port: label: SMTP port msg: SMTP 埠必須在 1 ~ 65535 之間。 text: 郵件服務器的端口號。 smtp_username: label: SMTP username msg: SMTP 用戶名不能為空。 smtp_password: label: SMTP password msg: SMTP 密碼不能為空。 test_email_recipient: label: Test email recipients text: 提供用於接收測試郵件的郵箱地址。 msg: 測試郵件收件人無效 smtp_authentication: label: 啟用身份驗證 title: SMTP authentication msg: SMTP 身份驗證不能為空。 "yes": "是" "no": "否" branding: page_title: 品牌 logo: label: 標誌 msg: 圖標不能為空。 text: 在你的網站左上方的Logo圖標。使用一個高度為56,長寬比大於3:1的寬長方形圖像。如果留空,將顯示網站標題文本。 mobile_logo: label: Mobile logo text: 在您網站的移動版本上使用的徽標。 使用高度為 56 的寬矩形圖像。如果留空,將使用“徽標”設置中的圖像。 square_icon: label: Square icon msg: 方形圖示不能為空。 text: 用作元數據圖標的基礎的圖像。最好是大於512x512。 favicon: label: 網站圖示 text: 您網站的圖標。 要在 CDN 上正常工作,它必須是 png。 將調整為 32x32的大小。 如果留空,將使用“方形圖標”。 legal: page_title: 法律條款 terms_of_service: label: Terms of service text: "您可以在此加入服務內容的條款。如果您已經在別處托管了文檔,請在這裡提供完整的URL。" privacy_policy: label: Privacy policy text: "您可以在此加入隱私政策內容。如果您已經在別處托管了文檔,請在這裡提供完整的URL。" external_content_display: label: External content text: "Content includes images, videos, and media embedded from external websites." always_display: Always display external content ask_before_display: Ask before displaying external content write: page_title: Files min_content: label: Minimum question body length text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for each question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." msg: contain_reserved: "recommended tags cannot contain reserved tags" required_tag: title: Set required tags label: Set “Recommend tags” as required tags text: "每個新問題必須至少有一個推薦標籤。" reserved_tags: label: Reserved tags text: "Reserved tags can only be used by moderator." image_size: label: Max image size (MB) text: "The maximum image upload size." attachment_size: label: Max attachment size (MB) text: "The maximum attachment files upload size." image_megapixels: label: Max image megapixels text: "Maximum number of megapixels allowed for an image." image_extensions: label: Authorized image extensions text: "A list of file extensions allowed for image display, separate with commas." attachment_extensions: label: Authorized attachment extensions text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." seo: page_title: 搜尋引擎優化 permalink: label: 固定連結 text: 自定義URL結構可以提高可用性,以及你的連結的向前相容性。 robots: label: robots.txt text: 這將永久覆蓋任何相關的網站設置。 themes: page_title: 主題 themes: label: 主題 text: 選擇一個現有主題。 color_scheme: label: Color scheme navbar_style: label: Navbar background style primary_color: label: 主色調 text: 修改您主題使用的顏色 layout: label: Layout full_width: Full-width fixed_width: Fixed-width css_and_html: page_title: CSS 與 HTML custom_css: label: 自定義CSS text: > head: label: 頭部 text: > header: label: 標題 text: > footer: label: 頁尾 text: This will insert before </body>. sidebar: label: Sidebar text: This will insert in sidebar. login: page_title: 登入 membership: title: 會員 label: 允許新註冊 text: 關閉以防止任何人創建新帳戶。 email_registration: title: Email registration label: Allow email registration text: Turn off to prevent anyone creating new account through email. allowed_email_domains: title: Allowed email domains text: Email domains that users must register accounts with. One domain per line. Ignored when empty. private: title: 非公開的 label: 需要登入 text: 只有登入使用者才能訪問這個社群。 password_login: title: Password login label: Allow email and password login text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." installed_plugins: title: Installed Plugins plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. filter: all: All active: Active inactive: Inactive outdated: Outdated plugins: label: Plugins text: Select an existing plugin. name: Name version: Version status: Status action: Action deactivate: Deactivate activate: Activate settings: Settings settings_users: title: Users avatar: label: Default avatar text: For users without a custom avatar of their own. gravatar_base_url: label: Gravatar 基礎網址 text: URL of the Gravatar provider's API base. Ignored when empty. profile_editable: title: Profile editable allow_update_display_name: label: Allow users to change their display name allow_update_username: label: Allow users to change their username allow_update_avatar: label: Allow users to change their profile image allow_update_bio: label: Allow users to change their about me allow_update_website: label: Allow users to change their website allow_update_location: label: Allow users to change their location privilege: title: Privileges level: label: Reputation required level text: Choose the reputation required for the privileges msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 badges: action: Action active: Active activate: Activate all: All awards: Awards deactivate: Deactivate filter: placeholder: Filter by name, badge:id group: Group inactive: Inactive name: Name show_logs: Show logs status: Status title: Badges apikeys: title: API Keys add_api_key: Add API Key desc: Description scope: Scope key: Key created: Created last_used: Last used add_or_edit_modal: add_title: Add API Key edit_title: Edit API Key description: Description description_required: Description is required. scope: Scope global: Global read-only: Read-only created_modal: title: API key created api_key: API key description: This key will not be displayed again. Make sure you take a copy before continuing. delete_modal: title: Delete API Key content: Any applications or scripts using this key will no longer be able to access the API. This is permanent! ai_settings: enabled: label: AI enabled check: Enable AI features text: The AI model must be configured correctly before it can be used. provider: label: Provider api_host: label: API host msg: API host is required api_key: label: API key check: Check check_success: "Connection successful." msg: API key is required model: label: Model msg: Model is required add_success: AI settings updated successfully. conversations: topic: Topic helpful: Helpful unhelpful: Unhelpful created: Created action: Action empty: No conversations found. delete_modal: title: Delete conversation content: Are you sure you want to delete this conversation? This is permanent! delete_success: Conversation deleted successfully. mcp: mcp_server: label: MCP server switch: Enabled type: label: Type url: label: URL http_header: label: HTTP header text: Please replace {key} with the API Key. form: optional: (選填) empty: 不能為空 invalid: 是無效的 btn_submit: 儲存 not_found_props: "所需屬性 {{ key }} 未找到。" select: Select page_review: review: 審核 proposed: 提案 question_edit: 問題編輯 answer_edit: 回答編輯 tag_edit: '標籤管理: 編輯標籤' edit_summary: 編輯摘要 edit_question: 編輯問題 edit_answer: 編輯回答 edit_tag: 編輯標籤 empty: 沒有剩餘的審核任務。 approve_revision_tip: Do you approve this revision? approve_flag_tip: Do you approve this flag? approve_post_tip: Do you approve this post? approve_user_tip: Do you approve this user? suggest_edits: Suggested edits flag_post: Flag post flag_user: Flag user queued_post: Queued post queued_user: Queued user filter_label: Type reputation: reputation flag_post_type: Flagged this post as {{ type }}. flag_user_type: Flagged this user as {{ type }}. edit_post: Edit post list_post: List post unlist_post: Unlist post timeline: undeleted: 未刪除的 deleted: 刪除 downvote: 反對 upvote: 贊同 accept: 採納 cancelled: 已取消 commented: '評論:' rollback: 回滾 edited: 最後編輯於 answered: 回答於 asked: 提問於 closed: 關閉 reopened: 重新開啟 created: 創建於 pin: pinned unpin: unpinned show: listed hide: unlisted title: "歷史記錄" tag_title: "時間線" show_votes: "顯示投票" n_or_a: N/A title_for_question: "時間線" title_for_answer: "{{ title }} 的 {{ author }} 回答時間線" title_for_tag: "標籤的時間線" datetime: 日期時間 type: 類型 by: 由 comment: 評論 no_data: "我們找不到任何東西。" users: title: 用戶 users_with_the_most_reputation: Users with the highest reputation scores this week users_with_the_most_vote: Users who voted the most this week staffs: 我們的社區工作人員 reputation: 聲望值 votes: 選票 prompt: leave_page: 你確定要離開此頁面? changes_not_save: 你所做的變更可能不會儲存。 draft: discard_confirm: Are you sure you want to discard your draft? messages: post_deleted: This post has been deleted. post_cancel_deleted: This post has been undeleted. post_pin: This post has been pinned. post_unpin: This post has been unpinned. post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. post_list: This post has been listed. post_unlist: This post has been unlisted. post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. post_closed: This post has been closed. answer_deleted: This answer has been deleted. answer_cancel_deleted: This answer has been undeleted. change_user_role: This user's role has been changed. user_inactive: This user is already inactive. user_normal: This user is already normal. user_suspended: This user has been suspended. user_deleted: This user has been deleted. user_added: User has been added successfully. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. copy: Copy to clipboard copied: Copied external_content_warning: External images/media are not displayed. ================================================ FILE: internal/base/conf/conf.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package conf import ( "bytes" "os" "path/filepath" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/path" "github.com/apache/answer/internal/base/server" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/router" "github.com/apache/answer/internal/service/service_config" "github.com/apache/answer/pkg/writer" "github.com/segmentfault/pacman/contrib/conf/viper" "gopkg.in/yaml.v3" ) // AllConfig all config type AllConfig struct { Debug bool `json:"debug" mapstructure:"debug" yaml:"debug"` Server *Server `json:"server" mapstructure:"server" yaml:"server"` Data *Data `json:"data" mapstructure:"data" yaml:"data"` I18n *translator.I18n `json:"i18n" mapstructure:"i18n" yaml:"i18n"` ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config" yaml:"service_config"` Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui" yaml:"swaggerui"` UI *server.UI `json:"ui" mapstructure:"ui" yaml:"ui"` } type envConfigOverrides struct { SwaggerHost string SwaggerAddressPort string SiteAddr string } func loadEnvs() (envOverrides *envConfigOverrides) { return &envConfigOverrides{ SwaggerHost: os.Getenv("SWAGGER_HOST"), SwaggerAddressPort: os.Getenv("SWAGGER_ADDRESS_PORT"), SiteAddr: os.Getenv("SITE_ADDR"), } } type PathIgnore struct { Users []string `yaml:"users"` } // Server server config type Server struct { HTTP *server.HTTP `json:"http" mapstructure:"http" yaml:"http"` } // Data data config type Data struct { Database *data.Database `json:"database" mapstructure:"database" yaml:"database"` Cache *data.CacheConf `json:"cache" mapstructure:"cache" yaml:"cache"` } // SetDefault set default config func (c *AllConfig) SetDefault() { if c.UI == nil { c.UI = &server.UI{} } } func (c *AllConfig) SetEnvironmentOverrides() { envs := loadEnvs() if envs.SiteAddr != "" { c.Server.HTTP.Addr = envs.SiteAddr } if envs.SwaggerHost != "" { c.Swaggerui.Host = envs.SwaggerHost } if envs.SwaggerAddressPort != "" { c.Swaggerui.Address = envs.SwaggerAddressPort } } // ReadConfig read config func ReadConfig(configFilePath string) (c *AllConfig, err error) { if len(configFilePath) == 0 { configFilePath = filepath.Join(path.ConfigFileDir, path.DefaultConfigFileName) } c = &AllConfig{} config, err := viper.NewWithPath(configFilePath) if err != nil { return nil, err } if err = config.Parse(&c); err != nil { return nil, err } c.SetDefault() c.SetEnvironmentOverrides() return c, nil } // RewriteConfig rewrite config file path func RewriteConfig(configFilePath string, allConfig *AllConfig) error { buf := bytes.Buffer{} enc := yaml.NewEncoder(&buf) defer func() { _ = enc.Close() }() enc.SetIndent(2) if err := enc.Encode(allConfig); err != nil { return err } return writer.ReplaceFile(configFilePath, buf.String()) } ================================================ FILE: internal/base/constant/acticity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant type ActivityTypeKey string const ( ActEdited = "edited" ActClosed = "closed" ActVotedDown = "voted_down" ActVotedUp = "voted_up" ActVoteDown = "vote_down" ActVoteUp = "vote_up" ActUpVote = "upvote" ActDownVote = "downvote" ActFollow = "follow" ActAccepted = "accepted" ActAccept = "accept" ActPin = "pin" ActUnPin = "unpin" ActShow = "show" ActHide = "hide" ) const ( ActQuestionAsked ActivityTypeKey = "question.asked" ActQuestionClosed ActivityTypeKey = "question.closed" ActQuestionReopened ActivityTypeKey = "question.reopened" ActQuestionAnswered ActivityTypeKey = "question.answered" ActQuestionCommented ActivityTypeKey = "question.commented" ActQuestionAccept ActivityTypeKey = "question.accept" ActQuestionUpvote ActivityTypeKey = "question.upvote" ActQuestionDownVote ActivityTypeKey = "question.downvote" ActQuestionEdited ActivityTypeKey = "question.edited" ActQuestionRollback ActivityTypeKey = "question.rollback" ActQuestionDeleted ActivityTypeKey = "question.deleted" ActQuestionUndeleted ActivityTypeKey = "question.undeleted" ActQuestionPin ActivityTypeKey = "question.pin" ActQuestionUnPin ActivityTypeKey = "question.unpin" ActQuestionHide ActivityTypeKey = "question.hide" ActQuestionShow ActivityTypeKey = "question.show" ) const ( ActAnswerAnswered ActivityTypeKey = "answer.answered" ActAnswerCommented ActivityTypeKey = "answer.commented" ActAnswerAccept ActivityTypeKey = "answer.accept" ActAnswerUpvote ActivityTypeKey = "answer.upvote" ActAnswerDownVote ActivityTypeKey = "answer.downvote" ActAnswerEdited ActivityTypeKey = "answer.edited" ActAnswerRollback ActivityTypeKey = "answer.rollback" ActAnswerDeleted ActivityTypeKey = "answer.deleted" ActAnswerUndeleted ActivityTypeKey = "answer.undeleted" ) const ( ActTagCreated ActivityTypeKey = "tag.created" ActTagEdited ActivityTypeKey = "tag.edited" ActTagRollback ActivityTypeKey = "tag.rollback" ActTagDeleted ActivityTypeKey = "tag.deleted" ActTagUndeleted ActivityTypeKey = "tag.undeleted" ) ================================================ FILE: internal/base/constant/ai_config.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( AIConfigProvider = "ai_config.provider" ) const ( DefaultAIPromptConfigZhCN = `你是一个智能助手,可以帮助用户查询系统中的信息。用户问题:%s 你可以使用以下工具来查询系统信息: - get_questions: 搜索系统中已存在的问题,使用这个工具可以获取问题列表后注意需要使用 get_answers_by_question_id 获取问题的答案 - get_answers_by_question_id: 根据问题ID获取该问题的所有答案 - get_comments: 搜索评论信息 - get_tags: 搜索标签信息 - get_tag_detail: 获取特定标签的详细信息 - get_user: 搜索用户信息 请根据用户的问题智能地使用这些工具来提供准确的答案。如果需要查询系统信息,请先使用相应的工具获取数据。` DefaultAIPromptConfigEnUS = `You are an intelligent assistant that can help users query information in the system. User question: %s You can use the following tools to query system information: - get_questions: Search for existing questions in the system. After using this tool to get the question list, you need to use get_answers_by_question_id to get the answers to the questions - get_answers_by_question_id: Get all answers for a question based on question ID - get_comments: Search for comment information - get_tags: Search for tag information - get_tag_detail: Get detailed information about a specific tag - get_user: Search for user information Please intelligently use these tools based on the user's question to provide accurate answers. If you need to query system information, please use the appropriate tools to get the data first.` ) ================================================ FILE: internal/base/constant/cache_key.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant import "time" const ( UserStatusChangedCacheKey = "answer:user:status:" UserStatusChangedCacheTime = 7 * 24 * time.Hour UserTokenCacheKey = "answer:user:token:" UserTokenCacheTime = 7 * 24 * time.Hour UserVisitTokenCacheKey = "answer:user:visit:" UserVisitCacheTime = 7 * 24 * 60 * 60 UserVisitCookiesCacheKey = "visit" AdminTokenCacheKey = "answer:admin:token:" AdminTokenCacheTime = 7 * 24 * time.Hour UserTokenMappingCacheKey = "answer:user-token:mapping:" UserEmailCodeCacheKey = "answer:user:email-code:" UserEmailCodeCacheTime = 10 * time.Minute UserLatestEmailCodeCacheKey = "answer:user-id:email-code:" SiteInfoCacheKey = "answer:site-info:" SiteInfoCacheTime = 1 * time.Hour ConfigID2KEYCacheKeyPrefix = "answer:config:id:" ConfigKEY2ContentCacheKeyPrefix = "answer:config:key:" ConfigCacheTime = 1 * time.Hour ConnectorUserExternalInfoCacheKey = "answer:connector:" ConnectorUserExternalInfoCacheTime = 10 * time.Minute SiteMapQuestionCacheKeyPrefix = "answer:sitemap:question:%d" SiteMapQuestionCacheTime = time.Hour SitemapMaxSize = 50000 NewQuestionNotificationLimitCacheKeyPrefix = "answer:new-question-notification-limit:" NewQuestionNotificationLimitCacheTime = 7 * 24 * time.Hour NewQuestionNotificationLimitMax = 50 RateLimitCacheKeyPrefix = "answer:rate-limit:" RateLimitCacheTime = 5 * time.Minute RedDotCacheKey = "answer:red-dot:%s:%s" RedDotCacheTime = 30 * 24 * time.Hour ) ================================================ FILE: internal/base/constant/comment.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant import "time" const ( CommentEditDeadline = time.Minute * 5 ) ================================================ FILE: internal/base/constant/constant.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( DefaultPageSize = 20 // Default number of pages DefaultBulkUser = 5000 ) var ( Version = "" Revision = "" GoVersion = "" ) var Timezones = []string{ // Americas "America/New_York", "America/Chicago", "America/Los_Angeles", "America/Toronto", "America/Vancouver", "America/Mexico_City", "America/Sao_Paulo", "America/Buenos_Aires", // Europe "Europe/London", "Europe/Paris", "Europe/Berlin", "Europe/Madrid", "Europe/Rome", "Europe/Moscow", // Asia "Asia/Shanghai", "Asia/Tokyo", "Asia/Singapore", "Asia/Dubai", "Asia/Hong_Kong", "Asia/Seoul", "Asia/Bangkok", "Asia/Kolkata", // Pacific "Australia/Sydney", "Australia/Melbourne", "Pacific/Auckland", // Africa "Africa/Cairo", "Africa/Johannesburg", "Africa/Lagos", } ================================================ FILE: internal/base/constant/ctx_flag.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( AcceptLanguageFlag = "Accept-Language" ShortIDFlag = "Short-ID-Enabled" ) type ContextKey string const ( AcceptLanguageContextKey ContextKey = ContextKey(AcceptLanguageFlag) ShortIDContextKey ContextKey = ContextKey(ShortIDFlag) ) ================================================ FILE: internal/base/constant/email_tpl_key.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( EmailTplKeyChangeEmailTitle = "email_tpl.change_email.title" EmailTplKeyChangeEmailBody = "email_tpl.change_email.body" EmailTplKeyNewAnswerTitle = "email_tpl.new_answer.title" EmailTplKeyNewAnswerBody = "email_tpl.new_answer.body" EmailTplKeyNewCommentTitle = "email_tpl.new_comment.title" EmailTplKeyNewCommentBody = "email_tpl.new_comment.body" EmailTplKeyPassResetTitle = "email_tpl.pass_reset.title" EmailTplKeyPassResetBody = "email_tpl.pass_reset.body" EmailTplKeyRegisterTitle = "email_tpl.register.title" EmailTplKeyRegisterBody = "email_tpl.register.body" EmailTplKeyTestTitle = "email_tpl.test.title" EmailTplKeyTestBody = "email_tpl.test.body" EmailTplKeyInvitedAnswerTitle = "email_tpl.invited_you_to_answer.title" EmailTplKeyInvitedAnswerBody = "email_tpl.invited_you_to_answer.body" EmailTplKeyNewQuestionTitle = "email_tpl.new_question.title" EmailTplKeyNewQuestionBody = "email_tpl.new_question.body" ) ================================================ FILE: internal/base/constant/event.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant // EventType event type. It is used to define the type of event. Such as object.action type EventType string // event object const ( eventQuestion = "question" eventAnswer = "answer" eventComment = "comment" eventUser = "user" ) // event action const ( eventCreate = "create" eventUpdate = "update" eventDelete = "delete" eventVote = "vote" eventAccept = "accept" // only question have the accept event eventShare = "share" // the object share link has been clicked eventFlag = "flag" eventReact = "react" ) const ( EventUserUpdate EventType = eventUser + "." + eventUpdate EventUserShare EventType = eventUser + "." + eventShare ) const ( EventQuestionCreate EventType = eventQuestion + "." + eventCreate EventQuestionUpdate EventType = eventQuestion + "." + eventUpdate EventQuestionDelete EventType = eventQuestion + "." + eventDelete EventQuestionVote EventType = eventQuestion + "." + eventVote EventQuestionAccept EventType = eventQuestion + "." + eventAccept EventQuestionFlag EventType = eventQuestion + "." + eventFlag EventQuestionReact EventType = eventQuestion + "." + eventReact ) const ( EventAnswerCreate EventType = eventAnswer + "." + eventCreate EventAnswerUpdate EventType = eventAnswer + "." + eventUpdate EventAnswerDelete EventType = eventAnswer + "." + eventDelete EventAnswerVote EventType = eventAnswer + "." + eventVote EventAnswerFlag EventType = eventAnswer + "." + eventFlag EventAnswerReact EventType = eventAnswer + "." + eventReact ) const ( EventCommentCreate EventType = eventComment + "." + eventCreate EventCommentUpdate EventType = eventComment + "." + eventUpdate EventCommentDelete EventType = eventComment + "." + eventDelete EventCommentVote EventType = eventComment + "." + eventVote EventCommentFlag EventType = eventComment + "." + eventFlag ) ================================================ FILE: internal/base/constant/meta.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( ReactionTooltipLabel = "reaction.tooltip" ) ================================================ FILE: internal/base/constant/notification.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( // NotificationUpdateQuestion update question NotificationUpdateQuestion = "notification.action.update_question" // NotificationAnswerTheQuestion answer the question NotificationAnswerTheQuestion = "notification.action.answer_the_question" // NotificationUpVotedTheQuestion up voted the question NotificationUpVotedTheQuestion = "notification.action.up_voted_question" // NotificationDownVotedTheQuestion down voted the question NotificationDownVotedTheQuestion = "notification.action.down_voted_question" // NotificationUpdateAnswer update answer NotificationUpdateAnswer = "notification.action.update_answer" // NotificationAcceptAnswer accept answer NotificationAcceptAnswer = "notification.action.accept_answer" // NotificationUpVotedTheAnswer up voted the answer NotificationUpVotedTheAnswer = "notification.action.up_voted_answer" // NotificationDownVotedTheAnswer down voted the answer NotificationDownVotedTheAnswer = "notification.action.down_voted_answer" // NotificationCommentQuestion comment question NotificationCommentQuestion = "notification.action.comment_question" // NotificationCommentAnswer comment answer NotificationCommentAnswer = "notification.action.comment_answer" // NotificationUpVotedTheComment up voted the comment NotificationUpVotedTheComment = "notification.action.up_voted_comment" // NotificationReplyToYou reply to you NotificationReplyToYou = "notification.action.reply_to_you" // NotificationMentionYou mention you NotificationMentionYou = "notification.action.mention_you" // NotificationYourQuestionIsClosed your question is closed NotificationYourQuestionIsClosed = "notification.action.your_question_is_closed" // NotificationYourQuestionWasDeleted your question was deleted NotificationYourQuestionWasDeleted = "notification.action.your_question_was_deleted" // NotificationYourAnswerWasDeleted your answer was deleted NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted" // NotificationYourCommentWasDeleted your comment was deleted NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted" // NotificationInvitedYouToAnswer invited you to answer NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer" // NotificationEarnedBadge earned badge NotificationEarnedBadge = "notification.action.earned_badge" ) type NotificationChannelKey string type NotificationSource string const ( InboxSource NotificationSource = "inbox" AllNewQuestionSource NotificationSource = "all_new_question" AllNewQuestionForFollowingTagsSource NotificationSource = "all_new_question_for_following_tags" ) const ( EmailChannel NotificationChannelKey = "email" ) const ( NotificationTypeInbox = "inbox" NotificationTypeAchievement = "achievement" NotificationTypeBadgeAchievement = "badge" ) var ( NotificationMsgTypeMapping = map[string]int{ NotificationUpdateQuestion: 1, NotificationAnswerTheQuestion: 1, NotificationUpVotedTheQuestion: 2, NotificationDownVotedTheQuestion: 2, NotificationUpdateAnswer: 1, NotificationAcceptAnswer: 1, NotificationUpVotedTheAnswer: 2, NotificationDownVotedTheAnswer: 2, NotificationCommentQuestion: 1, NotificationCommentAnswer: 1, NotificationUpVotedTheComment: 2, NotificationReplyToYou: 1, NotificationMentionYou: 1, NotificationYourQuestionIsClosed: 1, NotificationYourQuestionWasDeleted: 1, NotificationYourAnswerWasDeleted: 1, NotificationYourCommentWasDeleted: 1, NotificationInvitedYouToAnswer: 3, } ) ================================================ FILE: internal/base/constant/object_type.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( QuestionObjectType = "question" AnswerObjectType = "answer" TagObjectType = "tag" UserObjectType = "user" CollectionObjectType = "collection" CommentObjectType = "comment" ReportObjectType = "report" BadgeObjectType = "badge" BadgeAwardObjectType = "badge_award" ) var ( ObjectTypeStrMapping = map[string]int{ QuestionObjectType: 1, AnswerObjectType: 2, TagObjectType: 3, UserObjectType: 4, CollectionObjectType: 6, CommentObjectType: 7, ReportObjectType: 8, BadgeObjectType: 9, BadgeAwardObjectType: 10, } ObjectTypeNumberMapping = map[int]string{ 1: QuestionObjectType, 2: AnswerObjectType, 3: TagObjectType, 4: UserObjectType, 6: CollectionObjectType, 7: CommentObjectType, 8: ReportObjectType, 9: BadgeObjectType, 10: BadgeAwardObjectType, } ) ================================================ FILE: internal/base/constant/plugin_config_key.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( PluginStatus = "plugin.status" ) ================================================ FILE: internal/base/constant/privilege.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant import "github.com/apache/answer/internal/base/reason" type Privilege struct { Key string `json:"key"` Label string `json:"label"` Value int `validate:"gte=1" json:"value"` } const ( RankQuestionAddKey = "rank.question.add" RankQuestionEditKey = "rank.question.edit" RankQuestionDeleteKey = "rank.question.delete" RankQuestionVoteUpKey = "rank.question.vote_up" RankQuestionVoteDownKey = "rank.question.vote_down" RankAnswerAddKey = "rank.answer.add" RankAnswerEditKey = "rank.answer.edit" RankAnswerDeleteKey = "rank.answer.delete" RankAnswerAcceptKey = "rank.answer.accept" RankAnswerVoteUpKey = "rank.answer.vote_up" RankAnswerVoteDownKey = "rank.answer.vote_down" RankInviteSomeoneToAnswerKey = "rank.answer.invite_someone_to_answer" RankCommentAddKey = "rank.comment.add" RankCommentEditKey = "rank.comment.edit" RankCommentDeleteKey = "rank.comment.delete" RankReportAddKey = "rank.report.add" RankTagAddKey = "rank.tag.add" RankTagEditKey = "rank.tag.edit" RankTagDeleteKey = "rank.tag.delete" RankTagSynonymKey = "rank.tag.synonym" RankLinkUrlLimitKey = "rank.link.url_limit" RankVoteDetailKey = "rank.vote.detail" RankCommentVoteUpKey = "rank.comment.vote_up" RankCommentVoteDownKey = "rank.comment.vote_down" RankQuestionEditWithoutReviewKey = "rank.question.edit_without_review" RankAnswerEditWithoutReviewKey = "rank.answer.edit_without_review" RankTagEditWithoutReviewKey = "rank.tag.edit_without_review" RankAnswerAuditKey = "rank.answer.audit" RankQuestionAuditKey = "rank.question.audit" RankTagAuditKey = "rank.tag.audit" RankQuestionCloseKey = "rank.question.close" RankQuestionReopenKey = "rank.question.reopen" RankTagUseReservedTagKey = "rank.tag.use_reserved_tag" ) var ( RankAllPrivileges = []*Privilege{ {Label: reason.RankQuestionAddLabel, Key: RankQuestionAddKey}, {Label: reason.RankAnswerAddLabel, Key: RankAnswerAddKey}, {Label: reason.RankCommentAddLabel, Key: RankCommentAddKey}, {Label: reason.RankReportAddLabel, Key: RankReportAddKey}, {Label: reason.RankCommentVoteUpLabel, Key: RankCommentVoteUpKey}, {Label: reason.RankLinkUrlLimitLabel, Key: RankLinkUrlLimitKey}, {Label: reason.RankQuestionVoteUpLabel, Key: RankQuestionVoteUpKey}, {Label: reason.RankAnswerVoteUpLabel, Key: RankAnswerVoteUpKey}, {Label: reason.RankQuestionVoteDownLabel, Key: RankQuestionVoteDownKey}, {Label: reason.RankAnswerVoteDownLabel, Key: RankAnswerVoteDownKey}, {Label: reason.RankInviteSomeoneToAnswerLabel, Key: RankInviteSomeoneToAnswerKey}, {Label: reason.RankTagAddLabel, Key: RankTagAddKey}, {Label: reason.RankTagEditLabel, Key: RankTagEditKey}, {Label: reason.RankQuestionEditLabel, Key: RankQuestionEditKey}, {Label: reason.RankAnswerEditLabel, Key: RankAnswerEditKey}, {Label: reason.RankQuestionEditWithoutReviewLabel, Key: RankQuestionEditWithoutReviewKey}, {Label: reason.RankAnswerEditWithoutReviewLabel, Key: RankAnswerEditWithoutReviewKey}, {Label: reason.RankQuestionAuditLabel, Key: RankQuestionAuditKey}, {Label: reason.RankAnswerAuditLabel, Key: RankAnswerAuditKey}, {Label: reason.RankTagAuditLabel, Key: RankTagAuditKey}, {Label: reason.RankTagEditWithoutReviewLabel, Key: RankTagEditWithoutReviewKey}, {Label: reason.RankTagSynonymLabel, Key: RankTagSynonymKey}, } ) ================================================ FILE: internal/base/constant/question.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( DeletedQuestionTitleTrKey = "question.deleted_title" QuestionsTitleTrKey = "question.questions_title" TagsListTitleTrKey = "tag.tags_title" TagHasNoDescription = "tag.no_description" ) ================================================ FILE: internal/base/constant/reason.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( ReasonSpam = "reason.spam" ReasonRudeOrAbusive = "reason.rude_or_abusive" ReasonSomething = "reason.something" ReasonADuplicate = "reason.a_duplicate" ReasonNotAAnswer = "reason.not_a_answer" ReasonNoLongerNeeded = "reason.no_longer_needed" ReasonCommunitySpecific = "reason.community_specific" ReasonNotClarity = "reason.not_clarity" ReasonNormal = "reason.normal" ReasonNormalUser = "reason.normal.user" ReasonClosed = "reason.closed" ReasonDeleted = "reason.deleted" ReasonDeletedUser = "reason.deleted.user" ReasonSuspended = "reason.suspended" ReasonInactive = "reason.inactive" ReasonLooksOk = "reason.looks_ok" ReasonNeedsEdit = "reason.needs_edit" ReasonNeedsClose = "reason.needs_close" ReasonNeedsDelete = "reason.needs_delete" ) ================================================ FILE: internal/base/constant/revision.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant type ReviewingType string const ( QueuedPost ReviewingType = "queued_post" QueuedUser ReviewingType = "queued_user" FlaggedPost ReviewingType = "flagged_post" FlaggedUser ReviewingType = "flagged_user" SuggestedPostEdit ReviewingType = "suggested_post_edit" ) const ( ReportOperationEditPost = "edit_post" ReportOperationClosePost = "close_post" ReportOperationDeletePost = "delete_post" ReportOperationUnlistPost = "unlist_post" ReportOperationIgnoreReport = "ignore_report" ) const ( ReviewQueuedPostLabel = "review.queued_post" ReviewFlaggedPostLabel = "review.flagged_post" ReviewSuggestedPostEditLabel = "review.suggested_post_edit" ) ================================================ FILE: internal/base/constant/site_info.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( DefaultGravatarBaseURL = "https://www.gravatar.com/avatar/" DefaultAvatar = "system" AvatarTypeDefault = "default" AvatarTypeGravatar = "gravatar" AvatarTypeCustom = "custom" ) const ( // PermalinkQuestionIDAndTitle /questions/10010000000000001/post-title PermalinkQuestionIDAndTitle = iota + 1 // PermalinkQuestionID /questions/10010000000000001 PermalinkQuestionID // PermalinkQuestionIDAndTitleByShortID /questions/11/post-title PermalinkQuestionIDAndTitleByShortID // PermalinkQuestionIDByShortID /questions/11 PermalinkQuestionIDByShortID ) const ( ColorSchemeDefault = "default" ColorSchemeLight = "light" ColorSchemeDark = "dark" ColorSchemeSystem = "system" ThemeLayoutFullWidth = "Full-width" ThemeLayoutFixedWidth = "Fixed-width" ) const ( EmailConfigKey = "email.config" ) const ( DefaultMaxImageMegapixel = 40 * 1000 * 1000 DefaultMaxImageSize = 4 * 1024 * 1024 DefaultMaxAttachmentSize = 8 * 1024 * 1024 ) ================================================ FILE: internal/base/constant/site_type.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( // SiteTypeLegal\SiteTypeLegal\SiteTypeWrite The following items will no longer be used. SiteTypeLegal = "legal" SiteTypeInterface = "interface" SiteTypeWrite = "write" SiteTypeGeneral = "general" SiteTypeBranding = "branding" SiteTypeSeo = "seo" SiteTypeLogin = "login" SiteTypeCustomCssHTML = "css-html" SiteTypeTheme = "theme" SiteTypePrivileges = "privileges" SiteTypeUsers = "users" SiteTypeAdvanced = "advanced" SiteTypeQuestions = "questions" SiteTypeTags = "tags" SiteTypeUsersSettings = "users_settings" SiteTypeInterfaceSettings = "interface_settings" SiteTypePolicies = "policies" SiteTypeSecurity = "security" SiteTypeAI = "ai" SiteTypeFeatureToggle = "feature-toggle" SiteTypeMCP = "mcp" ) ================================================ FILE: internal/base/constant/upload.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( AvatarSubPath = "avatar" AvatarThumbSubPath = "avatar_thumb" PostSubPath = "post" BrandingSubPath = "branding" FilesPostSubPath = "files/post" DeletedSubPath = "deleted" ) ================================================ FILE: internal/base/constant/user.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package constant const ( UserNormal = "normal" UserSuspended = "suspended" UserDeleted = "deleted" UserInactive = "inactive" ) const ( EmailStatusAvailable = 1 EmailStatusToBeVerified = 2 ) const ( DeletePermanentlyUsers = "users" DeletePermanentlyQuestions = "questions" DeletePermanentlyAnswers = "answers" ) func ConvertUserStatus(status, mailStatus int) string { switch status { case 1: if mailStatus == EmailStatusToBeVerified { return UserInactive } return UserNormal case 9: return UserSuspended case 10: return UserDeleted } return UserNormal } ================================================ FILE: internal/base/cron/cron.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package cron import ( "context" "fmt" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/file_record" "github.com/apache/answer/internal/service/service_config" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/internal/service/user_admin" "github.com/robfig/cron/v3" "github.com/segmentfault/pacman/log" ) // ScheduledTaskManager scheduled task manager type ScheduledTaskManager struct { siteInfoService siteinfo_common.SiteInfoCommonService questionService *content.QuestionService fileRecordService *file_record.FileRecordService userAdminService *user_admin.UserAdminService serviceConfig *service_config.ServiceConfig } // NewScheduledTaskManager new scheduled task manager func NewScheduledTaskManager( siteInfoService siteinfo_common.SiteInfoCommonService, questionService *content.QuestionService, fileRecordService *file_record.FileRecordService, userAdminService *user_admin.UserAdminService, serviceConfig *service_config.ServiceConfig, ) *ScheduledTaskManager { manager := &ScheduledTaskManager{ siteInfoService: siteInfoService, questionService: questionService, fileRecordService: fileRecordService, userAdminService: userAdminService, serviceConfig: serviceConfig, } return manager } func (s *ScheduledTaskManager) Run() { log.Infof("cron job manager start") s.questionService.SitemapCron(context.Background()) c := cron.New() _, err := c.AddFunc("0 */1 * * *", func() { ctx := context.Background() log.Infof("sitemap cron execution") s.questionService.SitemapCron(ctx) }) if err != nil { log.Error(err) } _, err = c.AddFunc("0 */1 * * *", func() { ctx := context.Background() log.Infof("refresh hottest cron execution") s.questionService.RefreshHottestCron(ctx) }) if err != nil { log.Error(err) } // Check for expired user suspensions every 10 minutes _, err = c.AddFunc("*/10 * * * *", func() { ctx := context.Background() log.Infof("checking expired user suspensions") err := s.userAdminService.CheckAndUnsuspendExpiredUsers(ctx) if err != nil { log.Errorf("failed to check expired user suspensions: %v", err) } }) if err != nil { log.Error(err) } if s.serviceConfig.CleanUpUploads { log.Infof("clean up uploads cron enabled") conf := s.serviceConfig _, err = c.AddFunc(fmt.Sprintf("0 */%d * * *", conf.CleanOrphanUploadsPeriodHours), func() { log.Infof("clean orphan upload files cron execution") s.fileRecordService.CleanOrphanUploadFiles(context.Background()) }) if err != nil { log.Error(err) } _, err = c.AddFunc(fmt.Sprintf("0 0 */%d * *", conf.PurgeDeletedFilesPeriodDays), func() { log.Infof("purge deleted files cron execution") s.fileRecordService.PurgeDeletedFiles(context.Background()) }) if err != nil { log.Error(err) } } c.Start() } ================================================ FILE: internal/base/cron/provider.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package cron import ( "github.com/google/wire" ) // ProviderSetService is providers. var ProviderSetService = wire.NewSet( NewScheduledTaskManager, ) ================================================ FILE: internal/base/data/config.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package data // Database database config type Database struct { Driver string `json:"driver" mapstructure:"driver" yaml:"driver"` Connection string `json:"connection" mapstructure:"connection" yaml:"connection"` ConnMaxLifeTime int `json:"conn_max_life_time" mapstructure:"conn_max_life_time" yaml:"conn_max_life_time,omitempty"` MaxOpenConn int `json:"max_open_conn" mapstructure:"max_open_conn" yaml:"max_open_conn,omitempty"` MaxIdleConn int `json:"max_idle_conn" mapstructure:"max_idle_conn" yaml:"max_idle_conn,omitempty"` } // CacheConf cache type CacheConf struct { FilePath string `json:"file_path" mapstructure:"file_path" yaml:"file_path"` } ================================================ FILE: internal/base/data/data.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package data import ( "path/filepath" "time" "github.com/apache/answer/pkg/dir" "github.com/apache/answer/plugin" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" "github.com/segmentfault/pacman/cache" "github.com/segmentfault/pacman/contrib/cache/memory" "github.com/segmentfault/pacman/log" _ "modernc.org/sqlite" "xorm.io/xorm" ormlog "xorm.io/xorm/log" "xorm.io/xorm/names" "xorm.io/xorm/schemas" ) // Data data type Data struct { DB *xorm.Engine Cache cache.Cache } // NewData new data instance func NewData(db *xorm.Engine, cache cache.Cache) (*Data, func(), error) { cleanup := func() { log.Info("closing the data resources") _ = db.Close() } return &Data{DB: db, Cache: cache}, cleanup, nil } // NewDB new database instance func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) { if dataConf.Driver == "" { dataConf.Driver = string(schemas.MYSQL) } if dataConf.Driver == string(schemas.SQLITE) { dataConf.Driver = "sqlite" dbFileDir := filepath.Dir(dataConf.Connection) log.Debugf("try to create database directory %s", dbFileDir) if err := dir.CreateDirIfNotExist(dbFileDir); err != nil { log.Errorf("create database dir failed: %s", err) } dataConf.MaxOpenConn = 1 } engine, err := xorm.NewEngine(dataConf.Driver, dataConf.Connection) if err != nil { return nil, err } if debug { engine.ShowSQL(true) } else { engine.SetLogLevel(ormlog.LOG_ERR) } if err = engine.Ping(); err != nil { return nil, err } if dataConf.MaxIdleConn > 0 { engine.SetMaxIdleConns(dataConf.MaxIdleConn) } if dataConf.MaxOpenConn > 0 { engine.SetMaxOpenConns(dataConf.MaxOpenConn) } if dataConf.ConnMaxLifeTime > 0 { engine.SetConnMaxLifetime(time.Duration(dataConf.ConnMaxLifeTime) * time.Second) } engine.SetColumnMapper(names.GonicMapper{}) return engine, nil } // NewCache new cache instance func NewCache(c *CacheConf) (cache.Cache, func(), error) { var pluginCache plugin.Cache _ = plugin.CallCache(func(fn plugin.Cache) error { pluginCache = fn return nil }) if pluginCache != nil { return pluginCache, func() {}, nil } // TODO What cache type should be initialized according to the configuration file memCache := memory.NewCache() if len(c.FilePath) > 0 { cacheFileDir := filepath.Dir(c.FilePath) log.Debugf("try to create cache directory %s", cacheFileDir) err := dir.CreateDirIfNotExist(cacheFileDir) if err != nil { log.Errorf("create cache dir failed: %s", err) } log.Infof("try to load cache file from %s", c.FilePath) if err := memory.Load(memCache, c.FilePath); err != nil { log.Warn(err) } go func() { ticker := time.Tick(time.Minute) for range ticker { if err := memory.Save(memCache, c.FilePath); err != nil { log.Warn(err) } } }() } cleanup := func() { log.Infof("try to save cache file to %s", c.FilePath) if err := memory.Save(memCache, c.FilePath); err != nil { log.Warn(err) } } return memCache, cleanup, nil } ================================================ FILE: internal/base/handler/handler.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package handler import ( "errors" "net/http" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/validator" "github.com/gin-gonic/gin" myErrors "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // HandleResponse Handle response body func HandleResponse(ctx *gin.Context, err error, data any) { lang := GetLangByCtx(ctx) // no error if err == nil { ctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, reason.Success, data).TrMsg(lang)) return } var myErr *myErrors.Error // unknown error if !errors.As(err, &myErr) { log.Error(err, "\n", myErrors.LogStack(2, 5)) ctx.JSON(http.StatusInternalServerError, NewRespBody( http.StatusInternalServerError, reason.UnknownError).TrMsg(lang)) return } // log internal server error if myErrors.IsInternalServer(myErr) { log.Error(myErr) } respBody := NewRespBodyFromError(myErr).TrMsg(lang) if data != nil { respBody.Data = data } ctx.JSON(myErr.Code, respBody) } // BindAndCheck bind request and check func BindAndCheck(ctx *gin.Context, data any) bool { lang := GetLangByCtx(ctx) if err := ctx.ShouldBind(data); err != nil { log.Errorf("http_handle BindAndCheck fail, %s", err.Error()) HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError), nil) return true } errField, err := validator.GetValidatorByLang(lang).Check(data) if err != nil { HandleResponse(ctx, err, errField) return true } return false } // BindAndCheckReturnErr bind request and check func BindAndCheckReturnErr(ctx *gin.Context, data any) (errFields []*validator.FormErrorField) { lang := GetLangByCtx(ctx) if err := ctx.ShouldBind(data); err != nil { log.Errorf("http_handle BindAndCheck fail, %s", err.Error()) HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError), nil) ctx.Abort() return nil } errFields, _ = validator.GetValidatorByLang(lang).Check(data) return errFields } ================================================ FILE: internal/base/handler/lang.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package handler import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) // GetLangByCtx get language from header func GetLangByCtx(ctx context.Context) i18n.Language { if ginCtx, ok := ctx.(*gin.Context); ok { acceptLanguage, ok := ginCtx.Get(constant.AcceptLanguageFlag) if ok { if acceptLanguage, ok := acceptLanguage.(i18n.Language); ok { return acceptLanguage } return i18n.DefaultLanguage } } acceptLanguage, ok := ctx.Value(constant.AcceptLanguageContextKey).(i18n.Language) if ok { return acceptLanguage } return i18n.DefaultLanguage } ================================================ FILE: internal/base/handler/response.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package handler import ( "github.com/apache/answer/internal/base/translator" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/i18n" ) // RespBody response body. type RespBody struct { // http code Code int `json:"code"` // reason key Reason string `json:"reason"` // response message Message string `json:"msg"` // response data Data any `json:"data"` } // TrMsg translate the reason cause as a message func (r *RespBody) TrMsg(lang i18n.Language) *RespBody { if len(r.Message) == 0 { r.Message = translator.Tr(lang, r.Reason) } return r } // NewRespBody new response body func NewRespBody(code int, reason string) *RespBody { return &RespBody{ Code: code, Reason: reason, } } // NewRespBodyFromError new response body from error func NewRespBodyFromError(e *errors.Error) *RespBody { return &RespBody{ Code: e.Code, Reason: e.Reason, Message: e.Message, } } // NewRespBodyData new response body with data func NewRespBodyData(code int, reason string, data any) *RespBody { return &RespBody{ Code: code, Reason: reason, Data: data, } } ================================================ FILE: internal/base/handler/short_id.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package handler import ( "context" "github.com/apache/answer/internal/base/constant" ) // GetEnableShortID get language from header func GetEnableShortID(ctx context.Context) bool { flag, ok := ctx.Value(constant.ShortIDContextKey).(bool) if ok { return flag } return false } ================================================ FILE: internal/base/middleware/accept_language.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "strings" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/translator" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" "golang.org/x/text/language" ) // ExtractAndSetAcceptLanguage extract accept language from header and set to context func ExtractAndSetAcceptLanguage(ctx *gin.Context) { // The language of our front-end configuration, like en_US acceptLanguage := ctx.GetHeader(constant.AcceptLanguageFlag) tag, _, err := language.ParseAcceptLanguage(acceptLanguage) if err != nil || len(tag) == 0 { ctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish) return } acceptLang := strings.ReplaceAll(tag[0].String(), "-", "_") for _, option := range translator.LanguageOptions { if option.Value == acceptLang { ctx.Set(constant.AcceptLanguageFlag, i18n.Language(acceptLang)) return } } // default language ctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish) } ================================================ FILE: internal/base/middleware/api_key_auth.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // AuthAPIKey middleware to authenticate API key func (am *AuthUserMiddleware) AuthAPIKey() gin.HandlerFunc { return func(ctx *gin.Context) { token := ExtractToken(ctx) if len(token) == 0 { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } pass, err := am.authService.AuthAPIKey(ctx, ctx.Request.Method == "GET", token) if err != nil { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } if !pass { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } ctx.Next() } } ================================================ FILE: internal/base/middleware/auth.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "net/http" "strings" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/role" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/ui" "github.com/gin-gonic/gin" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) var ctxUUIDKey = "ctxUuidKey" // AuthUserMiddleware auth user middleware type AuthUserMiddleware struct { authService *auth.AuthService siteInfoCommonService siteinfo_common.SiteInfoCommonService } // NewAuthUserMiddleware new auth user middleware func NewAuthUserMiddleware( authService *auth.AuthService, siteInfoCommonService siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware { return &AuthUserMiddleware{ authService: authService, siteInfoCommonService: siteInfoCommonService, } } // Auth get token and auth user, set user info to context if user is already login func (am *AuthUserMiddleware) Auth() gin.HandlerFunc { return func(ctx *gin.Context) { token := ExtractToken(ctx) if len(token) == 0 { ctx.Next() return } userInfo, err := am.authService.GetUserCacheInfo(ctx, token) if err != nil { ctx.Next() return } if userInfo != nil { ctx.Set(ctxUUIDKey, userInfo) } ctx.Next() } } // EjectUserBySiteInfo if admin config the site can access by nologin user, eject user. func (am *AuthUserMiddleware) EjectUserBySiteInfo() gin.HandlerFunc { return func(ctx *gin.Context) { mustLogin := false siteInfo, _ := am.siteInfoCommonService.GetSiteSecurity(ctx) if siteInfo != nil { mustLogin = siteInfo.LoginRequired } if !mustLogin { ctx.Next() return } // If site in private mode, user must login. userInfo := GetUserInfoFromContext(ctx) if userInfo == nil { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } // If user is not active, eject user. if userInfo.EmailStatus != entity.EmailStatusAvailable { handler.HandleResponse(ctx, errors.Forbidden(reason.EmailNeedToBeVerified), &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeInactive}) ctx.Abort() return } ctx.Next() } } // MustAuthWithoutAccountAvailable auth user info, any login user can access though user is not active. func (am *AuthUserMiddleware) MustAuthWithoutAccountAvailable() gin.HandlerFunc { return func(ctx *gin.Context) { token := ExtractToken(ctx) if len(token) == 0 { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } userInfo, err := am.authService.GetUserCacheInfo(ctx, token) if err != nil || userInfo == nil { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } if userInfo.UserStatus == entity.UserStatusDeleted { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } ctx.Set(ctxUUIDKey, userInfo) ctx.Next() } } // MustAuthAndAccountAvailable auth user info and check user status, only allow active user access. func (am *AuthUserMiddleware) MustAuthAndAccountAvailable() gin.HandlerFunc { return func(ctx *gin.Context) { token := ExtractToken(ctx) if len(token) == 0 { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } userInfo, err := am.authService.GetUserCacheInfo(ctx, token) if err != nil || userInfo == nil { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } if userInfo.EmailStatus != entity.EmailStatusAvailable { handler.HandleResponse(ctx, errors.Forbidden(reason.EmailNeedToBeVerified), &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeInactive}) ctx.Abort() return } if userInfo.UserStatus == entity.UserStatusSuspended { handler.HandleResponse(ctx, errors.Forbidden(reason.UserSuspended), &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUserSuspended}) ctx.Abort() return } if userInfo.UserStatus == entity.UserStatusDeleted { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } ctx.Set(ctxUUIDKey, userInfo) ctx.Next() } } func (am *AuthUserMiddleware) AdminAuth() gin.HandlerFunc { return func(ctx *gin.Context) { token := ExtractToken(ctx) if len(token) == 0 { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } userInfo, err := am.authService.GetAdminUserCacheInfo(ctx, token) if err != nil || userInfo == nil { handler.HandleResponse(ctx, errors.Forbidden(reason.UnauthorizedError), nil) ctx.Abort() return } if userInfo != nil { if userInfo.UserStatus == entity.UserStatusDeleted { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return } ctx.Set(ctxUUIDKey, userInfo) } ctx.Next() } } func (am *AuthUserMiddleware) CheckPrivateMode() gin.HandlerFunc { return func(ctx *gin.Context) { resp, err := am.siteInfoCommonService.GetSiteSecurity(ctx) if err != nil { ShowIndexPage(ctx) ctx.Abort() return } if resp.LoginRequired { ShowIndexPage(ctx) ctx.Abort() return } ctx.Next() } } func ShowIndexPage(ctx *gin.Context) { ctx.Header("content-type", "text/html;charset=utf-8") ctx.Header("X-Frame-Options", "DENY") file, err := ui.Build.ReadFile("build/index.html") if err != nil { log.Error(err) ctx.Status(http.StatusNotFound) return } ctx.String(http.StatusOK, string(file)) } // GetLoginUserIDFromContext get user id from context func GetLoginUserIDFromContext(ctx *gin.Context) (userID string) { userInfo := GetUserInfoFromContext(ctx) if userInfo == nil { return "" } return userInfo.UserID } // GetIsAdminFromContext get user is admin from context func GetIsAdminFromContext(ctx *gin.Context) (isAdmin bool) { userInfo := GetUserInfoFromContext(ctx) if userInfo == nil { return false } return userInfo.RoleID == role.RoleAdminID } // GetUserInfoFromContext get user info from context func GetUserInfoFromContext(ctx *gin.Context) (u *entity.UserCacheInfo) { userInfo, exist := ctx.Get(ctxUUIDKey) if !exist { return nil } u, ok := userInfo.(*entity.UserCacheInfo) if !ok { return nil } return u } func GetUserIsAdminModerator(ctx *gin.Context) (isAdminModerator bool) { userInfo, exist := ctx.Get(ctxUUIDKey) if !exist { return false } u, ok := userInfo.(*entity.UserCacheInfo) if !ok { return false } if u.RoleID == role.RoleAdminID || u.RoleID == role.RoleModeratorID { return true } return false } func GetLoginUserIDInt64FromContext(ctx *gin.Context) (userID int64) { userIDStr := GetLoginUserIDFromContext(ctx) return converter.StringToInt64(userIDStr) } // ExtractToken extract token from context func ExtractToken(ctx *gin.Context) (token string) { token = ctx.GetHeader("Authorization") if len(token) == 0 { token = ctx.Query("Authorization") } return strings.TrimPrefix(token, "Bearer ") } ================================================ FILE: internal/base/middleware/avatar.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "fmt" "net/http" "net/url" "os" "path" "path/filepath" "strings" "github.com/apache/answer/internal/service/service_config" "github.com/apache/answer/internal/service/uploader" "github.com/apache/answer/pkg/converter" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) type AvatarMiddleware struct { serviceConfig *service_config.ServiceConfig uploaderService uploader.UploaderService } // NewAvatarMiddleware new auth user middleware func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig, uploaderService uploader.UploaderService, ) *AvatarMiddleware { return &AvatarMiddleware{ serviceConfig: serviceConfig, uploaderService: uploaderService, } } func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { return func(ctx *gin.Context) { uri := ctx.Request.RequestURI if strings.Contains(uri, "/uploads/avatar/") { size := converter.StringToInt(ctx.Query("s")) uriWithoutQuery, _ := url.Parse(uri) filename := filepath.Base(uriWithoutQuery.Path) filePath := fmt.Sprintf("%s/avatar/%s", am.serviceConfig.UploadPath, filename) var err error if size != 0 { filePath, err = am.uploaderService.AvatarThumbFile(ctx, filename, size) if err != nil { log.Error(err) ctx.AbortWithStatus(http.StatusNotFound) return } } avatarFile, err := os.ReadFile(filePath) if err != nil { log.Error(err) ctx.Abort() return } ctx.Header("content-type", fmt.Sprintf("image/%s", strings.TrimLeft(path.Ext(filePath), "."))) _, err = ctx.Writer.Write(avatarFile) if err != nil { log.Error(err) } ctx.Abort() return } else { urlInfo, err := url.Parse(uri) if err != nil { ctx.Next() return } ext := strings.TrimPrefix(filepath.Ext(urlInfo.Path), ".") ctx.Header("content-type", fmt.Sprintf("image/%s", ext)) } ctx.Next() } } ================================================ FILE: internal/base/middleware/header.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "strings" "github.com/gin-gonic/gin" ) func HeadersByRequestURI() gin.HandlerFunc { return func(c *gin.Context) { if strings.HasPrefix(c.Request.RequestURI, "/static/") { c.Header("cache-control", "public, max-age=31536000") } } } ================================================ FILE: internal/base/middleware/mcp_auth.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // AuthMcpEnable check mcp is enabled func (am *AuthUserMiddleware) AuthMcpEnable() gin.HandlerFunc { return func(ctx *gin.Context) { mcpConfig, err := am.siteInfoCommonService.GetSiteMCP(ctx) if err != nil { handler.HandleResponse(ctx, errors.InternalServer(reason.UnknownError), nil) ctx.Abort() return } if mcpConfig != nil && mcpConfig.Enabled { ctx.Next() return } handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) ctx.Abort() log.Error("abort mcp auth middleware, get mcp config error: ", err) } } ================================================ FILE: internal/base/middleware/provider.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "github.com/google/wire" ) // ProviderSetMiddleware is providers. var ProviderSetMiddleware = wire.NewSet( NewAuthUserMiddleware, NewAvatarMiddleware, NewShortIDMiddleware, NewRateLimitMiddleware, ) ================================================ FILE: internal/base/middleware/rate_limit.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "encoding/json" "fmt" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/repo/limit" "github.com/apache/answer/pkg/encryption" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) type RateLimitMiddleware struct { limitRepo *limit.LimitRepo } // NewRateLimitMiddleware new rate limit middleware func NewRateLimitMiddleware(limitRepo *limit.LimitRepo) *RateLimitMiddleware { return &RateLimitMiddleware{ limitRepo: limitRepo, } } // DuplicateRequestRejection detects and rejects duplicate requests // It only works for the requests that post content. Such as add question, add answer, comment etc. func (rm *RateLimitMiddleware) DuplicateRequestRejection(ctx *gin.Context, req any) (reject bool, key string) { userID := GetLoginUserIDFromContext(ctx) fullPath := ctx.FullPath() reqJson, _ := json.Marshal(req) key = encryption.MD5(fmt.Sprintf("%s:%s:%s", userID, fullPath, string(reqJson))) var err error reject, err = rm.limitRepo.CheckAndRecord(ctx, key) if err != nil { log.Errorf("check and record rate limit error: %s", err.Error()) return false, key } if !reject { return false, key } log.Debugf("duplicate request: [%s] %s", fullPath, string(reqJson)) handler.HandleResponse(ctx, errors.BadRequest(reason.DuplicateRequestError), nil) return true, key } // DuplicateRequestClear clear duplicate request record func (rm *RateLimitMiddleware) DuplicateRequestClear(ctx *gin.Context, key string) { err := rm.limitRepo.ClearRecord(ctx, key) if err != nil { log.Errorf("clear rate limit error: %s", err.Error()) } } ================================================ FILE: internal/base/middleware/short_id.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) type ShortIDMiddleware struct { siteInfoService siteinfo_common.SiteInfoCommonService } func NewShortIDMiddleware(siteInfoService siteinfo_common.SiteInfoCommonService) *ShortIDMiddleware { return &ShortIDMiddleware{ siteInfoService: siteInfoService, } } func (sm *ShortIDMiddleware) SetShortIDFlag() gin.HandlerFunc { return func(ctx *gin.Context) { siteSeo, err := sm.siteInfoService.GetSiteSeo(ctx) if err != nil { log.Error(err) return } ctx.Set(constant.ShortIDFlag, siteSeo.IsShortLink()) } } ================================================ FILE: internal/base/middleware/user_center_plugin_auth.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // BanAPIForUserCenter ban api for user center func BanAPIForUserCenter(ctx *gin.Context) { uc, ok := plugin.GetUserCenter() if !ok { return } if !uc.Description().EnabledOriginalUserSystem { handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) ctx.Abort() return } ctx.Next() } ================================================ FILE: internal/base/middleware/visit_img_auth.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package middleware import ( "net/http" "os" "strings" "github.com/apache/answer/internal/base/constant" "github.com/gin-gonic/gin" ) // VisitAuth when user visit the site image, check visit token. This only for private mode. func (am *AuthUserMiddleware) VisitAuth() gin.HandlerFunc { return func(ctx *gin.Context) { if len(os.Getenv("SKIP_FILE_ACCESS_VERIFY")) > 0 { ctx.Next() return } // If visit brand image, no need to check visit token. Because the brand image is public. if strings.HasPrefix(ctx.Request.URL.Path, "/uploads/branding/") { ctx.Next() return } siteSecurity, err := am.siteInfoCommonService.GetSiteSecurity(ctx) if err != nil { return } if !siteSecurity.LoginRequired { ctx.Next() return } visitToken, err := ctx.Cookie(constant.UserVisitCookiesCacheKey) if err != nil || len(visitToken) == 0 { ctx.Abort() ctx.Redirect(http.StatusFound, "/403") return } if !am.authService.CheckUserVisitToken(ctx, visitToken) { ctx.Abort() ctx.Redirect(http.StatusFound, "/403") return } } } ================================================ FILE: internal/base/pager/pager.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package pager import ( "errors" "reflect" "xorm.io/xorm" ) // Help xorm page helper func Help(page, pageSize int, rowsSlicePtr any, rowElement any, session *xorm.Session) (total int64, err error) { page, pageSize = ValPageAndPageSize(page, pageSize) sliceValue := reflect.Indirect(reflect.ValueOf(rowsSlicePtr)) if sliceValue.Kind() != reflect.Slice { return 0, errors.New("not a slice") } startNum := (page - 1) * pageSize return session.Limit(pageSize, startNum).FindAndCount(rowsSlicePtr, rowElement) } ================================================ FILE: internal/base/pager/pagination.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package pager import ( "reflect" ) // PageModel page model type PageModel struct { Count int64 `json:"count"` List any `json:"list"` } // PageCond page condition type PageCond struct { Page int PageSize int } // NewPageModel new page model func NewPageModel(totalRecords int64, records any) *PageModel { sliceValue := reflect.Indirect(reflect.ValueOf(records)) if sliceValue.Kind() != reflect.Slice { panic("not a slice") } if totalRecords < 0 { totalRecords = 0 } return &PageModel{ Count: totalRecords, List: records, } } // ValPageAndPageSize validate page pageSize func ValPageAndPageSize(page, pageSize int) (int, int) { if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 10 } return page, pageSize } // ValPageOutOfRange validate page out of range func ValPageOutOfRange(total int64, page, pageSize int) bool { if total <= 0 { return false } if pageSize <= 0 { return true } totalPages := (total + int64(pageSize) - 1) / int64(pageSize) return page < 1 || page > int(totalPages) } ================================================ FILE: internal/base/path/path.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package path import ( "path/filepath" "sync" ) const ( DefaultConfigFileName = "config.yaml" DefaultCacheFileName = "cache.db" DefaultReservedUsernamesConfigFileName = "reserved-usernames.json" ) var ( ConfigFileDir = "/conf/" UploadFilePath = "/uploads/" I18nPath = "/i18n/" CacheDir = "/cache/" formatAllPathOnce sync.Once ) func FormatAllPath(dataDirPath string) { formatAllPathOnce.Do(func() { ConfigFileDir = filepath.Join(dataDirPath, ConfigFileDir) UploadFilePath = filepath.Join(dataDirPath, UploadFilePath) I18nPath = filepath.Join(dataDirPath, I18nPath) CacheDir = filepath.Join(dataDirPath, CacheDir) }) } // GetConfigFilePath get config file path func GetConfigFilePath() string { return filepath.Join(ConfigFileDir, DefaultConfigFileName) } ================================================ FILE: internal/base/queue/queue.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package queue import ( "context" "sync" "github.com/segmentfault/pacman/log" ) type Service[T any] interface { // Send enqueues a message to be processed asynchronously. Send(ctx context.Context, msg T) // RegisterHandler sets the handler function for processing messages. RegisterHandler(handler func(ctx context.Context, msg T) error) // Close gracefully shuts down the queue, waiting for pending messages to be processed. Close() } // Queue is a generic message queue service that processes messages asynchronously. // It is thread-safe and supports graceful shutdown. type Queue[T any] struct { name string queue chan T handler func(ctx context.Context, msg T) error mu sync.RWMutex closed bool wg sync.WaitGroup } // New creates a new queue with the given name and buffer size. func New[T any](name string, bufferSize int) *Queue[T] { q := &Queue[T]{ name: name, queue: make(chan T, bufferSize), } q.startWorker() return q } // Send enqueues a message to be processed asynchronously. // It will block if the queue is full. func (q *Queue[T]) Send(ctx context.Context, msg T) { q.mu.RLock() defer q.mu.RUnlock() if q.closed { log.Warnf("[%s] queue is closed, dropping message", q.name) return } select { case q.queue <- msg: log.Debugf("[%s] enqueued message: %+v", q.name, msg) case <-ctx.Done(): log.Warnf("[%s] context cancelled while sending message", q.name) } } // RegisterHandler sets the handler function for processing messages. // This is thread-safe and can be called at any time. func (q *Queue[T]) RegisterHandler(handler func(ctx context.Context, msg T) error) { q.mu.Lock() defer q.mu.Unlock() q.handler = handler } // Close gracefully shuts down the queue, waiting for pending messages to be processed. func (q *Queue[T]) Close() { q.mu.Lock() if q.closed { q.mu.Unlock() return } q.closed = true q.mu.Unlock() close(q.queue) q.wg.Wait() log.Infof("[%s] queue closed", q.name) } // startWorker starts the background goroutine that processes messages. func (q *Queue[T]) startWorker() { q.wg.Add(1) go func() { defer q.wg.Done() for msg := range q.queue { q.processMessage(msg) } }() } // processMessage handles a single message with proper synchronization. func (q *Queue[T]) processMessage(msg T) { q.mu.RLock() handler := q.handler q.mu.RUnlock() if handler == nil { log.Warnf("[%s] no handler registered, dropping message: %+v", q.name, msg) return } // Use background context for async processing // TODO: Consider adding timeout or using a derived context if err := handler(context.TODO(), msg); err != nil { log.Errorf("[%s] handler error: %v", q.name, err) } } ================================================ FILE: internal/base/queue/queue_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package queue import ( "context" "fmt" "sync" "sync/atomic" "testing" "time" ) type testMessage struct { ID int Data string } func TestQueue_SendAndReceive(t *testing.T) { q := New[*testMessage]("test", 10) defer q.Close() received := make(chan *testMessage, 1) q.RegisterHandler(func(ctx context.Context, msg *testMessage) error { received <- msg return nil }) msg := &testMessage{ID: 1, Data: "hello"} q.Send(context.Background(), msg) select { case r := <-received: if r.ID != msg.ID || r.Data != msg.Data { t.Errorf("received message mismatch: got %+v, want %+v", r, msg) } case <-time.After(time.Second): t.Fatal("timeout waiting for message") } } func TestQueue_MultipleMessages(t *testing.T) { q := New[*testMessage]("test", 10) defer q.Close() var count atomic.Int32 var wg sync.WaitGroup numMessages := 100 wg.Add(numMessages) q.RegisterHandler(func(ctx context.Context, msg *testMessage) error { count.Add(1) wg.Done() return nil }) for i := range numMessages { q.Send(context.Background(), &testMessage{ID: i}) } done := make(chan struct{}) go func() { wg.Wait() close(done) }() select { case <-done: if int(count.Load()) != numMessages { t.Errorf("expected %d messages, got %d", numMessages, count.Load()) } case <-time.After(5 * time.Second): t.Fatalf("timeout: only received %d of %d messages", count.Load(), numMessages) } } func TestQueue_NoHandlerDropsMessage(t *testing.T) { q := New[*testMessage]("test", 10) defer q.Close() // Send without handler - should not panic q.Send(context.Background(), &testMessage{ID: 1}) // Give time for the message to be processed (dropped) time.Sleep(100 * time.Millisecond) } func TestQueue_RegisterHandlerAfterSend(t *testing.T) { q := New[*testMessage]("test", 10) defer q.Close() received := make(chan *testMessage, 1) // Send first q.Send(context.Background(), &testMessage{ID: 1}) // Small delay then register handler time.Sleep(50 * time.Millisecond) q.RegisterHandler(func(ctx context.Context, msg *testMessage) error { received <- msg return nil }) // Send another message that should be received q.Send(context.Background(), &testMessage{ID: 2}) select { case r := <-received: if r.ID != 2 { // First message was dropped (no handler), second should be received t.Logf("received message ID: %d", r.ID) } case <-time.After(time.Second): t.Fatal("timeout waiting for message") } } func TestQueue_Close(t *testing.T) { q := New[*testMessage]("test", 10) var count atomic.Int32 q.RegisterHandler(func(ctx context.Context, msg *testMessage) error { count.Add(1) return nil }) // Send some messages for i := range 5 { q.Send(context.Background(), &testMessage{ID: i}) } // Close and wait q.Close() // All messages should have been processed if count.Load() != 5 { t.Errorf("expected 5 messages processed, got %d", count.Load()) } // Sending after close should not panic q.Send(context.Background(), &testMessage{ID: 99}) } func TestQueue_ConcurrentSend(t *testing.T) { q := New[*testMessage]("test", 100) defer q.Close() var count atomic.Int32 q.RegisterHandler(func(ctx context.Context, msg *testMessage) error { count.Add(1) return nil }) var wg sync.WaitGroup numGoroutines := 10 messagesPerGoroutine := 100 for i := range numGoroutines { wg.Add(1) go func(id int) { defer wg.Done() for j := range messagesPerGoroutine { q.Send(context.Background(), &testMessage{ID: id*1000 + j}) } }(i) } wg.Wait() // Wait for processing time.Sleep(500 * time.Millisecond) expected := int32(numGoroutines * messagesPerGoroutine) if count.Load() != expected { t.Errorf("expected %d messages, got %d", expected, count.Load()) } } func TestQueue_ConcurrentRegisterHandler(t *testing.T) { q := New[*testMessage]("test", 10) defer q.Close() // Concurrently register handlers - should not race var wg sync.WaitGroup for range 10 { wg.Add(1) go func() { defer wg.Done() q.RegisterHandler(func(ctx context.Context, msg *testMessage) error { return nil }) }() } wg.Wait() } // TestQueue_SendCloseRace is a regression test for the race condition between // Send and Close. Without proper synchronization, concurrent Send and Close // calls could cause a "send on closed channel" panic. // Run with: go test -race -run TestQueue_SendCloseRace func TestQueue_SendCloseRace(t *testing.T) { for i := range 100 { t.Run(fmt.Sprintf("iteration_%d", i), func(t *testing.T) { // Use large buffer to avoid blocking on channel send while holding RLock q := New[*testMessage]("test-race", 1000) q.RegisterHandler(func(ctx context.Context, msg *testMessage) error { return nil }) var wg sync.WaitGroup // Use cancellable context so senders can exit when Close is called ctx, cancel := context.WithCancel(context.Background()) // Start multiple senders for j := range 10 { wg.Add(1) go func(id int) { defer wg.Done() for k := range 100 { q.Send(ctx, &testMessage{ID: id*1000 + k}) } }(j) } // Close while senders are still running go func() { time.Sleep(time.Microsecond * 10) cancel() // Cancel context to unblock any waiting senders q.Close() }() wg.Wait() }) } } ================================================ FILE: internal/base/reason/privilege.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package reason const ( PrivilegeLevel1Desc = "privilege.level_1.description" PrivilegeLevel2Desc = "privilege.level_2.description" PrivilegeLevel3Desc = "privilege.level_3.description" PrivilegeLevelCustomDesc = "privilege.level_custom.description" RankQuestionAddLabel = "privilege.rank_question_add_label" RankAnswerAddLabel = "privilege.rank_answer_add_label" RankCommentAddLabel = "privilege.rank_comment_add_label" RankReportAddLabel = "privilege.rank_report_add_label" RankCommentVoteUpLabel = "privilege.rank_comment_vote_up_label" RankLinkUrlLimitLabel = "privilege.rank_link_url_limit_label" RankQuestionVoteUpLabel = "privilege.rank_question_vote_up_label" RankAnswerVoteUpLabel = "privilege.rank_answer_vote_up_label" RankQuestionVoteDownLabel = "privilege.rank_question_vote_down_label" RankAnswerVoteDownLabel = "privilege.rank_answer_vote_down_label" RankInviteSomeoneToAnswerLabel = "privilege.rank_invite_someone_to_answer_label" RankTagAddLabel = "privilege.rank_tag_add_label" RankTagEditLabel = "privilege.rank_tag_edit_label" RankQuestionEditLabel = "privilege.rank_question_edit_label" RankAnswerEditLabel = "privilege.rank_answer_edit_label" RankQuestionEditWithoutReviewLabel = "privilege.rank_question_edit_without_review_label" RankAnswerEditWithoutReviewLabel = "privilege.rank_answer_edit_without_review_label" RankQuestionAuditLabel = "privilege.rank_question_audit_label" RankAnswerAuditLabel = "privilege.rank_answer_audit_label" RankTagAuditLabel = "privilege.rank_tag_audit_label" RankTagEditWithoutReviewLabel = "privilege.rank_tag_edit_without_review_label" RankTagSynonymLabel = "privilege.rank_tag_synonym_label" ) ================================================ FILE: internal/base/reason/reason.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package reason const ( // Success . Success = "base.success" // UnknownError unknown error UnknownError = "base.unknown" // RequestFormatError request format error RequestFormatError = "base.request_format_error" // UnauthorizedError unauthorized error UnauthorizedError = "base.unauthorized_error" // DatabaseError database error DatabaseError = "base.database_error" // ForbiddenError forbidden error ForbiddenError = "base.forbidden_error" // DuplicateRequestError duplicate request error DuplicateRequestError = "base.duplicate_request_error" ) const ( EmailOrPasswordWrong = "error.object.email_or_password_incorrect" CommentNotFound = "error.comment.not_found" CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline" QuestionNotFound = "error.question.not_found" QuestionCannotDeleted = "error.question.cannot_deleted" QuestionCannotClose = "error.question.cannot_close" QuestionCannotUpdate = "error.question.cannot_update" QuestionAlreadyDeleted = "error.question.already_deleted" QuestionUnderReview = "error.question.under_review" QuestionContentCannotEmpty = "error.question.content_cannot_empty" QuestionContentLessThanMinimum = "error.question.content_less_than_minimum" AnswerNotFound = "error.answer.not_found" AnswerCannotDeleted = "error.answer.cannot_deleted" AnswerCannotUpdate = "error.answer.cannot_update" AnswerCannotAddByClosedQuestion = "error.answer.question_closed_cannot_add" AnswerRestrictAnswer = "error.answer.restrict_answer" AnswerContentCannotEmpty = "error.answer.content_cannot_empty" CommentEditWithoutPermission = "error.comment.edit_without_permission" CommentContentCannotEmpty = "error.comment.content_cannot_empty" DisallowVote = "error.object.disallow_vote" DisallowFollow = "error.object.disallow_follow" DisallowVoteYourSelf = "error.object.disallow_vote_your_self" CaptchaVerificationFailed = "error.object.captcha_verification_failed" OldPasswordVerificationFailed = "error.object.old_password_verification_failed" NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting" NewObjectAlreadyDeleted = "error.object.already_deleted" UserNotFound = "error.user.not_found" UsernameInvalid = "error.user.username_invalid" UsernameDuplicate = "error.user.username_duplicate" UserSetAvatar = "error.user.set_avatar" EmailDuplicate = "error.email.duplicate" EmailVerifyURLExpired = "error.email.verify_url_expired" EmailNeedToBeVerified = "error.email.need_to_be_verified" EmailIllegalDomainError = "error.email.illegal_email_domain_error" UserSuspended = "error.user.suspended" ObjectNotFound = "error.object.not_found" TagNotFound = "error.tag.not_found" TagNotContainSynonym = "error.tag.not_contain_synonym_tags" TagCannotUpdate = "error.tag.cannot_update" TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete" TagAlreadyExist = "error.tag.already_exist" TagMinCount = "error.tag.minimum_count" RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition" VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition" NoEnoughRankToOperate = "error.rank.no_enough_rank_to_operate" ThemeNotFound = "error.theme.not_found" LangNotFound = "error.lang.not_found" ReportHandleFailed = "error.report.handle_failed" ReportNotFound = "error.report.not_found" ReadConfigFailed = "error.config.read_config_failed" DatabaseConnectionFailed = "error.database.connection_failed" InstallCreateTableFailed = "error.database.create_table_failed" InstallConfigFailed = "error.install.create_config_failed" SiteInfoConfigNotFound = "error.site_info.config_not_found" UploadFileSourceUnsupported = "error.upload.source_unsupported" UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format" RecommendTagNotExist = "error.tag.recommend_tag_not_found" RecommendTagEnter = "error.tag.recommend_tag_enter" RevisionReviewUnderway = "error.revision.review_underway" RevisionNoPermission = "error.revision.no_permission" UserCannotUpdateYourRole = "error.user.cannot_update_your_role" TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself" NotAllowedRegistration = "error.user.not_allowed_registration" NotAllowedLoginViaPassword = "error.user.not_allowed_login_via_password" SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email" AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password" AdminCannotEditTheirProfile = "error.admin.cannot_edit_their_profile" AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status" UserAccessDenied = "error.user.access_denied" UserPageAccessDenied = "error.user.page_access_denied" AddBulkUsersFormatError = "error.user.add_bulk_users_format_error" AddBulkUsersAmountError = "error.user.add_bulk_users_amount_error" InvalidURLError = "error.common.invalid_url" MetaObjectNotFound = "error.meta.object_not_found" BadgeObjectNotFound = "error.badge.object_not_found" StatusInvalid = "error.common.status_invalid" UserStatusInactive = "error.user.status_inactive" UserStatusSuspendedForever = "error.user.status_suspended_forever" UserStatusSuspendedUntil = "error.user.status_suspended_until" UserStatusDeleted = "error.user.status_deleted" ErrFeatureDisabled = "error.feature.disabled" ) // user external login reasons const ( UserExternalLoginUnbindingForbidden = "error.user.external_login_unbinding_forbidden" UserExternalLoginMissingUserID = "error.user.external_login_missing_user_id" ) ================================================ FILE: internal/base/server/config.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package server // HTTP http config type HTTP struct { Addr string `json:"addr" mapstructure:"addr"` } // UI ui config type UI struct { BaseURL string `json:"base_url" mapstructure:"base_url" yaml:"base_url"` APIBaseURL string `json:"api_base_url" mapstructure:"api_base_url" yaml:"api_base_url"` } ================================================ FILE: internal/base/server/http.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package server import ( "html/template" "io/fs" "os" "strings" brotli "github.com/anargu/gin-brotli" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/router" "github.com/apache/answer/plugin" "github.com/apache/answer/ui" "github.com/gin-gonic/gin" ) // NewHTTPServer new http server. func NewHTTPServer(debug bool, staticRouter *router.StaticRouter, answerRouter *router.AnswerAPIRouter, swaggerRouter *router.SwaggerRouter, viewRouter *router.UIRouter, authUserMiddleware *middleware.AuthUserMiddleware, avatarMiddleware *middleware.AvatarMiddleware, shortIDMiddleware *middleware.ShortIDMiddleware, templateRouter *router.TemplateRouter, pluginAPIRouter *router.PluginAPIRouter, uiConf *UI, ) *gin.Engine { if debug { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } r := gin.New() r.Use(func(ctx *gin.Context) { if strings.Contains(ctx.Request.URL.Path, "/chat/completions") { return } brotli.Brotli(brotli.DefaultCompression)(ctx) }, middleware.ExtractAndSetAcceptLanguage, shortIDMiddleware.SetShortIDFlag()) r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") }) templatePath := os.Getenv("ANSWER_TEMPLATE_PATH") if templatePath != "" { r.LoadHTMLGlob(templatePath) } else { html, _ := fs.Sub(ui.Template, "template") htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*")) r.SetHTMLTemplate(htmlTemplate) } r.Use(middleware.HeadersByRequestURI()) viewRouter.Register(r, uiConf.BaseURL) rootGroup := r.Group("") swaggerRouter.Register(rootGroup) static := r.Group(uiConf.APIBaseURL) static.Use(avatarMiddleware.AvatarThumb(), authUserMiddleware.VisitAuth()) staticRouter.RegisterStaticRouter(static) // The route must be available without logging in mustUnAuthV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1") answerRouter.RegisterMustUnAuthAnswerAPIRouter(authUserMiddleware, mustUnAuthV1) // register api that no need to login unAuthV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1") unAuthV1.Use(authUserMiddleware.Auth(), authUserMiddleware.EjectUserBySiteInfo()) answerRouter.RegisterUnAuthAnswerAPIRouter(unAuthV1) // register api that must be authenticated but no need to check account status authWithoutStatusV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1") authWithoutStatusV1.Use(authUserMiddleware.MustAuthWithoutAccountAvailable()) answerRouter.RegisterAuthUserWithAnyStatusAnswerAPIRouter(authWithoutStatusV1) // register api that must be authenticated authV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1") authV1.Use(authUserMiddleware.MustAuthAndAccountAvailable()) answerRouter.RegisterAnswerAPIRouter(authV1) adminauthV1 := r.Group(uiConf.APIBaseURL + "/answer/admin/api") adminauthV1.Use(authUserMiddleware.AdminAuth()) answerRouter.RegisterAnswerAdminAPIRouter(adminauthV1) templateRouter.RegisterTemplateRouter(rootGroup, uiConf.BaseURL) // plugin routes pluginAPIRouter.RegisterUnAuthConnectorRouter(mustUnAuthV1) pluginAPIRouter.RegisterAuthUserConnectorRouter(authV1) pluginAPIRouter.RegisterAuthAdminConnectorRouter(adminauthV1) _ = plugin.CallAgent(func(agent plugin.Agent) error { agent.RegisterUnAuthRouter(mustUnAuthV1) agent.RegisterAuthUserRouter(authV1) agent.RegisterAuthAdminRouter(adminauthV1) return nil }) // mcp mcpAPIGroup := r.Group(uiConf.APIBaseURL + "/answer/api/v1") mcpAPIGroup.Use(authUserMiddleware.AuthMcpEnable(), authUserMiddleware.AuthAPIKey()) answerRouter.RegisterMCPRouter(mcpAPIGroup) return r } ================================================ FILE: internal/base/server/http_funcmap.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package server import ( "html/template" "regexp" "strconv" "strings" "time" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/controller" "github.com/apache/answer/internal/schema" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/day" "github.com/apache/answer/pkg/htmltext" "github.com/segmentfault/pacman/i18n" ) var funcMap = template.FuncMap{ "replaceHTMLTag": func(src string, tags ...string) string { p := `(?U)<(\d+)>.+` re := regexp.MustCompile(p) ms := re.FindAllStringSubmatch(src, -1) for _, mi := range ms { if mi[1] == mi[2] { i, err := strconv.Atoi(mi[1]) if err != nil || len(tags) < i { break } src = strings.ReplaceAll(src, mi[0], tags[i-1]) } } return src }, "join": func(sep string, elems ...string) string { return strings.Join(elems, sep) }, "templateHTML": func(data string) template.HTML { return template.HTML(data) }, "formatLinkNofollow": func(data string) template.HTML { return template.HTML(FormatLinkNofollow(data)) }, "translator": func(la i18n.Language, data string, params ...any) string { trans := translator.GlobalTrans.Tr(la, data) if len(params) > 0 && len(params)%2 == 0 { for i := 0; i < len(params); i += 2 { k := converter.InterfaceToString(params[i]) v := converter.InterfaceToString(params[i+1]) trans = strings.ReplaceAll(trans, "{{ "+k+" }}", v) trans = strings.ReplaceAll(trans, "{{"+k+"}}", v) } } return trans }, "timeFormatISO": func(tz string, timestamp int64) string { _, _ = time.LoadLocation(tz) return time.Unix(timestamp, 0).Format("2006-01-02T15:04:05.000Z") }, "translatorTimeFormatLongDate": func(la i18n.Language, tz string, timestamp int64) string { trans := translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_time") return day.Format(timestamp, trans, tz) }, "translatorTimeFormat": func(la i18n.Language, tz string, timestamp int64) string { var ( now = time.Now().Unix() between int64 = 0 trans string ) _, _ = time.LoadLocation(tz) if now > timestamp { between = now - timestamp } if between <= 1 { return translator.GlobalTrans.Tr(la, "ui.dates.now") } if between > 1 && between < 60 { trans = translator.GlobalTrans.Tr(la, "ui.dates.x_seconds_ago") return strings.ReplaceAll(trans, "{{count}}", converter.IntToString(between)) } if between >= 60 && between < 3600 { min := between / 60 trans = translator.GlobalTrans.Tr(la, "ui.dates.x_minutes_ago") return strings.ReplaceAll(trans, "{{count}}", strconv.FormatInt(min, 10)) } if between >= 3600 && between < 3600*24 { h := between / 3600 trans = translator.GlobalTrans.Tr(la, "ui.dates.x_hours_ago") return strings.ReplaceAll(trans, "{{count}}", strconv.FormatInt(h, 10)) } if between >= 3600*24 && between < 3600*24*366 && time.Unix(timestamp, 0).Format("2006") == time.Unix(now, 0).Format("2006") { trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date") return day.Format(timestamp, trans, tz) } trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_year") return day.Format(timestamp, trans, tz) }, "wrapComments": func(comments []*schema.GetCommentResp, la i18n.Language, tz string) map[string]any { return map[string]any{ "comments": comments, "language": la, "timezone": tz, } }, "urlTitle": htmltext.UrlTitle, } func FormatLinkNofollow(html string) string { var hrefRegexp = regexp.MustCompile("(?m).*?") match := hrefRegexp.FindAllString(html, -1) for _, v := range match { hasNofollow := strings.Contains(v, "rel=\"nofollow\"") hasSiteUrl := strings.Contains(v, controller.SiteUrl) if !hasSiteUrl { if !hasNofollow { nofollowUrl := strings.Replace(v, " 0 { b.WriteByte('.') } b.WriteString(part) } return b.String() } ================================================ FILE: internal/base/validator/validator.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package validator import ( "errors" "fmt" "reflect" "strings" "unicode" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/go-playground/locales" german "github.com/go-playground/locales/de" english "github.com/go-playground/locales/en" spanish "github.com/go-playground/locales/es" french "github.com/go-playground/locales/fr" italian "github.com/go-playground/locales/it" japanese "github.com/go-playground/locales/ja" korean "github.com/go-playground/locales/ko" portuguese "github.com/go-playground/locales/pt" russian "github.com/go-playground/locales/ru" vietnamese "github.com/go-playground/locales/vi" chinese "github.com/go-playground/locales/zh" chineseTraditional "github.com/go-playground/locales/zh_Hant_TW" ut "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10/translations/en" "github.com/go-playground/validator/v10/translations/es" "github.com/go-playground/validator/v10/translations/fr" "github.com/go-playground/validator/v10/translations/it" "github.com/go-playground/validator/v10/translations/ja" "github.com/go-playground/validator/v10/translations/pt" "github.com/go-playground/validator/v10/translations/ru" "github.com/go-playground/validator/v10/translations/vi" "github.com/go-playground/validator/v10/translations/zh" "github.com/go-playground/validator/v10/translations/zh_tw" "github.com/microcosm-cc/bluemonday" myErrors "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" ) type TranslatorLocal struct { La i18n.Language Lo locales.Translator RegisterFunc func(v *validator.Validate, trans ut.Translator) (err error) } var ( allLanguageTranslators = []*TranslatorLocal{ {La: i18n.LanguageChinese, Lo: chinese.New(), RegisterFunc: zh.RegisterDefaultTranslations}, {La: i18n.LanguageChineseTraditional, Lo: chineseTraditional.New(), RegisterFunc: zh_tw.RegisterDefaultTranslations}, {La: i18n.LanguageEnglish, Lo: english.New(), RegisterFunc: en.RegisterDefaultTranslations}, {La: i18n.LanguageGerman, Lo: german.New(), RegisterFunc: nil}, {La: i18n.LanguageSpanish, Lo: spanish.New(), RegisterFunc: es.RegisterDefaultTranslations}, {La: i18n.LanguageFrench, Lo: french.New(), RegisterFunc: fr.RegisterDefaultTranslations}, {La: i18n.LanguageItalian, Lo: italian.New(), RegisterFunc: it.RegisterDefaultTranslations}, {La: i18n.LanguageJapanese, Lo: japanese.New(), RegisterFunc: ja.RegisterDefaultTranslations}, {La: i18n.LanguageKorean, Lo: korean.New(), RegisterFunc: nil}, {La: i18n.LanguagePortuguese, Lo: portuguese.New(), RegisterFunc: pt.RegisterDefaultTranslations}, {La: i18n.LanguageRussian, Lo: russian.New(), RegisterFunc: ru.RegisterDefaultTranslations}, {La: i18n.LanguageVietnamese, Lo: vietnamese.New(), RegisterFunc: vi.RegisterDefaultTranslations}, } ) // MyValidator my validator type MyValidator struct { Validate *validator.Validate Tran ut.Translator Lang i18n.Language } // FormErrorField indicates the current form error content. which field is error and error message. type FormErrorField struct { ErrorField string `json:"error_field"` ErrorMsg string `json:"error_msg"` } // GlobalValidatorMapping is a mapping from validator to translator used var GlobalValidatorMapping = make(map[i18n.Language]*MyValidator, 0) func init() { for _, t := range allLanguageTranslators { tran, val := getTran(t.Lo), createDefaultValidator(t.La) if t.RegisterFunc != nil { if err := t.RegisterFunc(val, tran); err != nil { panic(err) } } GlobalValidatorMapping[t.La] = &MyValidator{Validate: val, Tran: tran, Lang: t.La} } } func getTran(lo locales.Translator) ut.Translator { tran, ok := ut.New(lo, lo).GetTranslator(lo.Locale()) if !ok { panic(fmt.Sprintf("not found translator %s", lo.Locale())) } return tran } func NotBlank(fl validator.FieldLevel) (res bool) { field := fl.Field() switch field.Kind() { case reflect.String: trimSpace := strings.TrimSpace(field.String()) res := len(trimSpace) > 0 if !res { field.SetString(trimSpace) } return true case reflect.Chan, reflect.Map, reflect.Slice, reflect.Array: return field.Len() > 0 case reflect.Ptr, reflect.Interface, reflect.Func: return !field.IsNil() default: return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface() } } func Sanitizer(fl validator.FieldLevel) (res bool) { field := fl.Field() switch field.Kind() { case reflect.String: filter := bluemonday.UGCPolicy() content := strings.ReplaceAll(filter.Sanitize(field.String()), "&", "&") field.SetString(content) return true case reflect.Chan, reflect.Map, reflect.Slice, reflect.Array: return field.Len() > 0 case reflect.Ptr, reflect.Interface, reflect.Func: return !field.IsNil() default: return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface() } } func createDefaultValidator(la i18n.Language) *validator.Validate { validate := validator.New() // _ = validate.RegisterValidation("notblank", validators.NotBlank) _ = validate.RegisterValidation("notblank", NotBlank) _ = validate.RegisterValidation("sanitizer", Sanitizer) validate.RegisterTagNameFunc(func(fld reflect.StructField) (res string) { defer func() { if len(res) > 0 { res = translator.Tr(la, res) } }() if jsonTag := fld.Tag.Get("json"); len(jsonTag) > 0 { if jsonTag == "-" { return "" } return jsonTag } if formTag := fld.Tag.Get("form"); len(formTag) > 0 { return formTag } return fld.Name }) return validate } func GetValidatorByLang(lang i18n.Language) *MyValidator { if GlobalValidatorMapping[lang] != nil { return GlobalValidatorMapping[lang] } return GlobalValidatorMapping[i18n.DefaultLanguage] } // Check / func (m *MyValidator) Check(value any) (errFields []*FormErrorField, err error) { defer func() { if len(errFields) == 0 { return } for _, field := range errFields { if len(field.ErrorField) == 0 { continue } firstRune := []rune(field.ErrorMsg)[0] if !unicode.IsLetter(firstRune) || !unicode.Is(unicode.Latin, firstRune) { continue } upperFirstRune := unicode.ToUpper(firstRune) field.ErrorMsg = string(upperFirstRune) + field.ErrorMsg[1:] if !strings.HasSuffix(field.ErrorMsg, ".") { field.ErrorMsg += "." } } }() err = m.Validate.Struct(value) if err != nil { var valErrors validator.ValidationErrors if !errors.As(err, &valErrors) { log.Error(err) return nil, errors.New("validate check exception") } for _, fieldError := range valErrors { errField := &FormErrorField{ ErrorField: fieldError.Field(), ErrorMsg: fieldError.Translate(m.Tran), } // get original tag name from value for set err field key. structNamespace := fieldError.StructNamespace() _, fieldName, found := strings.Cut(structNamespace, ".") if found { originalTag := getObjectTagByFieldName(value, fieldName) if len(originalTag) > 0 { errField.ErrorField = originalTag } } errFields = append(errFields, errField) } if len(errFields) > 0 { errMsg := "" if len(errFields) == 1 { errMsg = errFields[0].ErrorMsg } return errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) } } if v, ok := value.(Checker); ok { errFields, err = v.Check() if err == nil { return nil, nil } errMsg := "" for _, errField := range errFields { errField.ErrorMsg = translator.Tr(m.Lang, errField.ErrorMsg) errMsg = errField.ErrorMsg } return errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) } return nil, nil } // Checker . type Checker interface { Check() (errField []*FormErrorField, err error) } func getObjectTagByFieldName(obj any, fieldName string) (tag string) { defer func() { if err := recover(); err != nil { log.Error(err) } }() objT := reflect.TypeOf(obj) objT = objT.Elem() structField, exists := objT.FieldByName(fieldName) if !exists { return "" } tag = structField.Tag.Get("json") if len(tag) == 0 { return structField.Tag.Get("form") } return tag } ================================================ FILE: internal/cli/build.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package cli import ( "bytes" "fmt" "io" "io/fs" "os" "os/exec" "path" "path/filepath" "strings" "text/template" "github.com/Masterminds/semver/v3" "github.com/apache/answer/pkg/dir" "github.com/apache/answer/pkg/writer" "github.com/segmentfault/pacman/log" "gopkg.in/yaml.v3" ) const ( mainGoTpl = `package main import ( answercmd "github.com/apache/answer/cmd" // remote plugins {{- range .remote_plugins}} _ "{{.}}" {{- end}} // local plugins {{- range .local_plugins}} _ "answer/{{.}}" {{- end}} ) func main() { answercmd.Main() } ` goModTpl = `module answer go 1.23 ` ) type answerBuilder struct { buildingMaterial *buildingMaterial BuildError error } type buildingMaterial struct { answerModuleReplacement string plugins []*pluginInfo outputPath string tmpDir string originalAnswerInfo OriginalAnswerInfo } type OriginalAnswerInfo struct { Version string Revision string Time string } type pluginInfo struct { // Name of the plugin e.g. github.com/apache/answer-plugins/github-connector Name string // Path to the plugin. If path exist, read plugin from local filesystem Path string // Version of the plugin Version string } func newAnswerBuilder(buildDir, outputPath string, plugins []string, originalAnswerInfo OriginalAnswerInfo) *answerBuilder { material := &buildingMaterial{originalAnswerInfo: originalAnswerInfo} parentDir, _ := filepath.Abs(".") if buildDir != "" { material.tmpDir = filepath.Join(parentDir, buildDir) } else { material.tmpDir, _ = os.MkdirTemp(parentDir, "answer_build") } if len(outputPath) == 0 { outputPath = filepath.Join(parentDir, "new_answer") } material.outputPath, _ = filepath.Abs(outputPath) material.plugins = formatPlugins(plugins) material.answerModuleReplacement = os.Getenv("ANSWER_MODULE") return &answerBuilder{ buildingMaterial: material, } } func (a *answerBuilder) DoTask(task func(b *buildingMaterial) error) { if a.BuildError != nil { return } a.BuildError = task(a.buildingMaterial) } // BuildNewAnswer builds a new answer with specified plugins func BuildNewAnswer(buildDir, outputPath string, plugins []string, originalAnswerInfo OriginalAnswerInfo) (err error) { builder := newAnswerBuilder(buildDir, outputPath, plugins, originalAnswerInfo) builder.DoTask(createMainGoFile) builder.DoTask(downloadGoModFile) builder.DoTask(movePluginToVendor) builder.DoTask(copyUIFiles) builder.DoTask(buildUI) builder.DoTask(mergeI18nFiles) builder.DoTask(buildBinary) builder.DoTask(cleanByproduct) return builder.BuildError } func formatPlugins(plugins []string) (formatted []*pluginInfo) { for _, plugin := range plugins { plugin = strings.TrimSpace(plugin) // plugin description like this 'github.com/apache/answer-plugins/github-connector@latest=/local/path' info := &pluginInfo{} plugin, info.Path, _ = strings.Cut(plugin, "=") info.Name, info.Version, _ = strings.Cut(plugin, "@") formatted = append(formatted, info) } return formatted } // createMainGoFile creates main.go file in tmp dir that content is mainGoTpl func createMainGoFile(b *buildingMaterial) (err error) { fmt.Printf("[build] build dir: %s\n", b.tmpDir) err = dir.CreateDirIfNotExist(b.tmpDir) if err != nil { return err } var ( remotePlugins []string ) for _, p := range b.plugins { remotePlugins = append(remotePlugins, versionedModulePath(p.Name, p.Version)) } mainGoFile := &bytes.Buffer{} tmpl, err := template.New("main").Parse(mainGoTpl) if err != nil { return err } err = tmpl.Execute(mainGoFile, map[string]any{ "remote_plugins": remotePlugins, }) if err != nil { return err } err = writer.WriteFile(filepath.Join(b.tmpDir, "main.go"), mainGoFile.String()) if err != nil { return err } err = writer.WriteFile(filepath.Join(b.tmpDir, "go.mod"), goModTpl) if err != nil { return err } for _, p := range b.plugins { // If user set a path, use it to replace the module with local path if len(p.Path) > 0 { replacement := fmt.Sprintf("%s@%s=%s", p.Name, p.Version, p.Path) err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run() } else if len(p.Version) > 0 { // If user specify a version, use it to get specific version of the module err = b.newExecCmd("go", "get", fmt.Sprintf("%s@%s", p.Name, p.Version)).Run() } if err != nil { return err } } return } // downloadGoModFile run go mod commands to download dependencies func downloadGoModFile(b *buildingMaterial) (err error) { // If user specify a module replacement, use it. Otherwise, use the latest version. if len(b.answerModuleReplacement) > 0 { replacement := fmt.Sprintf("%s=%s", "github.com/apache/answer", b.answerModuleReplacement) err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run() if err != nil { return err } } err = b.newExecCmd("go", "mod", "tidy").Run() if err != nil { return err } err = b.newExecCmd("go", "mod", "vendor").Run() if err != nil { return err } return } // movePluginToVendor move plugin to vendor dir // Traverse the plugins, and if the plugin path is not github.com/apache/answer-plugins, move the contents of the current plugin to the vendor/github.com/apache/answer-plugins/ directory. func movePluginToVendor(b *buildingMaterial) (err error) { pluginsDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer-plugins/") for _, p := range b.plugins { pluginDir := filepath.Join(b.tmpDir, "vendor/", p.Name) pluginName := filepath.Base(p.Name) if !strings.HasPrefix(p.Name, "github.com/apache/answer-plugins/") { fmt.Printf("try to copy dir from %s to %s\n", pluginDir, filepath.Join(pluginsDir, pluginName)) err = copyDirEntries(os.DirFS(pluginDir), ".", filepath.Join(pluginsDir, pluginName), "node_modules") if err != nil { return err } } } return nil } // copyUIFiles copy ui files from answer module to tmp dir func copyUIFiles(b *buildingMaterial) (err error) { goListCmd := b.newExecCmd("go", "list", "-mod=mod", "-m", "-f", "{{.Dir}}", "github.com/apache/answer") buf := new(bytes.Buffer) goListCmd.Stdout = buf if err = goListCmd.Run(); err != nil { return fmt.Errorf("failed to run go list: %w", err) } answerDir := strings.TrimSpace(buf.String()) goModUIDir := filepath.Join(answerDir, "ui") localUIBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer/ui/") // The node_modules folder generated during development will interfere packaging, so it needs to be ignored. if err = copyDirEntries(os.DirFS(goModUIDir), ".", localUIBuildDir, "node_modules"); err != nil { return fmt.Errorf("failed to copy ui files: %w", err) } pluginsDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer-plugins/") localUIPluginDir := filepath.Join(localUIBuildDir, "src/plugins/") // copy plugins dir fmt.Printf("try to copy dir from %s to %s\n", pluginsDir, localUIPluginDir) // if plugins dir not exist means no plugins if !dir.CheckDirExist(pluginsDir) { return nil } pluginsDirEntries, err := os.ReadDir(pluginsDir) if err != nil { return fmt.Errorf("failed to read plugins dir: %w", err) } for _, entry := range pluginsDirEntries { if !entry.IsDir() { continue } sourcePluginDir := filepath.Join(pluginsDir, entry.Name()) // check if plugin is a ui plugin packageJsonPath := filepath.Join(sourcePluginDir, "package.json") fmt.Printf("check if %s is a ui plugin\n", packageJsonPath) if !dir.CheckFileExist(packageJsonPath) { continue } pnpmInstallCmd := b.newExecCmd("pnpm", "install") pnpmInstallCmd.Dir = sourcePluginDir if err = pnpmInstallCmd.Run(); err != nil { return fmt.Errorf("failed to install plugin dependencies: %w", err) } localPluginDir := filepath.Join(localUIPluginDir, entry.Name()) fmt.Printf("try to copy dir from %s to %s\n", sourcePluginDir, localPluginDir) if err = copyDirEntries(os.DirFS(sourcePluginDir), ".", localPluginDir, "node_modules"); err != nil { return fmt.Errorf("failed to copy ui files: %w", err) } } formatUIPluginsDirName(localUIPluginDir) return nil } // buildUI run pnpm install and pnpm build commands to build ui func buildUI(b *buildingMaterial) (err error) { localUIBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer/ui") pnpmInstallCmd := b.newExecCmd("pnpm", "pre-install") pnpmInstallCmd.Dir = localUIBuildDir if err = pnpmInstallCmd.Run(); err != nil { return err } pnpmBuildCmd := b.newExecCmd("pnpm", "build") pnpmBuildCmd.Dir = localUIBuildDir if err = pnpmBuildCmd.Run(); err != nil { return err } return nil } // mergeI18nFiles merge i18n files func mergeI18nFiles(b *buildingMaterial) (err error) { fmt.Printf("try to merge i18n files\n") type YamlPluginContent struct { Plugin map[string]any `yaml:"plugin"` } pluginAllTranslations := make(map[string]*YamlPluginContent) for _, plugin := range b.plugins { i18nDir := filepath.Join(b.tmpDir, fmt.Sprintf("vendor/%s/i18n", plugin.Name)) fmt.Println("i18n dir: ", i18nDir) if !dir.CheckDirExist(i18nDir) { continue } entries, err := os.ReadDir(i18nDir) if err != nil { return err } for _, file := range entries { // ignore directory if file.IsDir() { continue } // ignore non-YAML file if filepath.Ext(file.Name()) != ".yaml" { continue } buf, err := os.ReadFile(filepath.Join(i18nDir, file.Name())) if err != nil { log.Debugf("read translation file failed: %s %s", file.Name(), err) continue } translation := &YamlPluginContent{} if err = yaml.Unmarshal(buf, translation); err != nil { log.Debugf("unmarshal translation file failed: %s %s", file.Name(), err) continue } if pluginAllTranslations[file.Name()] == nil { pluginAllTranslations[file.Name()] = &YamlPluginContent{Plugin: make(map[string]any)} } for k, v := range translation.Plugin { pluginAllTranslations[file.Name()].Plugin[k] = v } } } originalI18nDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer/i18n") entries, err := os.ReadDir(originalI18nDir) if err != nil { return err } for _, file := range entries { // ignore directory if file.IsDir() { continue } // ignore non-YAML file filename := file.Name() if filepath.Ext(filename) != ".yaml" && filename != "i18n.yaml" { continue } // if plugin don't have this translation file, ignore it if pluginAllTranslations[filename] == nil { continue } out, _ := yaml.Marshal(pluginAllTranslations[filename]) buf, err := os.OpenFile(filepath.Join(originalI18nDir, filename), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Debugf("read translation file failed: %s %s", filename, err) continue } _, _ = buf.WriteString("\n") _, _ = buf.Write(out) _ = buf.Close() } return err } func copyDirEntries(sourceFs fs.FS, sourceDir, targetDir string, ignoreDir ...string) (err error) { err = dir.CreateDirIfNotExist(targetDir) if err != nil { return err } ignoreThisDir := func(path string) bool { for _, s := range ignoreDir { if strings.HasPrefix(path, s) { return true } } return false } err = fs.WalkDir(sourceFs, sourceDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if ignoreThisDir(path) { return nil } // Convert the path to use forward slashes, important because we use embedded FS which always uses forward slashes path = filepath.ToSlash(path) // Construct the absolute path for the source file/directory srcPath := filepath.Join(sourceDir, path) srcPath = filepath.ToSlash(srcPath) // Construct the absolute path for the destination file/directory dstPath := filepath.Join(targetDir, path) if d.IsDir() { // Create the directory in the destination err := os.MkdirAll(dstPath, os.ModePerm) if err != nil { return fmt.Errorf("failed to create directory %s: %w", dstPath, err) } } else { // Open the source file srcFile, err := sourceFs.Open(srcPath) if err != nil { return fmt.Errorf("failed to open source file %s: %w", srcPath, err) } defer srcFile.Close() // Create the destination file dstFile, err := os.Create(dstPath) if err != nil { return fmt.Errorf("failed to create destination file %s: %w", dstPath, err) } defer dstFile.Close() // Copy the file contents _, err = io.Copy(dstFile, srcFile) if err != nil { return fmt.Errorf("failed to copy file contents from %s to %s: %w", srcPath, dstPath, err) } } return nil }) return err } // format plugins dir name from dash to underline func formatUIPluginsDirName(dirPath string) { entries, err := os.ReadDir(dirPath) if err != nil { fmt.Printf("read ui plugins dir failed: [%s] %s\n", dirPath, err) return } for _, entry := range entries { if !entry.IsDir() || !strings.Contains(entry.Name(), "-") { continue } newName := strings.ReplaceAll(entry.Name(), "-", "_") if err := os.Rename(filepath.Join(dirPath, entry.Name()), filepath.Join(dirPath, newName)); err != nil { fmt.Printf("rename ui plugins dir failed: [%s] %s\n", dirPath, err) } else { fmt.Printf("rename ui plugins dir success: [%s] -> [%s]\n", entry.Name(), newName) } } } // buildBinary build binary file func buildBinary(b *buildingMaterial) (err error) { versionInfo := b.originalAnswerInfo cmdPkg := "github.com/apache/answer/cmd" ldflags := fmt.Sprintf("-X %s.Version=%s -X %s.Revision=%s -X %s.Time=%s", cmdPkg, versionInfo.Version, cmdPkg, versionInfo.Revision, cmdPkg, versionInfo.Time) err = b.newExecCmd("go", "build", "-ldflags", ldflags, "-o", b.outputPath, ".").Run() if err != nil { return err } return } // cleanByproduct delete tmp dir func cleanByproduct(b *buildingMaterial) (err error) { return os.RemoveAll(b.tmpDir) } func (b *buildingMaterial) newExecCmd(command string, args ...string) *exec.Cmd { cmd := exec.Command(command, args...) fmt.Println(cmd.Args) cmd.Dir = b.tmpDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd } func versionedModulePath(modulePath, moduleVersion string) string { if moduleVersion == "" { return modulePath } ver, err := semver.StrictNewVersion(strings.TrimPrefix(moduleVersion, "v")) if err != nil { return modulePath } major := ver.Major() if major > 1 { modulePath += fmt.Sprintf("/v%d", major) } return path.Clean(modulePath) } ================================================ FILE: internal/cli/config.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package cli import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) type ConfigField struct { AllowPasswordLogin bool `json:"allow_password_login"` // The slug name of plugin that you want to deactivate DeactivatePluginSlugName string `json:"deactivate_plugin_slug_name"` } // SetDefaultConfig set default config func SetDefaultConfig(dbConf *data.Database, cacheConf *data.CacheConf, field *ConfigField) error { db, err := data.NewDB(false, dbConf) if err != nil { return err } defer func() { _ = db.Close() }() cache, cacheCleanup, err := data.NewCache(cacheConf) if err != nil { fmt.Println("new cache failed") } defer func() { if cache != nil { _ = cache.Flush(context.Background()) cacheCleanup() } }() if field.AllowPasswordLogin { return defaultLoginConfig(db) } if len(field.DeactivatePluginSlugName) > 0 { return deactivatePlugin(db, field.DeactivatePluginSlugName) } return nil } func defaultLoginConfig(x *xorm.Engine) (err error) { fmt.Println("set default login config") loginSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeLogin, } exist, err := x.Get(loginSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { var content map[string]any _ = json.Unmarshal([]byte(loginSiteInfo.Content), &content) content["allow_password_login"] = true dataByte, _ := json.Marshal(content) loginSiteInfo.Content = string(dataByte) _, err = x.ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo) if err != nil { return fmt.Errorf("update site info failed: %w", err) } } return nil } func deactivatePlugin(x *xorm.Engine, pluginSlugName string) (err error) { fmt.Printf("try to deactivate plugin: %s\n", pluginSlugName) item := &entity.Config{Key: constant.PluginStatus} exist, err := x.Get(item) if err != nil { return fmt.Errorf("get config failed: %w", err) } if !exist { return nil } pluginStatusMapping := make(map[string]bool) _ = json.Unmarshal([]byte(item.Value), &pluginStatusMapping) status, ok := pluginStatusMapping[pluginSlugName] if !ok { fmt.Printf("plugin %s not exist\n", pluginSlugName) return nil } if !status { fmt.Printf("plugin %s already deactivated\n", pluginSlugName) return nil } pluginStatusMapping[pluginSlugName] = false dataByte, _ := json.Marshal(pluginStatusMapping) item.Value = string(dataByte) _, err = x.ID(item.ID).Cols("value").Update(item) if err != nil { return fmt.Errorf("update plugin status failed: %w", err) } return nil } ================================================ FILE: internal/cli/dump.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package cli import ( "fmt" "path/filepath" "time" "github.com/apache/answer/internal/base/data" "xorm.io/xorm/schemas" ) // DumpAllData dump all database data to sql func DumpAllData(dataConf *data.Database, dumpDataPath string) error { db, err := data.NewDB(false, dataConf) if err != nil { return err } defer func() { _ = db.Close() }() if err = db.Ping(); err != nil { return err } name := filepath.Join(dumpDataPath, fmt.Sprintf("answer_dump_data_%s.sql", time.Now().Format("2006-01-02"))) return db.DumpAllToFile(name, schemas.MYSQL) } ================================================ FILE: internal/cli/i18n.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package cli import ( "fmt" "os" "path/filepath" "strings" "github.com/apache/answer/i18n" "github.com/apache/answer/pkg/dir" "github.com/apache/answer/pkg/writer" "gopkg.in/yaml.v3" ) type YamlPluginContent struct { Plugin map[string]any `yaml:"plugin"` } // ReplaceI18nFilesLocal replace i18n files func ReplaceI18nFilesLocal(i18nDir string) error { i18nList, err := i18n.I18n.ReadDir(".") if err != nil { fmt.Println(err.Error()) return err } fmt.Printf("[i18n] find i18n bundle %d\n", len(i18nList)) for _, item := range i18nList { path := filepath.Join(i18nDir, item.Name()) content, err := i18n.I18n.ReadFile(item.Name()) if err != nil { continue } exist := dir.CheckFileExist(path) if exist { fmt.Printf("[i18n] install %s file exist, try to replace it\n", item.Name()) if err = os.Remove(path); err != nil { fmt.Println(err) } } fmt.Printf("[i18n] install %s bundle...\n", item.Name()) err = writer.WriteFile(path, string(content)) if err != nil { fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error()) } else { fmt.Printf("[i18n] install %s bundle success\n", item.Name()) } } return nil } // MergeI18nFilesLocal merge i18n files func MergeI18nFilesLocal(originalI18nDir, targetI18nDir string) (err error) { pluginAllTranslations := make(map[string]*YamlPluginContent) err = findI18nFileInDir(pluginAllTranslations, targetI18nDir) if err != nil { return err } entries, err := os.ReadDir(originalI18nDir) if err != nil { return err } for _, file := range entries { // ignore directory if file.IsDir() { continue } // ignore non-YAML file filename := file.Name() if filepath.Ext(filename) != ".yaml" && filename != "i18n.yaml" { continue } // if plugin don't have this translation file, ignore it if pluginAllTranslations[filename] == nil { continue } out, _ := yaml.Marshal(pluginAllTranslations[filename]) buf, err := os.OpenFile(filepath.Join(originalI18nDir, filename), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { fmt.Printf("[i18n] read translation file failed: %s %s\n", filename, err) continue } _, _ = buf.WriteString("\n") _, _ = buf.Write(out) _ = buf.Close() fmt.Printf("[i18n] merge i18n file: %s success\n", filename) } return nil } // find i18n file in dir func findI18nFileInDir(pluginAllTranslations map[string]*YamlPluginContent, i18nDir string) error { // if i18n dir is not i18n, find deeper dirBase := filepath.Base(i18nDir) if dirBase != "i18n" { if strings.HasPrefix(dirBase, ".") { return nil } // find all i18n dir in target dir targetDirs, err := os.ReadDir(i18nDir) if err != nil { return err } for _, targetDir := range targetDirs { if targetDir.IsDir() { if err := findI18nFileInDir(pluginAllTranslations, filepath.Join(i18nDir, targetDir.Name())); err != nil { fmt.Printf("[i18n] find i18n file in dir failed: %s %s\n", targetDir.Name(), err) } } } return nil } fmt.Printf("[i18n] find i18n file in dir: %s\n", i18nDir) // if i18nDir is i18n, find all yaml files entries, err := os.ReadDir(i18nDir) if err != nil { return err } for _, file := range entries { // ignore directory if file.IsDir() { continue } // ignore non-YAML file if filepath.Ext(file.Name()) != ".yaml" { continue } buf, err := os.ReadFile(filepath.Join(i18nDir, file.Name())) if err != nil { fmt.Printf("[i18n] read translation file failed: %s %s\n", file.Name(), err) continue } translation := &YamlPluginContent{} if err = yaml.Unmarshal(buf, translation); err != nil { fmt.Printf("[i18n] unmarshal translation file failed: %s %s\n", file.Name(), err) continue } if pluginAllTranslations[file.Name()] == nil { pluginAllTranslations[file.Name()] = &YamlPluginContent{Plugin: make(map[string]any)} } for k, v := range translation.Plugin { pluginAllTranslations[file.Name()].Plugin[k] = v } } return nil } ================================================ FILE: internal/cli/install.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package cli import ( "fmt" "os" "path/filepath" "github.com/apache/answer/configs" "github.com/apache/answer/i18n" "github.com/apache/answer/internal/base/path" "github.com/apache/answer/pkg/dir" "github.com/apache/answer/pkg/writer" ) // InstallAllInitialEnvironment install all initial environment func InstallAllInitialEnvironment(dataDirPath string) { path.FormatAllPath(dataDirPath) installUploadDir() InstallI18nBundle(false) fmt.Println("install all initial environment done") } func InstallConfigFile(configFilePath string) error { if len(configFilePath) == 0 { configFilePath = filepath.Join(path.ConfigFileDir, path.DefaultConfigFileName) } fmt.Println("[config-file] try to create at ", configFilePath) // if config file already exists do nothing. if CheckConfigFile(configFilePath) { fmt.Printf("[config-file] %s already exists\n", configFilePath) return nil } if err := dir.CreateDirIfNotExist(path.ConfigFileDir); err != nil { fmt.Printf("[config-file] create directory fail %s\n", err.Error()) return fmt.Errorf("create directory fail %s", err.Error()) } fmt.Printf("[config-file] create directory success, config file is %s\n", configFilePath) if err := writer.WriteFile(configFilePath, string(configs.Config)); err != nil { fmt.Printf("[config-file] install fail %s\n", err.Error()) return fmt.Errorf("write file failed %s", err) } fmt.Printf("[config-file] install success\n") return nil } func installUploadDir() { fmt.Println("[upload-dir] try to install...") if err := dir.CreateDirIfNotExist(path.UploadFilePath); err != nil { fmt.Printf("[upload-dir] install fail %s\n", err.Error()) } else { fmt.Printf("[upload-dir] install success, upload directory is %s\n", path.UploadFilePath) } } func InstallI18nBundle(replace bool) { fmt.Println("[i18n] try to install i18n bundle...") // if SKIP_REPLACE_I18N is set, skip replace i18n bundles if len(os.Getenv("SKIP_REPLACE_I18N")) > 0 { replace = false } if err := dir.CreateDirIfNotExist(path.I18nPath); err != nil { fmt.Println(err.Error()) return } i18nList, err := i18n.I18n.ReadDir(".") if err != nil { fmt.Println(err.Error()) return } fmt.Printf("[i18n] find i18n bundle %d\n", len(i18nList)) for _, item := range i18nList { path := filepath.Join(path.I18nPath, item.Name()) content, err := i18n.I18n.ReadFile(item.Name()) if err != nil { continue } exist := dir.CheckFileExist(path) if exist && !replace { continue } if exist { fmt.Printf("[i18n] install %s file exist, try to replace it\n", item.Name()) if err = os.Remove(path); err != nil { fmt.Println(err) } } fmt.Printf("[i18n] install %s bundle...\n", item.Name()) err = writer.WriteFile(path, string(content)) if err != nil { fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error()) } else { fmt.Printf("[i18n] install %s bundle success\n", item.Name()) } } } ================================================ FILE: internal/cli/install_check.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package cli import ( "fmt" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/path" "github.com/apache/answer/internal/entity" "github.com/apache/answer/pkg/dir" ) func CheckConfigFile(configPath string) bool { return dir.CheckFileExist(configPath) } func CheckUploadDir() bool { return dir.CheckDirExist(path.UploadFilePath) } // CheckDBConnection check database whether the connection is normal func CheckDBConnection(dataConf *data.Database) bool { db, err := data.NewDB(false, dataConf) if err != nil { fmt.Printf("connection database failed: %s\n", err) return false } defer func() { _ = db.Close() }() if err = db.Ping(); err != nil { fmt.Printf("connection ping database failed: %s\n", err) return false } return true } // CheckDBTableExist check database whether the table is already exists func CheckDBTableExist(dataConf *data.Database) bool { db, err := data.NewDB(false, dataConf) if err != nil { fmt.Printf("connection database failed: %s\n", err) return false } defer func() { _ = db.Close() }() if err = db.Ping(); err != nil { fmt.Printf("connection ping database failed: %s\n", err) return false } exist, err := db.IsTableExist(&entity.Version{}) if err != nil { fmt.Printf("check table exist failed: %s\n", err) return false } if !exist { fmt.Printf("check table not exist\n") return false } return true } ================================================ FILE: internal/cli/reset_password.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package cli import ( "bufio" "context" "crypto/rand" "fmt" "math/big" "os" "runtime" "strings" "github.com/apache/answer/internal/base/conf" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/path" "github.com/apache/answer/internal/repo/api_key" "github.com/apache/answer/internal/repo/auth" "github.com/apache/answer/internal/repo/user" authService "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/pkg/checker" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" "golang.org/x/crypto/bcrypt" "golang.org/x/term" _ "modernc.org/sqlite" "xorm.io/xorm" ) const ( charsetLower = "abcdefghijklmnopqrstuvwxyz" charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" charsetDigits = "0123456789" charsetSpecial = "!@#$%^&*~?_-" maxRetries = 10 defaultRandomPasswordLength = 12 ) var charset = []string{ charsetLower, charsetUpper, charsetDigits, charsetSpecial, } type ResetPasswordOptions struct { Email string Password string } func ResetPassword(ctx context.Context, dataDirPath string, opts *ResetPasswordOptions) error { path.FormatAllPath(dataDirPath) config, err := conf.ReadConfig(path.GetConfigFilePath()) if err != nil { return fmt.Errorf("read config file failed: %w", err) } db, err := initDatabase(config.Data.Database.Driver, config.Data.Database.Connection) if err != nil { return fmt.Errorf("connect database failed: %w", err) } defer func() { _ = db.Close() }() cache, cacheCleanup, err := data.NewCache(config.Data.Cache) if err != nil { return fmt.Errorf("initialize cache failed: %w", err) } defer cacheCleanup() dataData, dataCleanup, err := data.NewData(db, cache) if err != nil { return fmt.Errorf("initialize data layer failed: %w", err) } defer dataCleanup() userRepo := user.NewUserRepo(dataData) authRepo := auth.NewAuthRepo(dataData) apiKeyRepo := api_key.NewAPIKeyRepo(dataData) authSvc := authService.NewAuthService(authRepo, apiKeyRepo) email := strings.TrimSpace(opts.Email) if email == "" { reader := bufio.NewReader(os.Stdin) fmt.Print("Please input user email: ") emailInput, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("read email input failed: %w", err) } email = strings.TrimSpace(emailInput) } userInfo, exist, err := userRepo.GetByEmail(ctx, email) if err != nil { return fmt.Errorf("query user failed: %w", err) } if !exist { return fmt.Errorf("user not found: %s", email) } fmt.Printf("You are going to reset password for user: %s\n", email) password := strings.TrimSpace(opts.Password) if password != "" { printWarning("Passing password via command line may be recorded in shell history") if err := checker.CheckPassword(password); err != nil { return fmt.Errorf("password validation failed: %w", err) } } else { password, err = promptForPassword() if err != nil { return fmt.Errorf("password input failed: %w", err) } } if !confirmAction(fmt.Sprintf("This will reset password for user '[%s]%s'. Continue?", userInfo.DisplayName, email)) { fmt.Println("Operation cancelled") return nil } hashPwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return fmt.Errorf("encrypt password failed: %w", err) } if err = userRepo.UpdatePass(ctx, userInfo.ID, string(hashPwd)); err != nil { return fmt.Errorf("update password failed: %w", err) } authSvc.RemoveUserAllTokens(ctx, userInfo.ID) fmt.Printf("Password has been successfully updated for user: %s\n", email) fmt.Println("All login sessions have been cleared") return nil } // promptForPassword prompts for a password func promptForPassword() (string, error) { for { input, err := getPasswordInput("Please input new password (empty to generate random password): ") if err != nil { return "", err } if input == "" { password, err := generateRandomPasswordWithRetry() if err != nil { return "", fmt.Errorf("generate random password failed: %w", err) } fmt.Printf("Generated random password: %s\n", password) fmt.Println("Please save this password in a secure location") return password, nil } if err := checker.CheckPassword(input); err != nil { fmt.Printf("Password validation failed: %v\n", err) fmt.Println("Please try again") continue } confirmPwd, err := getPasswordInput("Please confirm new password: ") if err != nil { return "", err } if input != confirmPwd { fmt.Println("Passwords do not match, please try again") continue } return input, nil } } func generateRandomPasswordWithRetry() (string, error) { var password string var err error for range maxRetries { password, err = generateRandomPassword(defaultRandomPasswordLength) if err != nil { continue } if err := checker.CheckPassword(password); err == nil { return password, nil } } if err != nil { return "", err } return "", fmt.Errorf("failed to generate valid password after %d retries", maxRetries) } func getPasswordInput(prompt string) (string, error) { fmt.Print(prompt) password, err := term.ReadPassword(int(os.Stdin.Fd())) if err != nil { return "", err } fmt.Println() return string(password), nil } func generateRandomPassword(length int) (string, error) { if length < len(charset) { return "", fmt.Errorf("password length must be at least %d", len(charset)) } bytes := make([]byte, length) for i, charsetItem := range charset { charIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(charsetItem)))) if err != nil { return "", err } bytes[i] = charsetItem[charIndex.Int64()] } fullCharset := strings.Join(charset, "") for i := len(charset); i < length; i++ { charIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(fullCharset)))) if err != nil { return "", err } bytes[i] = fullCharset[charIndex.Int64()] } for i := len(bytes) - 1; i > 0; i-- { j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1))) if err != nil { return "", err } bytes[i], bytes[j.Int64()] = bytes[j.Int64()], bytes[i] } return string(bytes), nil } func initDatabase(driver, connection string) (*xorm.Engine, error) { dataConf := &data.Database{Driver: driver, Connection: connection} if !CheckDBConnection(dataConf) { return nil, fmt.Errorf("database connection check failed") } engine, err := data.NewDB(false, dataConf) if err != nil { return nil, err } return engine, nil } func printWarning(msg string) { if runtime.GOOS == "windows" { fmt.Printf("[WARNING] %s\n", msg) } else { fmt.Printf("\033[31m[WARNING] %s\033[0m\n", msg) } } func confirmAction(prompt string) bool { reader := bufio.NewReader(os.Stdin) fmt.Printf("%s [y/N]: ", prompt) response, err := reader.ReadString('\n') if err != nil { return false } response = strings.ToLower(strings.TrimSpace(response)) return response == "y" || response == "yes" } ================================================ FILE: internal/controller/activity_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/role" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" ) type ActivityController struct { activityService *activity.ActivityService } // NewActivityController new activity controller. func NewActivityController( activityService *activity.ActivityService) *ActivityController { return &ActivityController{activityService: activityService} } // GetObjectTimeline get object timeline // @Summary get object timeline // @Description get object timeline // @Tags Comment // @Produce json // @Param object_id query string false "object id" // @Param tag_slug_name query string false "tag slug name" // @Param object_type query string false "object type" Enums(question, answer, tag) // @Param show_vote query boolean false "is show vote" // @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp} // @Router /answer/api/v1/activity/timeline [get] func (ac *ActivityController) GetObjectTimeline(ctx *gin.Context) { req := &schema.GetObjectTimelineReq{} if handler.BindAndCheck(ctx, req) { return } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) if userInfo := middleware.GetUserInfoFromContext(ctx); userInfo != nil { req.IsAdmin = userInfo.RoleID == role.RoleAdminID } resp, err := ac.activityService.GetObjectTimeline(ctx, req) handler.HandleResponse(ctx, err, resp) } // GetObjectTimelineDetail get object timeline detail // @Summary get object timeline detail // @Description get object timeline detail // @Tags Comment // @Produce json // @Param revision_id query string true "revision id" // @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp} // @Router /answer/api/v1/activity/timeline/detail [get] func (ac *ActivityController) GetObjectTimelineDetail(ctx *gin.Context) { req := &schema.GetObjectTimelineDetailReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := ac.activityService.GetObjectTimelineDetail(ctx, req) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller/ai_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "context" "encoding/json" "fmt" "maps" "net/http" "strings" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/schema/mcp_tools" "github.com/apache/answer/internal/service/ai_conversation" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/comment" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/feature_toggle" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/siteinfo_common" tagcommonser "github.com/apache/answer/internal/service/tag_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/token" "github.com/gin-gonic/gin" "github.com/mark3labs/mcp-go/mcp" "github.com/sashabaranov/go-openai" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" ) type AIController struct { searchService *content.SearchService siteInfoService siteinfo_common.SiteInfoCommonService tagCommonService *tagcommonser.TagCommonService questioncommon *questioncommon.QuestionCommon commentRepo comment.CommentRepo userCommon *usercommon.UserCommon answerRepo answercommon.AnswerRepo mcpController *MCPController aiConversationService ai_conversation.AIConversationService featureToggleSvc *feature_toggle.FeatureToggleService } // NewAIController new site info controller. func NewAIController( searchService *content.SearchService, siteInfoService siteinfo_common.SiteInfoCommonService, tagCommonService *tagcommonser.TagCommonService, questioncommon *questioncommon.QuestionCommon, commentRepo comment.CommentRepo, userCommon *usercommon.UserCommon, answerRepo answercommon.AnswerRepo, mcpController *MCPController, aiConversationService ai_conversation.AIConversationService, featureToggleSvc *feature_toggle.FeatureToggleService, ) *AIController { return &AIController{ searchService: searchService, siteInfoService: siteInfoService, tagCommonService: tagCommonService, questioncommon: questioncommon, commentRepo: commentRepo, userCommon: userCommon, answerRepo: answerRepo, mcpController: mcpController, aiConversationService: aiConversationService, featureToggleSvc: featureToggleSvc, } } func (c *AIController) ensureAIChatEnabled(ctx *gin.Context) bool { if c.featureToggleSvc == nil { return true } if err := c.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureAIChatbot); err != nil { handler.HandleResponse(ctx, err, nil) return false } return true } type ChatCompletionsRequest struct { Messages []Message `validate:"required,gte=1" json:"messages"` ConversationID string `json:"conversation_id"` UserID string `json:"-"` } type Message struct { Role string `json:"role" binding:"required"` Content string `json:"content" binding:"required"` } type ChatCompletionsResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []Choice `json:"choices"` Usage Usage `json:"usage"` } type StreamResponse struct { ChatCompletionID string `json:"chat_completion_id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []StreamChoice `json:"choices"` } type Choice struct { Index int `json:"index"` Message Message `json:"message"` FinishReason string `json:"finish_reason"` } type StreamChoice struct { Index int `json:"index"` Delta Delta `json:"delta"` FinishReason *string `json:"finish_reason"` } type Delta struct { Role string `json:"role,omitempty"` Content string `json:"content,omitempty"` } type Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } type ConversationContext struct { ConversationID string UserID string UserQuestion string Messages []*ai_conversation.ConversationMessage IsNewConversation bool Model string } func (c *ConversationContext) GetOpenAIMessages() []openai.ChatCompletionMessage { messages := make([]openai.ChatCompletionMessage, len(c.Messages)) for i, msg := range c.Messages { messages[i] = openai.ChatCompletionMessage{ Role: msg.Role, Content: msg.Content, } } return messages } // sendStreamData func sendStreamData(w http.ResponseWriter, data StreamResponse) { jsonData, err := json.Marshal(data) if err != nil { return } _, _ = fmt.Fprintf(w, "data: %s\n\n", string(jsonData)) if f, ok := w.(http.Flusher); ok { f.Flush() } } func (c *AIController) ChatCompletions(ctx *gin.Context) { if !c.ensureAIChatEnabled(ctx) { return } aiConfig, err := c.siteInfoService.GetSiteAI(context.Background()) if err != nil { log.Errorf("Failed to get AI config: %v", err) handler.HandleResponse(ctx, errors.BadRequest("AI service configuration error"), nil) return } if !aiConfig.Enabled { handler.HandleResponse(ctx, errors.ServiceUnavailable("AI service is not enabled"), nil) return } aiProvider := aiConfig.GetProvider() req := &ChatCompletionsRequest{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) data, _ := json.Marshal(req) log.Infof("ai chat request data: %s", string(data)) ctx.Header("Content-Type", "text/event-stream") ctx.Header("Cache-Control", "no-cache") ctx.Header("Connection", "keep-alive") ctx.Header("Access-Control-Allow-Origin", "*") ctx.Header("Access-Control-Allow-Headers", "Cache-Control") ctx.Status(http.StatusOK) w := ctx.Writer if f, ok := w.(http.Flusher); ok { f.Flush() } chatcmplID := "chatcmpl-" + token.GenerateToken() created := time.Now().Unix() firstResponse := StreamResponse{ ChatCompletionID: chatcmplID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: aiProvider.Model, Choices: []StreamChoice{{Index: 0, Delta: Delta{Role: "assistant"}, FinishReason: nil}}, } sendStreamData(w, firstResponse) conversationCtx := c.initializeConversationContext(ctx, aiProvider.Model, req) if conversationCtx == nil { log.Error("Failed to initialize conversation context") c.sendErrorResponse(w, chatcmplID, aiProvider.Model, "Failed to initialize conversation context") return } c.redirectRequestToAI(ctx, w, chatcmplID, conversationCtx) finishReason := "stop" endResponse := StreamResponse{ ChatCompletionID: chatcmplID, Object: "chat.completion.chunk", Created: created, Model: aiProvider.Model, Choices: []StreamChoice{{Index: 0, Delta: Delta{}, FinishReason: &finishReason}}, } sendStreamData(w, endResponse) _, _ = fmt.Fprintf(w, "data: [DONE]\n\n") if f, ok := w.(http.Flusher); ok { f.Flush() } c.saveConversationRecord(ctx, chatcmplID, conversationCtx) } func (c *AIController) redirectRequestToAI(ctx *gin.Context, w http.ResponseWriter, id string, conversationCtx *ConversationContext) { client := c.createOpenAIClient() c.handleAIConversation(ctx, w, id, client, conversationCtx) } // createOpenAIClient func (c *AIController) createOpenAIClient() *openai.Client { config := openai.DefaultConfig("") config.BaseURL = "" aiConfig, err := c.siteInfoService.GetSiteAI(context.Background()) if err != nil { log.Errorf("Failed to get AI config: %v", err) return openai.NewClientWithConfig(config) } if !aiConfig.Enabled { log.Warn("AI feature is disabled") return openai.NewClientWithConfig(config) } aiProvider := aiConfig.GetProvider() config = openai.DefaultConfig(aiProvider.APIKey) config.BaseURL = aiProvider.APIHost if !strings.HasSuffix(config.BaseURL, "/v1") { config.BaseURL += "/v1" } return openai.NewClientWithConfig(config) } // getPromptByLanguage func (c *AIController) getPromptByLanguage(language i18n.Language, question string) string { aiConfig, err := c.siteInfoService.GetSiteAI(context.Background()) if err != nil { log.Errorf("Failed to get AI config: %v", err) return c.getDefaultPrompt(language, question) } var promptTemplate string switch language { case i18n.LanguageChinese: promptTemplate = aiConfig.PromptConfig.ZhCN case i18n.LanguageEnglish: promptTemplate = aiConfig.PromptConfig.EnUS default: promptTemplate = aiConfig.PromptConfig.EnUS } if promptTemplate == "" { return c.getDefaultPrompt(language, question) } return fmt.Sprintf(promptTemplate, question) } // getDefaultPrompt prompt func (c *AIController) getDefaultPrompt(language i18n.Language, question string) string { switch language { case i18n.LanguageChinese: return fmt.Sprintf(constant.DefaultAIPromptConfigZhCN, question) case i18n.LanguageEnglish: return fmt.Sprintf(constant.DefaultAIPromptConfigEnUS, question) default: return fmt.Sprintf(constant.DefaultAIPromptConfigEnUS, question) } } // initializeConversationContext func (c *AIController) initializeConversationContext(ctx *gin.Context, model string, req *ChatCompletionsRequest) *ConversationContext { if len(req.ConversationID) == 0 { req.ConversationID = token.GenerateToken() } conversationCtx := &ConversationContext{ UserID: req.UserID, Messages: make([]*ai_conversation.ConversationMessage, 0), ConversationID: req.ConversationID, Model: model, } conversationDetail, exist, err := c.aiConversationService.GetConversationDetail(ctx, &schema.AIConversationDetailReq{ ConversationID: req.ConversationID, UserID: req.UserID, }) if err != nil { log.Errorf("Failed to get conversation detail: %v", err) return nil } if !exist { conversationCtx.UserQuestion = req.Messages[0].Content conversationCtx.Messages = c.buildInitialMessages(ctx, req) conversationCtx.IsNewConversation = true return conversationCtx } conversationCtx.IsNewConversation = false for _, record := range conversationDetail.Records { conversationCtx.Messages = append(conversationCtx.Messages, &ai_conversation.ConversationMessage{ ChatCompletionID: record.ChatCompletionID, Role: record.Role, Content: record.Content, }) } conversationCtx.Messages = append(conversationCtx.Messages, &ai_conversation.ConversationMessage{ Role: req.Messages[0].Role, Content: req.Messages[0].Content, }) return conversationCtx } // buildInitialMessages func (c *AIController) buildInitialMessages(ctx *gin.Context, req *ChatCompletionsRequest) []*ai_conversation.ConversationMessage { question := "" if len(req.Messages) == 1 { question = req.Messages[0].Content } else { messages := make([]*ai_conversation.ConversationMessage, len(req.Messages)) for i, msg := range req.Messages { messages[i] = &ai_conversation.ConversationMessage{ Role: msg.Role, Content: msg.Content, } } return messages } currentLang := handler.GetLangByCtx(ctx) prompt := c.getPromptByLanguage(currentLang, question) return []*ai_conversation.ConversationMessage{{Role: openai.ChatMessageRoleUser, Content: prompt}} } // saveConversationRecord func (c *AIController) saveConversationRecord(ctx context.Context, chatcmplID string, conversationCtx *ConversationContext) { if conversationCtx == nil || len(conversationCtx.Messages) == 0 { return } if conversationCtx.IsNewConversation { topic := conversationCtx.UserQuestion if topic == "" { log.Warn("No user message found for new conversation") return } err := c.aiConversationService.CreateConversation(ctx, conversationCtx.UserID, conversationCtx.ConversationID, topic) if err != nil { log.Errorf("Failed to create conversation: %v", err) return } } err := c.aiConversationService.SaveConversationRecords(ctx, conversationCtx.ConversationID, chatcmplID, conversationCtx.Messages) if err != nil { log.Errorf("Failed to save conversation records: %v", err) } } func (c *AIController) handleAIConversation(ctx *gin.Context, w http.ResponseWriter, id string, client *openai.Client, conversationCtx *ConversationContext) { maxRounds := 10 messages := conversationCtx.GetOpenAIMessages() for round := range maxRounds { log.Debugf("AI conversation round: %d", round+1) aiReq := openai.ChatCompletionRequest{ Model: conversationCtx.Model, Messages: messages, Tools: c.getMCPTools(), Stream: true, } toolCalls, newMessages, finished, aiResponse := c.processAIStream(ctx, w, id, conversationCtx.Model, client, aiReq, messages) messages = newMessages if aiResponse != "" { conversationCtx.Messages = append(conversationCtx.Messages, &ai_conversation.ConversationMessage{ Role: "assistant", Content: aiResponse, }) } if finished { return } if len(toolCalls) > 0 { messages = c.executeToolCalls(ctx, w, id, conversationCtx.Model, toolCalls, messages) } else { return } } log.Warnf("AI conversation reached maximum rounds limit: %d", maxRounds) } // processAIStream func (c *AIController) processAIStream( _ *gin.Context, w http.ResponseWriter, id, model string, client *openai.Client, aiReq openai.ChatCompletionRequest, messages []openai.ChatCompletionMessage) ( []openai.ToolCall, []openai.ChatCompletionMessage, bool, string) { stream, err := client.CreateChatCompletionStream(context.Background(), aiReq) if err != nil { log.Errorf("Failed to create stream: %v", err) c.sendErrorResponse(w, id, model, "Failed to create AI stream") return nil, messages, true, "" } defer func() { _ = stream.Close() }() var currentToolCalls []openai.ToolCall var accumulatedContent strings.Builder var accumulatedMessage openai.ChatCompletionMessage toolCallsMap := make(map[int]*openai.ToolCall) for { response, err := stream.Recv() if err != nil { if err.Error() == "EOF" { log.Info("Stream finished") break } log.Errorf("Stream error: %v", err) break } choice := response.Choices[0] if len(choice.Delta.ToolCalls) > 0 { for _, deltaToolCall := range choice.Delta.ToolCalls { index := *deltaToolCall.Index if _, exists := toolCallsMap[index]; !exists { toolCallsMap[index] = &openai.ToolCall{ ID: deltaToolCall.ID, Type: deltaToolCall.Type, Function: openai.FunctionCall{ Name: deltaToolCall.Function.Name, Arguments: deltaToolCall.Function.Arguments, }, } } else { if deltaToolCall.Function.Arguments != "" { toolCallsMap[index].Function.Arguments += deltaToolCall.Function.Arguments } if deltaToolCall.Function.Name != "" { toolCallsMap[index].Function.Name = deltaToolCall.Function.Name } } } } if choice.Delta.Content != "" { accumulatedContent.WriteString(choice.Delta.Content) contentResponse := StreamResponse{ ChatCompletionID: id, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: model, Choices: []StreamChoice{ { Index: 0, Delta: Delta{ Content: choice.Delta.Content, }, FinishReason: nil, }, }, } sendStreamData(w, contentResponse) } if len(choice.FinishReason) > 0 { if choice.FinishReason == "tool_calls" { for _, toolCall := range toolCallsMap { currentToolCalls = append(currentToolCalls, *toolCall) } return currentToolCalls, messages, false, accumulatedContent.String() } else { aiResponseContent := accumulatedContent.String() if aiResponseContent != "" { accumulatedMessage = openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleAssistant, Content: aiResponseContent, } messages = append(messages, accumulatedMessage) } return nil, messages, true, aiResponseContent } } } aiResponseContent := accumulatedContent.String() if aiResponseContent != "" { accumulatedMessage = openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleAssistant, Content: aiResponseContent, } messages = append(messages, accumulatedMessage) } if len(toolCallsMap) > 0 { for _, toolCall := range toolCallsMap { currentToolCalls = append(currentToolCalls, *toolCall) } return currentToolCalls, messages, false, aiResponseContent } return currentToolCalls, messages, len(currentToolCalls) == 0, aiResponseContent } // executeToolCalls func (c *AIController) executeToolCalls(ctx *gin.Context, _ http.ResponseWriter, _, _ string, toolCalls []openai.ToolCall, messages []openai.ChatCompletionMessage) []openai.ChatCompletionMessage { validToolCalls := make([]openai.ToolCall, 0) for _, toolCall := range toolCalls { if toolCall.ID == "" || toolCall.Function.Name == "" { log.Errorf("Invalid tool call: missing required fields. ID: %s, Function: %v", toolCall.ID, toolCall.Function) continue } if toolCall.Function.Arguments == "" { toolCall.Function.Arguments = "{}" } validToolCalls = append(validToolCalls, toolCall) log.Debugf("Valid tool call: ID=%s, Name=%s, Arguments=%s", toolCall.ID, toolCall.Function.Name, toolCall.Function.Arguments) } if len(validToolCalls) == 0 { log.Warn("No valid tool calls found") return messages } assistantMsg := openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleAssistant, ToolCalls: validToolCalls, } messages = append(messages, assistantMsg) for _, toolCall := range validToolCalls { if toolCall.Function.Name != "" { var args map[string]any if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { log.Errorf("Failed to parse tool arguments for %s: %v, arguments: %s", toolCall.Function.Name, err, toolCall.Function.Arguments) errorResult := fmt.Sprintf("Error parsing tool arguments: %v", err) toolMessage := openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleTool, Content: errorResult, ToolCallID: toolCall.ID, } messages = append(messages, toolMessage) continue } result, err := c.callMCPTool(ctx, toolCall.Function.Name, args) if err != nil { log.Errorf("Failed to call MCP tool %s: %v", toolCall.Function.Name, err) result = fmt.Sprintf("Error calling tool %s: %v", toolCall.Function.Name, err) } toolMessage := openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleTool, Content: result, ToolCallID: toolCall.ID, } messages = append(messages, toolMessage) } } return messages } // sendErrorResponse send error response in stream func (c *AIController) sendErrorResponse(w http.ResponseWriter, id, model, errorMsg string) { errorResponse := StreamResponse{ ChatCompletionID: id, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: model, Choices: []StreamChoice{ { Index: 0, Delta: Delta{ Content: fmt.Sprintf("Error: %s", errorMsg), }, FinishReason: nil, }, }, } sendStreamData(w, errorResponse) } // getMCPTools func (c *AIController) getMCPTools() []openai.Tool { openaiTools := make([]openai.Tool, 0) for _, mcpTool := range mcp_tools.MCPToolsList { openaiTool := c.convertMCPToolToOpenAI(mcpTool) openaiTools = append(openaiTools, openaiTool) } return openaiTools } // convertMCPToolToOpenAI func (c *AIController) convertMCPToolToOpenAI(mcpTool mcp.Tool) openai.Tool { properties := make(map[string]any) required := make([]string, 0) maps.Copy(properties, mcpTool.InputSchema.Properties) required = append(required, mcpTool.InputSchema.Required...) parameters := map[string]any{ "type": "object", "properties": properties, } if len(required) > 0 { parameters["required"] = required } return openai.Tool{ Type: openai.ToolTypeFunction, Function: &openai.FunctionDefinition{ Name: mcpTool.Name, Description: mcpTool.Description, Parameters: parameters, }, } } // callMCPTool func (c *AIController) callMCPTool(ctx context.Context, toolName string, arguments map[string]any) (string, error) { request := mcp.CallToolRequest{ Request: mcp.Request{}, Params: struct { Name string `json:"name"` Arguments any `json:"arguments,omitempty"` Meta *mcp.Meta `json:"_meta,omitempty"` }{ Name: toolName, Arguments: arguments, }, } var result *mcp.CallToolResult var err error log.Debugf("Calling MCP tool: %s with arguments: %v", toolName, arguments) switch toolName { case "get_questions": result, err = c.mcpController.MCPQuestionsHandler()(ctx, request) case "get_answers_by_question_id": result, err = c.mcpController.MCPAnswersHandler()(ctx, request) case "get_comments": result, err = c.mcpController.MCPCommentsHandler()(ctx, request) case "get_tags": result, err = c.mcpController.MCPTagsHandler()(ctx, request) case "get_tag_detail": result, err = c.mcpController.MCPTagDetailsHandler()(ctx, request) case "get_user": result, err = c.mcpController.MCPUserDetailsHandler()(ctx, request) default: return "", fmt.Errorf("unknown tool: %s", toolName) } if err != nil { return "", err } data, _ := json.Marshal(result) log.Debugf("MCP tool %s called successfully, result: %v", toolName, string(data)) if result != nil && len(result.Content) > 0 { if textContent, ok := result.Content[0].(mcp.TextContent); ok { return textContent.Text, nil } } return "No result found", nil } ================================================ FILE: internal/controller/ai_conversation_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/ai_conversation" "github.com/apache/answer/internal/service/feature_toggle" "github.com/gin-gonic/gin" ) // AIConversationController ai conversation controller type AIConversationController struct { aiConversationService ai_conversation.AIConversationService featureToggleSvc *feature_toggle.FeatureToggleService } // NewAIConversationController creates a new AI conversation controller func NewAIConversationController( aiConversationService ai_conversation.AIConversationService, featureToggleSvc *feature_toggle.FeatureToggleService, ) *AIConversationController { return &AIConversationController{ aiConversationService: aiConversationService, featureToggleSvc: featureToggleSvc, } } func (ctrl *AIConversationController) ensureEnabled(ctx *gin.Context) bool { if ctrl.featureToggleSvc == nil { return true } if err := ctrl.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureAIChatbot); err != nil { handler.HandleResponse(ctx, err, nil) return false } return true } // GetConversationList gets conversation list // @Summary get conversation list // @Description get conversation list // @Tags ai-conversation // @Accept json // @Produce json // @Param page query int false "page" // @Param page_size query int false "page size" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.AIConversationListItem}} // @Router /answer/api/v1/ai/conversation/page [get] func (ctrl *AIConversationController) GetConversationList(ctx *gin.Context) { if !ctrl.ensureEnabled(ctx) { return } req := &schema.AIConversationListReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := ctrl.aiConversationService.GetConversationList(ctx, req) handler.HandleResponse(ctx, err, resp) } // GetConversationDetail gets conversation detail // @Summary get conversation detail // @Description get conversation detail // @Tags ai-conversation // @Accept json // @Produce json // @Param conversation_id query string true "conversation id" // @Success 200 {object} handler.RespBody{data=schema.AIConversationDetailResp} // @Router /answer/api/v1/ai/conversation [get] func (ctrl *AIConversationController) GetConversationDetail(ctx *gin.Context) { if !ctrl.ensureEnabled(ctx) { return } req := &schema.AIConversationDetailReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, _, err := ctrl.aiConversationService.GetConversationDetail(ctx, req) handler.HandleResponse(ctx, err, resp) } // VoteRecord vote record // @Summary vote record // @Description vote record // @Tags ai-conversation // @Accept json // @Produce json // @Param data body schema.AIConversationVoteReq true "vote request" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/ai/conversation/vote [post] func (ctrl *AIConversationController) VoteRecord(ctx *gin.Context) { if !ctrl.ensureEnabled(ctx) { return } req := &schema.AIConversationVoteReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) err := ctrl.aiConversationService.VoteRecord(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller/answer_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "fmt" "net/http" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/action" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/permission" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // AnswerController answer controller type AnswerController struct { answerService *content.AnswerService rankService *rank.RankService actionService *action.CaptchaService siteInfoCommonService siteinfo_common.SiteInfoCommonService rateLimitMiddleware *middleware.RateLimitMiddleware } // NewAnswerController new controller func NewAnswerController( answerService *content.AnswerService, rankService *rank.RankService, actionService *action.CaptchaService, siteInfoCommonService siteinfo_common.SiteInfoCommonService, rateLimitMiddleware *middleware.RateLimitMiddleware, ) *AnswerController { return &AnswerController{ answerService: answerService, rankService: rankService, actionService: actionService, siteInfoCommonService: siteInfoCommonService, rateLimitMiddleware: rateLimitMiddleware, } } // RemoveAnswer delete answer // @Summary delete answer // @Description delete answer // @Tags Answer // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.RemoveAnswerReq true "answer" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/answer [delete] func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) { req := &schema.RemoveAnswerReq{} if handler.BindAndCheck(ctx, req) { return } req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.AnswerDelete, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanDelete = canList[0] || objectOwner if !req.CanDelete { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = ac.answerService.RemoveAnswer(ctx, req) if !isAdmin { ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID) } handler.HandleResponse(ctx, err, nil) } // RecoverAnswer recover answer // @Summary recover answer // @Description recover the deleted answer // @Tags Answer // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.RecoverAnswerReq true "answer" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/answer/recover [post] func (ac *AnswerController) RecoverAnswer(ctx *gin.Context) { req := &schema.RecoverAnswerReq{} if handler.BindAndCheck(ctx, req) { return } req.AnswerID = uid.DeShortID(req.AnswerID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.AnswerUnDelete, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !canList[0] { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = ac.answerService.RecoverAnswer(ctx, req) handler.HandleResponse(ctx, err, nil) } // GetAnswerInfo get answer info // @Summary Get Answer Detail // @Description Get Answer Detail // @Tags Answer // @Accept json // @Produce json // @Param id query string true "id" // @Success 200 {object} handler.RespBody{data=schema.GetAnswerInfoResp} // @Router /answer/api/v1/answer/info [get] func (ac *AnswerController) GetAnswerInfo(ctx *gin.Context) { id := ctx.Query("id") id = uid.DeShortID(id) userID := middleware.GetLoginUserIDFromContext(ctx) info, questionInfo, has, err := ac.answerService.Get(ctx, id, userID) if err != nil { handler.HandleResponse(ctx, err, gin.H{}) return } if !has { handler.HandleResponse(ctx, fmt.Errorf(""), gin.H{}) return } handler.HandleResponse(ctx, err, &schema.GetAnswerInfoResp{ Info: info, Question: questionInfo, }) } // AddAnswer add answer // @Summary Add Answer // @Description add answer // @Tags Answer // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.AnswerAddReq true "add answer request" // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/answer [post] func (ac *AnswerController) AddAnswer(ctx *gin.Context) { req := &schema.AnswerAddReq{} if handler.BindAndCheck(ctx, req) { return } reject, rejectKey := ac.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) if reject { return } defer func() { // If status is not 200 means that the bad request has been returned, so the record should be cleared if ctx.Writer.Status() != http.StatusOK { ac.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey) } }() req.QuestionID = uid.DeShortID(req.QuestionID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.AnswerEdit, permission.AnswerDelete, permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } linkUrlLimitUser := canList[2] isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin || !linkUrlLimitUser { captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionAnswer, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAdd, "") if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } write, err := ac.siteInfoCommonService.GetSiteQuestion(ctx) if err != nil { handler.HandleResponse(ctx, err, nil) return } if write.RestrictAnswer { // check if there's already an answer by this user ids, err := ac.answerService.GetCountByUserIDQuestionID(ctx, req.UserID, req.QuestionID) if err != nil { handler.HandleResponse(ctx, err, nil) return } if len(ids) >= 1 { handler.HandleResponse(ctx, errors.Forbidden(reason.AnswerRestrictAnswer), nil) return } } req.UserAgent = ctx.GetHeader("User-Agent") req.IP = ctx.ClientIP() answerID, err := ac.answerService.Insert(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !isAdmin || !linkUrlLimitUser { ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionAnswer, req.UserID) } info, questionInfo, has, err := ac.answerService.Get(ctx, answerID, req.UserID) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !has { handler.HandleResponse(ctx, nil, nil) return } objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, info.ID) req.CanEdit = canList[0] || objectOwner req.CanDelete = canList[1] || objectOwner info.MemberActions = permission.GetAnswerPermission(ctx, req.UserID, info.UserID, 0, req.CanEdit, req.CanDelete, false) handler.HandleResponse(ctx, nil, gin.H{ "info": info, "question": questionInfo, }) } // UpdateAnswer update answer // @Summary Update Answer // @Description Update Answer // @Tags Answer // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.AnswerUpdateReq true "AnswerUpdateReq" // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/answer [put] func (ac *AnswerController) UpdateAnswer(ctx *gin.Context) { req := &schema.AnswerUpdateReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.AnswerEdit, permission.AnswerEditWithoutReview, permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } linkUrlLimitUser := canList[2] isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin || !linkUrlLimitUser { captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) req.CanEdit = canList[0] || objectOwner req.NoNeedReview = canList[1] || objectOwner if !req.CanEdit { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } _, err = ac.answerService.Update(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !isAdmin || !linkUrlLimitUser { ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID) } _, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, &schema.AnswerUpdateResp{WaitForReview: !req.NoNeedReview}) } // AnswerList godoc // @Summary AnswerList // @Description AnswerList
order (default or updated) // @Tags Answer // @Accept json // @Produce json // @Param question_id query string true "question_id" // @Param order query string true "order" // @Param page query string true "page" // @Param page_size query string true "page_size" // @Success 200 {string} string "" // @Router /answer/api/v1/answer/page [get] func (ac *AnswerController) AnswerList(ctx *gin.Context) { req := &schema.AnswerListReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.QuestionID = uid.DeShortID(req.QuestionID) canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.AnswerEdit, permission.AnswerDelete, permission.AnswerUnDelete, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanEdit = canList[0] req.CanDelete = canList[1] req.CanRecover = canList[2] list, count, err := ac.answerService.SearchList(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, gin.H{ "list": list, "count": count, }) } // AcceptAnswer accept answer // @Summary Accept Answer // @Description Accept Answer // @Tags Answer // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.AcceptAnswerReq true "AcceptAnswerReq" // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/answer/acceptance [post] func (ac *AnswerController) AcceptAnswer(ctx *gin.Context) { req := &schema.AcceptAnswerReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.AnswerID = uid.DeShortID(req.AnswerID) req.QuestionID = uid.DeShortID(req.QuestionID) can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAccept, req.QuestionID) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = ac.answerService.AcceptAnswer(ctx, req) handler.HandleResponse(ctx, err, nil) } // AdminUpdateAnswerStatus update answer status // @Summary update answer status // @Description update answer status // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.AdminUpdateAnswerStatusReq true "AdminUpdateAnswerStatusReq" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/answer/status [put] func (ac *AnswerController) AdminUpdateAnswerStatus(ctx *gin.Context) { req := &schema.AdminUpdateAnswerStatusReq{} if handler.BindAndCheck(ctx, req) { return } req.AnswerID = uid.DeShortID(req.AnswerID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) err := ac.answerService.AdminSetAnswerStatus(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller/badge_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/badge" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" ) type BadgeController struct { badgeService *badge.BadgeService badgeAwardService *badge.BadgeAwardService } func NewBadgeController( badgeService *badge.BadgeService, badgeAwardService *badge.BadgeAwardService) *BadgeController { return &BadgeController{ badgeService: badgeService, badgeAwardService: badgeAwardService, } } // GetBadgeList list all badges // @Summary list all badges group by group // @Description list all badges group by group // @Tags api-badge // @Accept json // @Produce json // @Success 200 {object} handler.RespBody{data=[]schema.GetBadgeListResp} // @Router /answer/api/v1/badges [get] func (b *BadgeController) GetBadgeList(ctx *gin.Context) { userID := middleware.GetLoginUserIDFromContext(ctx) resp, err := b.badgeService.ListByGroup(ctx, userID) handler.HandleResponse(ctx, err, resp) } // GetBadgeInfo get badge info // @Summary get badge info // @Description get badge info // @Tags api-badge // @Accept json // @Produce json // @Param id query string true "id" default(string) // @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp} // @Router /answer/api/v1/badge [get] func (b *BadgeController) GetBadgeInfo(ctx *gin.Context) { id := ctx.Query("id") id = uid.DeShortID(id) userID := middleware.GetLoginUserIDFromContext(ctx) resp, err := b.badgeService.GetBadgeInfo(ctx, id, userID) handler.HandleResponse(ctx, err, resp) } // GetBadgeAwardList get badge award list // @Summary get badge award list // @Description get badge award list // @Tags api-badge // @Accept json // @Produce json // @Param page query int false "page" // @Param page_size query int false "page size" // @Param badge_id query string true "badge id" // @Param username query string false "only list the award by username" // @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp} // @Router /answer/api/v1/badge/awards/page [get] func (b *BadgeController) GetBadgeAwardList(ctx *gin.Context) { req := &schema.GetBadgeAwardWithPageReq{} if handler.BindAndCheck(ctx, req) { return } req.BadgeID = uid.DeShortID(req.BadgeID) resp, total, err := b.badgeAwardService.GetBadgeAwardList(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) } // GetAllBadgeAwardListByUsername get user badge award list // @Summary get user badge award list // @Description get user badge award list // @Tags api-badge // @Accept json // @Produce json // @Param username query string true "user name" // @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp} // @Router /answer/api/v1/badge/user/awards [get] func (b *BadgeController) GetAllBadgeAwardListByUsername(ctx *gin.Context) { req := &schema.GetUserBadgeAwardListReq{} if handler.BindAndCheck(ctx, req) { return } resp, total, err := b.badgeAwardService.GetUserBadgeAwardList(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) } // GetRecentBadgeAwardListByUsername get user badge award list // @Summary get user badge award list // @Description get user badge award list // @Tags api-badge // @Accept json // @Produce json // @Param username query string true "user name" // @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp} // @Router /answer/api/v1/badge/user/awards/recent [get] func (b *BadgeController) GetRecentBadgeAwardListByUsername(ctx *gin.Context) { req := &schema.GetUserBadgeAwardListReq{} if handler.BindAndCheck(ctx, req) { return } req.Limit = 10 resp, total, err := b.badgeAwardService.GetUserRecentBadgeAwardList(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) } ================================================ FILE: internal/controller/collection_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/collection" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" ) // CollectionController collection controller type CollectionController struct { collectionService *collection.CollectionService } // NewCollectionController new controller func NewCollectionController(collectionService *collection.CollectionService) *CollectionController { return &CollectionController{collectionService: collectionService} } // CollectionSwitch add collection // @Summary add collection // @Description add collection // @Tags Collection // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.CollectionSwitchReq true "collection" // @Success 200 {object} handler.RespBody{data=schema.CollectionSwitchResp} // @Router /answer/api/v1/collection/switch [post] func (cc *CollectionController) CollectionSwitch(ctx *gin.Context) { req := &schema.CollectionSwitchReq{} if handler.BindAndCheck(ctx, req) { return } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := cc.collectionService.CollectionSwitch(ctx, req) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller/comment_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "net/http" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/action" "github.com/apache/answer/internal/service/comment" "github.com/apache/answer/internal/service/permission" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // CommentController comment controller type CommentController struct { commentService *comment.CommentService rankService *rank.RankService actionService *action.CaptchaService rateLimitMiddleware *middleware.RateLimitMiddleware } // NewCommentController new controller func NewCommentController( commentService *comment.CommentService, rankService *rank.RankService, actionService *action.CaptchaService, rateLimitMiddleware *middleware.RateLimitMiddleware, ) *CommentController { return &CommentController{ commentService: commentService, rankService: rankService, actionService: actionService, rateLimitMiddleware: rateLimitMiddleware, } } // AddComment add comment // @Summary add comment // @Description add comment // @Tags Comment // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.AddCommentReq true "comment" // @Success 200 {object} handler.RespBody{data=schema.GetCommentResp} // @Router /answer/api/v1/comment [post] func (cc *CommentController) AddComment(ctx *gin.Context) { req := &schema.AddCommentReq{} if handler.BindAndCheck(ctx, req) { return } reject, rejectKey := cc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) if reject { return } defer func() { // If status is not 200 means that the bad request has been returned, so the record should be cleared if ctx.Writer.Status() != http.StatusOK { cc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey) } }() req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.CommentAdd, permission.CommentEdit, permission.CommentDelete, permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } linkUrlLimitUser := canList[3] isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin || !linkUrlLimitUser { captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionComment, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } req.CanAdd = canList[0] req.CanEdit = canList[1] req.CanDelete = canList[2] if !req.CanAdd { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } req.UserAgent = ctx.GetHeader("User-Agent") req.IP = ctx.ClientIP() resp, err := cc.commentService.AddComment(ctx, req) if !isAdmin || !linkUrlLimitUser { cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionComment, req.UserID) } handler.HandleResponse(ctx, err, resp) } // RemoveComment remove comment // @Summary remove comment // @Description remove comment // @Tags Comment // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.RemoveCommentReq true "comment" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/comment [delete] func (cc *CommentController) RemoveComment(ctx *gin.Context) { req := &schema.RemoveCommentReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentDelete, req.CommentID) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = cc.commentService.RemoveComment(ctx, req) if !isAdmin { cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID) } handler.HandleResponse(ctx, err, nil) } // UpdateComment update comment // @Summary update comment // @Description update comment // @Tags Comment // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UpdateCommentReq true "comment" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/comment [put] func (cc *CommentController) UpdateComment(ctx *gin.Context) { req := &schema.UpdateCommentReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetIsAdminFromContext(ctx) canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.CommentEdit, permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanEdit = canList[0] || cc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.CommentID) linkUrlLimitUser := canList[1] if !req.CanEdit { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } if !req.IsAdmin || !linkUrlLimitUser { captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } resp, err := cc.commentService.UpdateComment(ctx, req) if !req.IsAdmin || !linkUrlLimitUser { cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID) } handler.HandleResponse(ctx, err, resp) } // GetCommentWithPage get comment page // @Summary get comment page // @Description get comment page // @Tags Comment // @Produce json // @Param page query int false "page" // @Param page_size query int false "page size" // @Param object_id query string true "object id" // @Param query_cond query string false "query condition" Enums(vote) // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentResp}} // @Router /answer/api/v1/comment/page [get] func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) { req := &schema.GetCommentWithPageReq{} if handler.BindAndCheck(ctx, req) { return } req.ObjectID = uid.DeShortID(req.ObjectID) req.CommentID = uid.DeShortID(req.CommentID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.CommentEdit, permission.CommentDelete, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanEdit = canList[0] req.CanDelete = canList[1] resp, err := cc.commentService.GetCommentWithPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // GetCommentPersonalWithPage user personal comment list // @Summary user personal comment list // @Description user personal comment list // @Tags Comment // @Produce json // @Param page query int false "page" // @Param page_size query int false "page size" // @Param username query string false "username" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentPersonalWithPageResp}} // @Router /answer/api/v1/personal/comment/page [get] func (cc *CommentController) GetCommentPersonalWithPage(ctx *gin.Context) { req := &schema.GetCommentPersonalWithPageReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := cc.commentService.GetCommentPersonalWithPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // GetComment godoc // @Summary get comment by id // @Description get comment by id // @Tags Comment // @Produce json // @Param id query string true "id" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentResp}} // @Router /answer/api/v1/comment [get] func (cc *CommentController) GetComment(ctx *gin.Context) { req := &schema.GetCommentReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.CommentEdit, permission.CommentDelete, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanEdit = canList[0] req.CanDelete = canList[1] resp, err := cc.commentService.GetComment(ctx, req) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller/connector_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "fmt" "net/http" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/internal/service/user_external_login" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) const ( commonRouterPrefix = "/answer/api/v1" ConnectorLoginRouterPrefix = "/connector/login/" ConnectorRedirectRouterPrefix = "/connector/redirect/" ) // ConnectorController comment controller type ConnectorController struct { siteInfoService siteinfo_common.SiteInfoCommonService userExternalService *user_external_login.UserExternalLoginService emailService *export.EmailService } // NewConnectorController new controller func NewConnectorController( siteInfoService siteinfo_common.SiteInfoCommonService, emailService *export.EmailService, userExternalService *user_external_login.UserExternalLoginService, ) *ConnectorController { return &ConnectorController{ siteInfoService: siteInfoService, userExternalService: userExternalService, emailService: emailService, } } // ConnectorLoginDispatcher dispatch connector login request to specific connector by slug name // We can't register specific router for each connector when application start, because the plugin status will be changed by admin. // If the plugin is disabled, the router should be unavailable. func (cc *ConnectorController) ConnectorLoginDispatcher(ctx *gin.Context) { slugName := ctx.Param("name") var c plugin.Connector _ = plugin.CallConnector(func(connector plugin.Connector) error { if connector.ConnectorSlugName() == slugName { c = connector } return nil }) if c == nil { log.Errorf("connector %s not found", slugName) ctx.Redirect(http.StatusFound, "/50x") return } cc.ConnectorLogin(c)(ctx) } func (cc *ConnectorController) ConnectorRedirectDispatcher(ctx *gin.Context) { slugName := ctx.Param("name") var c plugin.Connector _ = plugin.CallConnector(func(connector plugin.Connector) error { if connector.ConnectorSlugName() == slugName { c = connector } return nil }) if c == nil { log.Errorf("connector %s not found", slugName) ctx.Redirect(http.StatusFound, "/50x") return } cc.ConnectorRedirect(c)(ctx) } func (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn func(ctx *gin.Context)) { return func(ctx *gin.Context) { general, err := cc.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Error(err) ctx.Redirect(http.StatusFound, "/50x") return } receiverURL := fmt.Sprintf("%s%s%s%s", general.SiteUrl, commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName()) redirectURL := connector.ConnectorSender(ctx, receiverURL) if len(redirectURL) > 0 { ctx.Redirect(http.StatusFound, redirectURL) } } } func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn func(ctx *gin.Context)) { return func(ctx *gin.Context) { siteGeneral, err := cc.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site info failed: %v", err) ctx.Redirect(http.StatusFound, "/50x") return } receiverURL := fmt.Sprintf("%s%s%s%s", siteGeneral.SiteUrl, commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName()) userInfo, err := connector.ConnectorReceiver(ctx, receiverURL) if err != nil { log.Errorf("connector received failed, error info: %v, response data is: %s", err, userInfo.MetaInfo) ctx.Redirect(http.StatusFound, "/50x") return } log.Debugf("connector received: %+v", userInfo) u := &schema.ExternalLoginUserInfoCache{ Provider: connector.ConnectorSlugName(), ExternalID: userInfo.ExternalID, DisplayName: userInfo.DisplayName, Username: userInfo.Username, Email: userInfo.Email, Avatar: userInfo.Avatar, MetaInfo: userInfo.MetaInfo, } resp, err := cc.userExternalService.ExternalLogin(ctx, u) if err != nil { log.Errorf("external login failed: %v", err) ctx.Redirect(http.StatusFound, "/50x") return } if len(resp.ErrMsg) > 0 { ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg)) return } if len(resp.AccessToken) > 0 { ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s", siteGeneral.SiteUrl, resp.AccessToken)) } else { ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/confirm-email?binding_key=%s", siteGeneral.SiteUrl, resp.BindingKey)) } } } // ConnectorsInfo get all enabled connectors // @Summary get all enabled connectors // @Description get all enabled connectors // @Tags PluginConnector // @Security ApiKeyAuth // @Produce json // @Success 200 {object} handler.RespBody{data=[]schema.ConnectorInfoResp} // @Router /answer/api/v1/connector/info [get] func (cc *ConnectorController) ConnectorsInfo(ctx *gin.Context) { general, err := cc.siteInfoService.GetSiteGeneral(ctx) if err != nil { handler.HandleResponse(ctx, err, nil) return } resp := make([]*schema.ConnectorInfoResp, 0) _ = plugin.CallConnector(func(fn plugin.Connector) error { connectorName := fn.ConnectorName() resp = append(resp, &schema.ConnectorInfoResp{ Name: connectorName.Translate(ctx), Icon: fn.ConnectorLogoSVG(), Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl, commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()), }) return nil }) handler.HandleResponse(ctx, nil, resp) } // ExternalLoginBindingUserSendEmail external login binding user send email // @Summary external login binding user send email // @Description external login binding user send email // @Tags PluginConnector // @Accept json // @Produce json // @Param data body schema.ExternalLoginBindingUserSendEmailReq true "external login binding user send email" // @Success 200 {object} handler.RespBody{data=schema.ExternalLoginBindingUserSendEmailResp} // @Router /answer/api/v1/connector/binding/email [post] func (cc *ConnectorController) ExternalLoginBindingUserSendEmail(ctx *gin.Context) { req := &schema.ExternalLoginBindingUserSendEmailReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := cc.userExternalService.ExternalLoginBindingUserSendEmail(ctx, req) handler.HandleResponse(ctx, err, resp) } // ConnectorsUserInfo get all connectors info about user // @Summary get all connectors info about user // @Description get all connectors info about user // @Tags PluginConnector // @Security ApiKeyAuth // @Produce json // @Success 200 {object} handler.RespBody{data=[]schema.ConnectorUserInfoResp} // @Router /answer/api/v1/connector/user/info [get] func (cc *ConnectorController) ConnectorsUserInfo(ctx *gin.Context) { general, err := cc.siteInfoService.GetSiteGeneral(ctx) if err != nil { handler.HandleResponse(ctx, err, nil) return } userID := middleware.GetLoginUserIDFromContext(ctx) userInfoList, err := cc.userExternalService.GetExternalLoginUserInfoList(ctx, userID) if err != nil { handler.HandleResponse(ctx, err, nil) return } userExternalLoginMapping := make(map[string]string) for _, userInfo := range userInfoList { userExternalLoginMapping[userInfo.Provider] = userInfo.ExternalID } resp := make([]*schema.ConnectorUserInfoResp, 0) _ = plugin.CallConnector(func(fn plugin.Connector) error { externalID := userExternalLoginMapping[fn.ConnectorSlugName()] connectorName := fn.ConnectorName() resp = append(resp, &schema.ConnectorUserInfoResp{ Name: connectorName.Translate(ctx), Icon: fn.ConnectorLogoSVG(), Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl, commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()), Binding: len(externalID) > 0, ExternalID: externalID, }) return nil }) handler.HandleResponse(ctx, nil, resp) } // ExternalLoginUnbinding unbind external user login // @Summary unbind external user login // @Description unbind external user login // @Tags PluginConnector // @Security ApiKeyAuth // @Accept json // @Produce json // @Param data body schema.ExternalLoginUnbindingReq true "ExternalLoginUnbindingReq" // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/connector/user/unbinding [delete] func (cc *ConnectorController) ExternalLoginUnbinding(ctx *gin.Context) { req := &schema.ExternalLoginUnbindingReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := cc.userExternalService.ExternalLoginUnbinding(ctx, req) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller/controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import "github.com/google/wire" // ProviderSetController is controller providers. var ProviderSetController = wire.NewSet( NewLangController, NewCommentController, NewReportController, NewVoteController, NewTagController, NewFollowController, NewCollectionController, NewUserController, NewQuestionController, NewAnswerController, NewSearchController, NewRevisionController, NewRankController, NewReasonController, NewNotificationController, NewSiteInfoController, NewDashboardController, NewUploadController, NewActivityController, NewTemplateController, NewConnectorController, NewUserCenterController, NewPermissionController, NewUserPluginController, NewReviewController, NewCaptchaController, NewMetaController, NewEmbedController, NewBadgeController, NewRenderController, NewSidebarController, NewMCPController, NewAIController, NewAIConversationController, ) ================================================ FILE: internal/controller/dashboard_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/service/dashboard" "github.com/gin-gonic/gin" ) type DashboardController struct { dashboardService dashboard.DashboardService } // NewDashboardController new controller func NewDashboardController( dashboardService dashboard.DashboardService, ) *DashboardController { return &DashboardController{ dashboardService: dashboardService, } } // DashboardInfo godoc // @Summary DashboardInfo // @Description DashboardInfo // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth // @Router /answer/admin/api/dashboard [get] // @Success 200 {object} handler.RespBody func (ac *DashboardController) DashboardInfo(ctx *gin.Context) { info, err := ac.dashboardService.Statistical(ctx) handler.HandleResponse(ctx, err, gin.H{ "info": info, }) } ================================================ FILE: internal/controller/embed_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" ) type EmbedController struct { } func NewEmbedController() *EmbedController { return &EmbedController{} } // GetEmbedConfig get embed plugin config // @Summary get embed plugin config // @Description get embed plugin config // @Tags Plugin // @Accept json // @Produce json // @Success 200 {object} handler.RespBody{data=[]plugin.EmbedConfig} // @Router /answer/api/v1/embed/config [get] func (c *EmbedController) GetEmbedConfig(ctx *gin.Context) { resp := make([]*plugin.EmbedConfig, 0) err := plugin.CallEmbed(func(embed plugin.Embed) (err error) { resp, err = embed.GetEmbedConfigs(ctx) return err }) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller/follow_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/follow" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/jinzhu/copier" ) // FollowController activity controller type FollowController struct { followService *follow.FollowService } // NewFollowController new controller func NewFollowController(followService *follow.FollowService) *FollowController { return &FollowController{followService: followService} } // Follow godoc // @Summary follow object or cancel follow operation // @Description follow object or cancel follow operation // @Tags Activity // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.FollowReq true "follow" // @Success 200 {object} handler.RespBody{data=schema.FollowResp} // @Router /answer/api/v1/follow [post] func (fc *FollowController) Follow(ctx *gin.Context) { req := &schema.FollowReq{} if handler.BindAndCheck(ctx, req) { return } req.ObjectID = uid.DeShortID(req.ObjectID) dto := &schema.FollowDTO{} _ = copier.Copy(dto, req) dto.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := fc.followService.Follow(ctx, dto) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) } else { handler.HandleResponse(ctx, err, resp) } } // UpdateFollowTags update user follow tags // @Summary update user follow tags // @Description update user follow tags // @Tags Activity // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UpdateFollowTagsReq true "follow" // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/follow/tags [put] func (fc *FollowController) UpdateFollowTags(ctx *gin.Context) { req := &schema.UpdateFollowTagsReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) err := fc.followService.UpdateFollowTags(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller/lang_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "encoding/json" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) type LangController struct { translator i18n.Translator siteInfoService siteinfo_common.SiteInfoCommonService } // NewLangController new language controller. func NewLangController(tr i18n.Translator, siteInfoService siteinfo_common.SiteInfoCommonService) *LangController { return &LangController{translator: tr, siteInfoService: siteInfoService} } // GetLangMapping get language config mapping // @Summary get language config mapping // @Description get language config mapping // @Tags Lang // @Param Accept-Language header string true "Accept-Language" // @Produce json // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/language/config [get] func (u *LangController) GetLangMapping(ctx *gin.Context) { data, _ := u.translator.Dump(handler.GetLangByCtx(ctx)) var resp map[string]any _ = json.Unmarshal(data, &resp) handler.HandleResponse(ctx, nil, resp) } // GetAdminLangOptions Get language options // @Summary Get language options // @Description Get language options // @Security ApiKeyAuth // @Tags Lang // @Produce json // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/language/options [get] func (u *LangController) GetAdminLangOptions(ctx *gin.Context) { handler.HandleResponse(ctx, nil, translator.LanguageOptions) } // GetUserLangOptions Get language options // @Summary Get language options // @Description Get language options // @Tags Lang // @Produce json // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/language/options [get] func (u *LangController) GetUserLangOptions(ctx *gin.Context) { siteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx) if err != nil { handler.HandleResponse(ctx, err, nil) return } options := translator.LanguageOptions if len(siteInterfaceResp.Language) > 0 { defaultOption := []*translator.LangOption{ {Label: translator.DefaultLangOption, Value: translator.DefaultLangOption}, } options = append(defaultOption, options...) } handler.HandleResponse(ctx, nil, options) } ================================================ FILE: internal/controller/mcp_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "context" "encoding/json" "fmt" "strings" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/comment" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/feature_toggle" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/siteinfo_common" tagcommonser "github.com/apache/answer/internal/service/tag_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/mark3labs/mcp-go/mcp" "github.com/segmentfault/pacman/log" ) type MCPController struct { searchService *content.SearchService siteInfoService siteinfo_common.SiteInfoCommonService tagCommonService *tagcommonser.TagCommonService questioncommon *questioncommon.QuestionCommon commentRepo comment.CommentRepo userCommon *usercommon.UserCommon answerRepo answercommon.AnswerRepo featureToggleSvc *feature_toggle.FeatureToggleService } // NewMCPController new site info controller. func NewMCPController( searchService *content.SearchService, siteInfoService siteinfo_common.SiteInfoCommonService, tagCommonService *tagcommonser.TagCommonService, questioncommon *questioncommon.QuestionCommon, commentRepo comment.CommentRepo, userCommon *usercommon.UserCommon, answerRepo answercommon.AnswerRepo, featureToggleSvc *feature_toggle.FeatureToggleService, ) *MCPController { return &MCPController{ searchService: searchService, siteInfoService: siteInfoService, tagCommonService: tagCommonService, questioncommon: questioncommon, commentRepo: commentRepo, userCommon: userCommon, answerRepo: answerRepo, featureToggleSvc: featureToggleSvc, } } func (c *MCPController) ensureMCPEnabled(ctx context.Context) error { if c.featureToggleSvc == nil { return nil } return c.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureMCP) } func (c *MCPController) MCPQuestionsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { if err := c.ensureMCPEnabled(ctx); err != nil { return nil, err } cond := schema.NewMCPSearchCond(request) siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general info failed: %v", err) return nil, err } searchResp, err := c.searchService.Search(ctx, &schema.SearchDTO{ Query: cond.ToQueryString() + " is:question", Page: 1, Size: 5, Order: "newest", }) if err != nil { return nil, err } resp := make([]*schema.MCPSearchQuestionInfoResp, 0) for _, question := range searchResp.SearchResults { t := &schema.MCPSearchQuestionInfoResp{ QuestionID: question.Object.QuestionID, Title: question.Object.Title, Content: question.Object.Excerpt, Link: fmt.Sprintf("%s/questions/%s", siteGeneral.SiteUrl, question.Object.QuestionID), } resp = append(resp, t) } data, _ := json.Marshal(resp) return mcp.NewToolResultText(string(data)), nil } } func (c *MCPController) MCPQuestionDetailHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { if err := c.ensureMCPEnabled(ctx); err != nil { return nil, err } cond := schema.NewMCPSearchQuestionDetail(request) siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general info failed: %v", err) return nil, err } question, err := c.questioncommon.Info(ctx, cond.QuestionID, "") if err != nil { log.Errorf("get question failed: %v", err) return mcp.NewToolResultText("No question found."), nil } resp := &schema.MCPSearchQuestionInfoResp{ QuestionID: question.ID, Title: question.Title, Content: question.Content, Link: fmt.Sprintf("%s/questions/%s", siteGeneral.SiteUrl, question.ID), } res, _ := json.Marshal(resp) return mcp.NewToolResultText(string(res)), nil } } func (c *MCPController) MCPAnswersHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { if err := c.ensureMCPEnabled(ctx); err != nil { return nil, err } cond := schema.NewMCPSearchAnswerCond(request) siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general info failed: %v", err) return nil, err } if len(cond.QuestionID) > 0 { answerList, err := c.answerRepo.GetAnswerList(ctx, &entity.Answer{QuestionID: cond.QuestionID}) if err != nil { log.Errorf("get answers failed: %v", err) return nil, err } resp := make([]*schema.MCPSearchAnswerInfoResp, 0) for _, answer := range answerList { t := &schema.MCPSearchAnswerInfoResp{ QuestionID: answer.QuestionID, AnswerID: answer.ID, AnswerContent: answer.OriginalText, Link: fmt.Sprintf("%s/questions/%s/answers/%s", siteGeneral.SiteUrl, answer.QuestionID, answer.ID), } resp = append(resp, t) } data, _ := json.Marshal(resp) return mcp.NewToolResultText(string(data)), nil } answerList, err := c.answerRepo.GetAnswerList(ctx, &entity.Answer{QuestionID: cond.QuestionID}) if err != nil { log.Errorf("get answers failed: %v", err) return nil, err } resp := make([]*schema.MCPSearchAnswerInfoResp, 0) for _, answer := range answerList { t := &schema.MCPSearchAnswerInfoResp{ QuestionID: answer.QuestionID, AnswerID: answer.ID, AnswerContent: answer.OriginalText, Link: fmt.Sprintf("%s/questions/%s/answers/%s", siteGeneral.SiteUrl, answer.QuestionID, answer.ID), } resp = append(resp, t) } data, _ := json.Marshal(resp) return mcp.NewToolResultText(string(data)), nil } } func (c *MCPController) MCPCommentsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { if err := c.ensureMCPEnabled(ctx); err != nil { return nil, err } cond := schema.NewMCPSearchCommentCond(request) siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general info failed: %v", err) return nil, err } dto := &comment.CommentQuery{ PageCond: pager.PageCond{Page: 1, PageSize: 5}, QueryCond: "newest", ObjectID: cond.ObjectID, } commentList, total, err := c.commentRepo.GetCommentPage(ctx, dto) if err != nil { return nil, err } if total == 0 { return mcp.NewToolResultText("No comments found."), nil } resp := make([]*schema.MCPSearchCommentInfoResp, 0) for _, comment := range commentList { t := &schema.MCPSearchCommentInfoResp{ CommentID: comment.ID, Content: comment.OriginalText, ObjectID: comment.ObjectID, Link: fmt.Sprintf("%s/comments/%s", siteGeneral.SiteUrl, comment.ID), } resp = append(resp, t) } data, _ := json.Marshal(resp) return mcp.NewToolResultText(string(data)), nil } } func (c *MCPController) MCPTagsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { if err := c.ensureMCPEnabled(ctx); err != nil { return nil, err } cond := schema.NewMCPSearchTagCond(request) siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general info failed: %v", err) return nil, err } tags, total, err := c.tagCommonService.GetTagPage(ctx, 1, 10, &entity.Tag{DisplayName: cond.TagName}, "newest") if err != nil { log.Errorf("get tags failed: %v", err) return nil, err } if total == 0 { res := strings.Builder{} res.WriteString("No tags found.\n") return mcp.NewToolResultText(res.String()), nil } resp := make([]*schema.MCPSearchTagResp, 0) for _, tag := range tags { t := &schema.MCPSearchTagResp{ TagName: tag.SlugName, DisplayName: tag.DisplayName, Description: tag.OriginalText, Link: fmt.Sprintf("%s/tags/%s", siteGeneral.SiteUrl, tag.SlugName), } resp = append(resp, t) } data, _ := json.Marshal(resp) return mcp.NewToolResultText(string(data)), nil } } func (c *MCPController) MCPTagDetailsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { if err := c.ensureMCPEnabled(ctx); err != nil { return nil, err } cond := schema.NewMCPSearchTagCond(request) siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general info failed: %v", err) return nil, err } tag, exist, err := c.tagCommonService.GetTagBySlugName(ctx, cond.TagName) if err != nil { log.Errorf("get tag failed: %v", err) return nil, err } if !exist { return mcp.NewToolResultText("Tag not found."), nil } resp := &schema.MCPSearchTagResp{ TagName: tag.SlugName, DisplayName: tag.DisplayName, Description: tag.OriginalText, Link: fmt.Sprintf("%s/tags/%s", siteGeneral.SiteUrl, tag.SlugName), } res, _ := json.Marshal(resp) return mcp.NewToolResultText(string(res)), nil } } func (c *MCPController) MCPUserDetailsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { if err := c.ensureMCPEnabled(ctx); err != nil { return nil, err } cond := schema.NewMCPSearchUserCond(request) siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general info failed: %v", err) return nil, err } user, exist, err := c.userCommon.GetUserBasicInfoByUserName(ctx, cond.Username) if err != nil { log.Errorf("get user failed: %v", err) return nil, err } if !exist { return mcp.NewToolResultText("User not found."), nil } resp := &schema.MCPSearchUserInfoResp{ Username: user.Username, DisplayName: user.DisplayName, Avatar: user.Avatar, Link: fmt.Sprintf("%s/users/%s", siteGeneral.SiteUrl, user.Username), } res, _ := json.Marshal(resp) return mcp.NewToolResultText(string(res)), nil } } ================================================ FILE: internal/controller/meta_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/meta" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" ) type MetaController struct { metaService *meta.MetaService } func NewMetaController( metaService *meta.MetaService, ) *MetaController { return &MetaController{ metaService: metaService, } } // AddOrUpdateReaction add or update reaction // @Summary add or update reaction // @Description update reaction. if not exist, add one // @Tags Meta // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UpdateReactionReq true "reaction" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/meta/reaction [put] func (mc *MetaController) AddOrUpdateReaction(ctx *gin.Context) { req := &schema.UpdateReactionReq{} if handler.BindAndCheck(ctx, req) { return } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := mc.metaService.AddOrUpdateReaction(ctx, req) handler.HandleResponse(ctx, err, resp) } // GetReaction get reaction // @Summary get reaction // @Description get reaction for an object // @Tags Meta // @Accept json // @Produce json // @Param object_id query string true "object_id" // @Success 200 {object} handler.RespBody{data=schema.ReactionRespItem} // @Router /answer/api/v1/meta/reaction [get] func (mc *MetaController) GetReaction(ctx *gin.Context) { req := &schema.GetReactionReq{} if handler.BindAndCheck(ctx, req) { return } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := mc.metaService.GetReactionByObjectId(ctx, req) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller/notification_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/notification" "github.com/apache/answer/internal/service/permission" "github.com/apache/answer/internal/service/rank" "github.com/gin-gonic/gin" ) // NotificationController notification controller type NotificationController struct { notificationService *notification.NotificationService rankService *rank.RankService } // NewNotificationController new controller func NewNotificationController( notificationService *notification.NotificationService, rankService *rank.RankService, ) *NotificationController { return &NotificationController{ notificationService: notificationService, rankService: rankService, } } // GetRedDot // @Summary GetRedDot // @Description GetRedDot // @Tags Notification // @Accept json // @Produce json // @Security ApiKeyAuth // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/notification/status [get] func (nc *NotificationController) GetRedDot(ctx *gin.Context) { req := &schema.GetRedDot{} req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.QuestionAudit, permission.AnswerAudit, permission.TagAudit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanReviewQuestion = canList[0] req.CanReviewAnswer = canList[1] req.CanReviewTag = canList[2] req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) resp, err := nc.notificationService.GetRedDot(ctx, req) handler.HandleResponse(ctx, err, resp) } // ClearRedDot // @Summary DelRedDot // @Description DelRedDot // @Tags Notification // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.NotificationClearRequest true "NotificationClearRequest" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/notification/status [put] func (nc *NotificationController) ClearRedDot(ctx *gin.Context) { req := &schema.NotificationClearRequest{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.QuestionAudit, permission.AnswerAudit, permission.TagAudit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanReviewQuestion = canList[0] req.CanReviewAnswer = canList[1] req.CanReviewTag = canList[2] resp, err := nc.notificationService.ClearRedDot(ctx, req) handler.HandleResponse(ctx, err, resp) } // ClearUnRead // @Summary ClearUnRead // @Description ClearUnRead // @Tags Notification // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.NotificationClearRequest true "NotificationClearRequest" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/notification/read/state/all [put] func (nc *NotificationController) ClearUnRead(ctx *gin.Context) { req := &schema.NotificationClearRequest{} if handler.BindAndCheck(ctx, req) { return } userID := middleware.GetLoginUserIDFromContext(ctx) err := nc.notificationService.ClearUnRead(ctx, userID, req.NotificationType) handler.HandleResponse(ctx, err, gin.H{}) } // ClearIDUnRead // @Summary ClearUnRead // @Description ClearUnRead // @Tags Notification // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.NotificationClearIDRequest true "NotificationClearIDRequest" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/notification/read/state [put] func (nc *NotificationController) ClearIDUnRead(ctx *gin.Context) { req := &schema.NotificationClearIDRequest{} if handler.BindAndCheck(ctx, req) { return } userID := middleware.GetLoginUserIDFromContext(ctx) err := nc.notificationService.ClearIDUnRead(ctx, userID, req.ID) handler.HandleResponse(ctx, err, gin.H{}) } // GetList get notification list // @Summary get notification list // @Description get notification list // @Tags Notification // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page size" // @Param page_size query int false "page size" // @Param type query string true "type" Enums(inbox,achievement) // @Param inbox_type query string true "inbox_type" Enums(all,posts,invites,votes) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/notification/page [get] func (nc *NotificationController) GetList(ctx *gin.Context) { req := &schema.NotificationSearch{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := nc.notificationService.GetNotificationPage(ctx, req) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller/permission_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/rank" "github.com/gin-gonic/gin" ) type PermissionController struct { rankService *rank.RankService } // NewPermissionController new language controller. func NewPermissionController(rankService *rank.RankService) *PermissionController { return &PermissionController{rankService: rankService} } // GetPermission check user permission // @Summary check user permission // @Description check user permission // @Tags Permission // @Security ApiKeyAuth // @Param Authorization header string true "access-token" // @Produce json // @Param action query string true "permission key" Enums(question.add, question.edit, question.edit_without_review, question.delete, question.close, question.reopen, question.vote_up, question.vote_down, question.pin, question.unpin, question.hide, question.show, answer.add, answer.edit, answer.edit_without_review, answer.delete, answer.accept, answer.vote_up, answer.vote_down, answer.invite_someone_to_answer, comment.add, comment.edit, comment.delete, comment.vote_up, comment.vote_down, report.add, tag.add, tag.edit, tag.edit_slug_name, tag.edit_without_review, tag.delete, tag.synonym, link.url_limit, vote.detail, answer.audit, question.audit, tag.audit, tag.use_reserved_tag) // @Success 200 {object} handler.RespBody{data=map[string]bool} // @Router /answer/api/v1/permission [get] func (u *PermissionController) GetPermission(ctx *gin.Context) { req := &schema.GetPermissionReq{} if handler.BindAndCheck(ctx, req) { return } userID := middleware.GetLoginUserIDFromContext(ctx) ops, requireRanks, err := u.rankService.CheckOperationPermissionsForRanks(ctx, userID, req.Actions) if err != nil { handler.HandleResponse(ctx, err, nil) return } lang := handler.GetLangByCtx(ctx) mapping := make(map[string]*schema.GetPermissionResp, len(ops)) for i, action := range req.Actions { t := &schema.GetPermissionResp{HasPermission: ops[i]} t.TrTip(lang, requireRanks[i]) mapping[action] = t } handler.HandleResponse(ctx, err, mapping) } ================================================ FILE: internal/controller/plugin_captcha_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "encoding/json" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" ) // CaptchaController comment controller type CaptchaController struct { } // NewCaptchaController new controller func NewCaptchaController() *CaptchaController { return &CaptchaController{} } type GetCaptchaConfigResp struct { SlugName string `json:"slug_name"` Config map[string]any `json:"config"` } // GetCaptchaConfig get captcha config func (uc *CaptchaController) GetCaptchaConfig(ctx *gin.Context) { resp := &GetCaptchaConfigResp{} _ = plugin.CallCaptcha(func(fn plugin.Captcha) error { resp.SlugName = fn.Info().SlugName _ = json.Unmarshal([]byte(fn.GetConfig()), &resp.Config) return nil }) handler.HandleResponse(ctx, nil, resp) } ================================================ FILE: internal/controller/plugin_sidebar_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" ) // SidebarController is the controller for the sidebar plugin. type SidebarController struct{} // NewSidebarController creates a new instance of SidebarController. func NewSidebarController() *SidebarController { return &SidebarController{} } // GetSidebarConfig retrieves the sidebar configuration from the registered sidebar plugins. func (uc *SidebarController) GetSidebarConfig(ctx *gin.Context) { resp := &plugin.SidebarConfig{} _ = plugin.CallSidebar(func(fn plugin.Sidebar) error { cfg, err := fn.GetSidebarConfig() if err != nil { return err } resp = cfg return nil }) handler.HandleResponse(ctx, nil, resp) } ================================================ FILE: internal/controller/plugin_user_center_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "fmt" "net/http" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/internal/service/user_external_login" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) const ( UserCenterLoginRouter = "/user-center/login/redirect" UserCenterSignUpRedirectRouter = "/user-center/sign-up/redirect" ) // UserCenterController comment controller type UserCenterController struct { userCenterLoginService *user_external_login.UserCenterLoginService siteInfoService siteinfo_common.SiteInfoCommonService } // NewUserCenterController new controller func NewUserCenterController( userCenterLoginService *user_external_login.UserCenterLoginService, siteInfoService siteinfo_common.SiteInfoCommonService, ) *UserCenterController { return &UserCenterController{ userCenterLoginService: userCenterLoginService, siteInfoService: siteInfoService, } } // UserCenterAgent get user center agent info func (uc *UserCenterController) UserCenterAgent(ctx *gin.Context) { resp := &schema.UserCenterAgentResp{} resp.Enabled = plugin.UserCenterEnabled() if !resp.Enabled { handler.HandleResponse(ctx, nil, resp) return } siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site info failed: %v", err) ctx.Redirect(http.StatusFound, "/50x") return } resp.AgentInfo = &schema.AgentInfo{} resp.AgentInfo.LoginRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl, commonRouterPrefix, UserCenterLoginRouter) resp.AgentInfo.SignUpRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl, commonRouterPrefix, UserCenterSignUpRedirectRouter) _ = plugin.CallUserCenter(func(uc plugin.UserCenter) error { info := uc.Description() resp.AgentInfo.Name = info.Name resp.AgentInfo.DisplayName = info.DisplayName.Translate(ctx) resp.AgentInfo.Icon = info.Icon resp.AgentInfo.Url = info.Url resp.AgentInfo.ControlCenterItems = make([]*schema.ControlCenter, 0) resp.AgentInfo.EnabledOriginalUserSystem = info.EnabledOriginalUserSystem items := uc.ControlCenterItems() for _, item := range items { resp.AgentInfo.ControlCenterItems = append(resp.AgentInfo.ControlCenterItems, &schema.ControlCenter{ Name: item.Name, Label: item.Label, Url: item.Url, }) } return nil }) handler.HandleResponse(ctx, nil, resp) } // UserCenterPersonalBranding get user center personal user info func (uc *UserCenterController) UserCenterPersonalBranding(ctx *gin.Context) { req := &schema.GetOtherUserInfoByUsernameReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := uc.userCenterLoginService.UserCenterPersonalBranding(ctx, req.Username) handler.HandleResponse(ctx, err, resp) } func (uc *UserCenterController) UserCenterLoginRedirect(ctx *gin.Context) { var redirectURL string _ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error { info := userCenter.Description() redirectURL = info.LoginRedirectURL return nil }) ctx.Redirect(http.StatusFound, redirectURL) } func (uc *UserCenterController) UserCenterSignUpRedirect(ctx *gin.Context) { var redirectURL string _ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error { info := userCenter.Description() redirectURL = info.LoginRedirectURL return nil }) ctx.Redirect(http.StatusFound, redirectURL) } func (uc *UserCenterController) UserCenterLoginCallback(ctx *gin.Context) { siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site info failed: %v", err) ctx.Redirect(http.StatusFound, "/50x") return } userCenter, ok := plugin.GetUserCenter() if !ok { ctx.Redirect(http.StatusFound, "/404") return } userInfo, err := userCenter.LoginCallback(ctx) if err != nil { log.Error(err) if !ctx.IsAborted() { ctx.Redirect(http.StatusFound, "/50x") } return } resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo) if err != nil { log.Errorf("external login failed: %v", err) ctx.Redirect(http.StatusFound, "/50x") return } if len(resp.ErrMsg) > 0 { ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg)) return } userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken) ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s", siteGeneral.SiteUrl, resp.AccessToken)) } func (uc *UserCenterController) UserCenterSignUpCallback(ctx *gin.Context) { siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site info failed: %v", err) ctx.Redirect(http.StatusFound, "/50x") return } userCenter, ok := plugin.GetUserCenter() if !ok { ctx.Redirect(http.StatusFound, "/404") return } userInfo, err := userCenter.SignUpCallback(ctx) if err != nil { log.Error(err) ctx.Redirect(http.StatusFound, "/50x") return } resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo) if err != nil { log.Errorf("external login failed: %v", err) ctx.Redirect(http.StatusFound, "/50x") return } if len(resp.ErrMsg) > 0 { ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg)) return } userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken) ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s", siteGeneral.SiteUrl, resp.AccessToken)) } // UserCenterUserSettings user center user settings func (uc *UserCenterController) UserCenterUserSettings(ctx *gin.Context) { userID := middleware.GetLoginUserIDFromContext(ctx) resp, err := uc.userCenterLoginService.UserCenterUserSettings(ctx, userID) handler.HandleResponse(ctx, err, resp) } // UserCenterAdminFunctionAgent user center admin function agent func (uc *UserCenterController) UserCenterAdminFunctionAgent(ctx *gin.Context) { resp, err := uc.userCenterLoginService.UserCenterAdminFunctionAgent(ctx) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller/question_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "net/http" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/action" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/permission" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" ) // QuestionController question controller type QuestionController struct { questionService *content.QuestionService answerService *content.AnswerService rankService *rank.RankService siteInfoService siteinfo_common.SiteInfoCommonService actionService *action.CaptchaService rateLimitMiddleware *middleware.RateLimitMiddleware } // NewQuestionController new controller func NewQuestionController( questionService *content.QuestionService, answerService *content.AnswerService, rankService *rank.RankService, siteInfoService siteinfo_common.SiteInfoCommonService, actionService *action.CaptchaService, rateLimitMiddleware *middleware.RateLimitMiddleware, ) *QuestionController { return &QuestionController{ questionService: questionService, answerService: answerService, rankService: rankService, siteInfoService: siteInfoService, actionService: actionService, rateLimitMiddleware: rateLimitMiddleware, } } // RemoveQuestion delete question // @Summary delete question // @Description delete question // @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.RemoveQuestionReq true "question" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question [delete] func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) { req := &schema.RemoveQuestionReq{} if handler.BindAndCheck(ctx, req) { return } req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetIsAdminFromContext(ctx) isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionDelete, req.ID) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = qc.questionService.RemoveQuestion(ctx, req) if !isAdmin { qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID) } handler.HandleResponse(ctx, err, nil) } // OperationQuestion Operation question // @Summary Operation question // @Description Operation question \n operation [pin unpin hide show] // @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.OperationQuestionReq true "question" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question/operation [put] func (qc *QuestionController) OperationQuestion(ctx *gin.Context) { req := &schema.OperationQuestionReq{} if handler.BindAndCheck(ctx, req) { return } req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.QuestionPin, permission.QuestionUnPin, permission.QuestionHide, permission.QuestionShow, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanPin = canList[0] req.CanList = canList[1] if (req.Operation == schema.QuestionOperationPin || req.Operation == schema.QuestionOperationUnPin) && !req.CanPin { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } if (req.Operation == schema.QuestionOperationHide || req.Operation == schema.QuestionOperationShow) && !req.CanList { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = qc.questionService.OperationQuestion(ctx, req) handler.HandleResponse(ctx, err, nil) } // CloseQuestion Close question // @Summary Close question // @Description Close question // @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.CloseQuestionReq true "question" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question/status [put] func (qc *QuestionController) CloseQuestion(ctx *gin.Context) { req := &schema.CloseQuestionReq{} if handler.BindAndCheck(ctx, req) { return } req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionClose, "") if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = qc.questionService.CloseQuestion(ctx, req) handler.HandleResponse(ctx, err, nil) } // ReopenQuestion reopen question // @Summary reopen question // @Description reopen question // @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.ReopenQuestionReq true "question" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question/reopen [put] func (qc *QuestionController) ReopenQuestion(ctx *gin.Context) { req := &schema.ReopenQuestionReq{} if handler.BindAndCheck(ctx, req) { return } req.QuestionID = uid.DeShortID(req.QuestionID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionReopen, "") if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = qc.questionService.ReopenQuestion(ctx, req) handler.HandleResponse(ctx, err, nil) } // GetQuestion get question details // @Summary get question details // @Description get question details // @Tags Question // @Accept json // @Produce json // @Param id query string true "Question TagID" default(1) // @Success 200 {string} string "" // @Router /answer/api/v1/question/info [get] func (qc *QuestionController) GetQuestion(ctx *gin.Context) { id := ctx.Query("id") id = uid.DeShortID(id) userID := middleware.GetLoginUserIDFromContext(ctx) req := schema.QuestionPermission{} canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{ permission.QuestionEdit, permission.QuestionDelete, permission.QuestionClose, permission.QuestionReopen, permission.QuestionPin, permission.QuestionUnPin, permission.QuestionHide, permission.QuestionShow, permission.AnswerInviteSomeoneToAnswer, permission.QuestionUnDelete, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, userID, id) req.CanEdit = canList[0] || objectOwner req.CanDelete = canList[1] req.CanClose = canList[2] req.CanReopen = canList[3] req.CanPin = canList[4] req.CanUnPin = canList[5] req.CanHide = canList[6] req.CanShow = canList[7] req.CanInviteOtherToAnswer = canList[8] req.CanRecover = canList[9] info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } if handler.GetEnableShortID(ctx) { info.ID = uid.EnShortID(info.ID) } handler.HandleResponse(ctx, nil, info) } // GetQuestionInviteUserInfo get question invite user info // @Summary get question invite user info // @Description get question invite user info // @Tags Question // @Accept json // @Produce json // @Param id query string true "Question ID" default(1) // @Success 200 {string} string "" // @Router /answer/api/v1/question/invite [get] func (qc *QuestionController) GetQuestionInviteUserInfo(ctx *gin.Context) { questionID := uid.DeShortID(ctx.Query("id")) resp, err := qc.questionService.InviteUserInfo(ctx, questionID) handler.HandleResponse(ctx, err, resp) } // SimilarQuestion godoc // @Summary Search Similar Question // @Description Search Similar Question // @Tags Question // @Accept json // @Produce json // @Param question_id query string true "question_id" default() // @Success 200 {string} string "" // @Router /answer/api/v1/question/similar/tag [get] func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) { questionID := ctx.Query("question_id") questionID = uid.DeShortID(questionID) userID := middleware.GetLoginUserIDFromContext(ctx) list, count, err := qc.questionService.SimilarQuestion(ctx, questionID, userID) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, gin.H{ "list": list, "count": count, }) } // QuestionPage get questions by page // @Summary get questions by page // @Description get questions by page // @Tags Question // @Accept json // @Produce json // @Param data body schema.QuestionPageReq true "QuestionPageReq" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}} // @Router /answer/api/v1/question/page [get] func (qc *QuestionController) QuestionPage(ctx *gin.Context) { req := &schema.QuestionPageReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) questions, total, err := qc.questionService.GetQuestionPage(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } if pager.ValPageOutOfRange(total, req.Page, req.PageSize) { handler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil) return } handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) } // QuestionRecommendPage get recommend questions by page // @Summary get recommend questions by page // @Description get recommend questions by page // @Tags Question // @Accept json // @Produce json // @Param data body schema.QuestionPageReq true "QuestionPageReq" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}} // @Router /answer/api/v1/question/recommend/page [get] func (qc *QuestionController) QuestionRecommendPage(ctx *gin.Context) { req := &schema.QuestionPageReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) if req.LoginUserID == "" { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) return } questions, total, err := qc.questionService.GetRecommendQuestionPage(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) } // AddQuestion add question // @Summary add question // @Description add question // @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.QuestionAdd true "question" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question [post] func (qc *QuestionController) AddQuestion(ctx *gin.Context) { req := &schema.QuestionAdd{} errFields := handler.BindAndCheckReturnErr(ctx, req) if ctx.IsAborted() { return } reject, rejectKey := qc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) if reject { return } defer func() { // If status is not 200 means that the bad request has been returned, so the record should be cleared if ctx.Writer.Status() != http.StatusOK { qc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey) } }() req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ permission.QuestionAdd, permission.QuestionEdit, permission.QuestionDelete, permission.QuestionClose, permission.QuestionReopen, permission.TagUseReservedTag, permission.TagAdd, permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } linkUrlLimitUser := canList[7] isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin || !linkUrlLimitUser { captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } req.CanAdd = canList[0] req.CanEdit = canList[1] req.CanDelete = canList[2] req.CanClose = canList[3] req.CanReopen = canList[4] req.CanUseReservedTag = canList[5] req.CanAddTag = canList[6] if !req.CanAdd { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } // can add tag hasNewTag, err := qc.questionService.HasNewTag(ctx, req.Tags) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !req.CanAddTag && hasNewTag { lang := handler.GetLangByCtx(ctx) msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[6]}) handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil) return } errList, err := qc.questionService.CheckAddQuestion(ctx, req) if err != nil { errlist, ok := errList.([]*validator.FormErrorField) if ok { errFields = append(errFields, errlist...) } } if len(errFields) > 0 { handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) return } req.UserAgent = ctx.GetHeader("User-Agent") req.IP = ctx.ClientIP() resp, err := qc.questionService.AddQuestion(ctx, req) if err != nil { errlist, ok := resp.([]*validator.FormErrorField) if ok { errFields = append(errFields, errlist...) } } if len(errFields) > 0 { handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) return } if !isAdmin || !linkUrlLimitUser { qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID) } handler.HandleResponse(ctx, err, resp) } // AddQuestionByAnswer add question // @Summary add question and answer // @Description add question and answer // @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.QuestionAddByAnswer true "question" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question/answer [post] func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) { req := &schema.QuestionAddByAnswer{} errFields := handler.BindAndCheckReturnErr(ctx, req) if ctx.IsAborted() { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.QuestionAdd, permission.QuestionEdit, permission.QuestionDelete, permission.QuestionClose, permission.QuestionReopen, permission.TagUseReservedTag, permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } linkUrlLimitUser := canList[6] isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin || !linkUrlLimitUser { captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } req.CanAdd = canList[0] req.CanEdit = canList[1] req.CanDelete = canList[2] req.CanClose = canList[3] req.CanReopen = canList[4] req.CanUseReservedTag = canList[5] if !req.CanAdd { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } questionReq := new(schema.QuestionAdd) err = copier.Copy(questionReq, req) if err != nil { handler.HandleResponse(ctx, errors.Forbidden(reason.RequestFormatError), nil) return } errList, err := qc.questionService.CheckAddQuestion(ctx, questionReq) if err != nil { errlist, ok := errList.([]*validator.FormErrorField) if ok { errFields = append(errFields, errlist...) } } if len(errFields) > 0 { handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) return } req.UserAgent = ctx.GetHeader("User-Agent") req.IP = ctx.ClientIP() resp, err := qc.questionService.AddQuestion(ctx, questionReq) if err != nil { errlist, ok := resp.([]*validator.FormErrorField) if ok { errFields = append(errFields, errlist...) } } if !isAdmin || !linkUrlLimitUser { qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID) } if len(errFields) > 0 { handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) return } // add the question id to the answer questionInfo, ok := resp.(*schema.QuestionInfoResp) if ok { answerReq := &schema.AnswerAddReq{} answerReq.QuestionID = uid.DeShortID(questionInfo.ID) answerReq.UserID = middleware.GetLoginUserIDFromContext(ctx) answerReq.Content = req.AnswerContent answerReq.HTML = req.AnswerHTML answerID, err := qc.answerService.Insert(ctx, answerReq) if err != nil { handler.HandleResponse(ctx, err, nil) return } info, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !has { handler.HandleResponse(ctx, nil, nil) return } handler.HandleResponse(ctx, err, gin.H{ "info": info, "question": questionInfo, }) return } handler.HandleResponse(ctx, err, resp) } // UpdateQuestion update question // @Summary update question // @Description update question // @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.QuestionUpdate true "question" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question [put] func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) { req := &schema.QuestionUpdate{} errFields := handler.BindAndCheckReturnErr(ctx, req) if ctx.IsAborted() { return } req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ permission.QuestionEdit, permission.QuestionDelete, permission.QuestionEditWithoutReview, permission.TagUseReservedTag, permission.TagAdd, permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } linkUrlLimitUser := canList[5] isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin || !linkUrlLimitUser { captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) req.CanEdit = canList[0] || objectOwner req.CanDelete = canList[1] req.NoNeedReview = canList[2] || objectOwner req.CanUseReservedTag = canList[3] req.CanAddTag = canList[4] if !req.CanEdit { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } errlist, err := qc.questionService.UpdateQuestionCheckTags(ctx, req) if err != nil { errFields = append(errFields, errlist...) } if len(errFields) > 0 { handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) return } // can add tag hasNewTag, err := qc.questionService.HasNewTag(ctx, req.Tags) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !req.CanAddTag && hasNewTag { lang := handler.GetLangByCtx(ctx) msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[4]}) handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil) return } resp, err := qc.questionService.UpdateQuestion(ctx, req) if err != nil { handler.HandleResponse(ctx, err, resp) return } respInfo, ok := resp.(*schema.QuestionInfoResp) if !ok { handler.HandleResponse(ctx, err, resp) return } if !isAdmin || !linkUrlLimitUser { qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID) } handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{UrlTitle: respInfo.UrlTitle, WaitForReview: !req.NoNeedReview}) } // QuestionRecover recover deleted question // @Summary recover deleted question // @Description recover deleted question // @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.QuestionRecoverReq true "question" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question/recover [post] func (qc *QuestionController) QuestionRecover(ctx *gin.Context) { req := &schema.QuestionRecoverReq{} if handler.BindAndCheck(ctx, req) { return } req.QuestionID = uid.DeShortID(req.QuestionID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.QuestionUnDelete, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !canList[0] { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = qc.questionService.RecoverQuestion(ctx, req) handler.HandleResponse(ctx, err, nil) } // UpdateQuestionInviteUser update question invite user // @Summary update question invite user // @Description update question invite user // @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.QuestionUpdateInviteUser true "question" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question/invite [put] func (qc *QuestionController) UpdateQuestionInviteUser(ctx *gin.Context) { req := &schema.QuestionUpdateInviteUser{} errFields := handler.BindAndCheckReturnErr(ctx, req) if ctx.IsAborted() { return } if len(errFields) > 0 { handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) return } req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionInvitationAnswer, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.AnswerInviteSomeoneToAnswer, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanInviteOtherToAnswer = canList[0] if !req.CanInviteOtherToAnswer { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = qc.questionService.UpdateQuestionInviteUser(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !isAdmin { qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionInvitationAnswer, req.UserID) } handler.HandleResponse(ctx, nil, nil) } // GetSimilarQuestions fuzzy query similar questions based on title // @Summary fuzzy query similar questions based on title // @Description fuzzy query similar questions based on title // @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param title query string true "title" default(string) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question/similar [get] func (qc *QuestionController) GetSimilarQuestions(ctx *gin.Context) { title := ctx.Query("title") resp, err := qc.questionService.GetQuestionsByTitle(ctx, title) handler.HandleResponse(ctx, err, resp) } // UserTop godoc // @Summary UserTop // @Description UserTop // @Tags Question // @Accept json // @Produce json // @Param username query string true "username" default(string) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/personal/qa/top [get] func (qc *QuestionController) UserTop(ctx *gin.Context) { userName := ctx.Query("username") userID := middleware.GetLoginUserIDFromContext(ctx) questionList, answerList, err := qc.questionService.SearchUserTopList(ctx, userName, userID) handler.HandleResponse(ctx, err, gin.H{ "question": questionList, "answer": answerList, }) } // PersonalQuestionPage list personal questions // @Summary list personal questions // @Description list personal questions // @Tags Personal // @Accept json // @Produce json // @Security ApiKeyAuth // @Param username query string true "username" default(string) // @Param order query string true "order" Enums(newest,score) // @Param page query string true "page" default(0) // @Param page_size query string true "page_size" default(20) // @Success 200 {object} handler.RespBody // @Router /personal/question/page [get] func (qc *QuestionController) PersonalQuestionPage(ctx *gin.Context) { req := &schema.PersonalQuestionPageReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) resp, err := qc.questionService.PersonalQuestionPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // PersonalAnswerPage list personal answers // @Summary list personal answers // @Description list personal answers // @Tags Personal // @Accept json // @Produce json // @Security ApiKeyAuth // @Param username query string true "username" default(string) // @Param order query string true "order" Enums(newest,score) // @Param page query string true "page" default(0) // @Param page_size query string true "page_size" default(20) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/personal/answer/page [get] func (qc *QuestionController) PersonalAnswerPage(ctx *gin.Context) { req := &schema.PersonalAnswerPageReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) resp, err := qc.questionService.PersonalAnswerPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // PersonalCollectionPage list personal collections // @Summary list personal collections // @Description list personal collections // @Tags Collection // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query string true "page" default(0) // @Param page_size query string true "page_size" default(20) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/personal/collection/page [get] func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) { req := &schema.PersonalCollectionPageReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := qc.questionService.PersonalCollectionPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // AdminQuestionPage admin question page // @Summary AdminQuestionPage admin question page // @Description Status:[available,closed,deleted,pending] // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page size" // @Param page_size query int false "page size" // @Param status query string false "user status" Enums(available, closed, deleted, pending) // @Param query query string false "question id or title" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/question/page [get] func (qc *QuestionController) AdminQuestionPage(ctx *gin.Context) { req := &schema.AdminQuestionPageReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := qc.questionService.AdminQuestionPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // AdminAnswerPage admin answer page // @Summary AdminAnswerPage admin answer page // @Description Status:[available,deleted,pending] // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page size" // @Param page_size query int false "page size" // @Param status query string false "user status" Enums(available,deleted,pending) // @Param query query string false "answer id or question title" // @Param question_id query string false "question id" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/answer/page [get] func (qc *QuestionController) AdminAnswerPage(ctx *gin.Context) { req := &schema.AdminAnswerPageReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := qc.questionService.AdminAnswerPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // AdminUpdateQuestionStatus update question status // @Summary update question status // @Description update question status // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.AdminUpdateQuestionStatusReq true "AdminUpdateQuestionStatusReq" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/question/status [put] func (qc *QuestionController) AdminUpdateQuestionStatus(ctx *gin.Context) { req := &schema.AdminUpdateQuestionStatusReq{} if handler.BindAndCheck(ctx, req) { return } req.QuestionID = uid.DeShortID(req.QuestionID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) err := qc.questionService.AdminSetQuestionStatus(ctx, req) handler.HandleResponse(ctx, err, nil) } // GetQuestionLink get question link // @Summary get question link // @Description get question link // @Tags Question // @Param data query schema.GetQuestionLinkReq true "GetQuestionLinkReq" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}} // @Router /answer/api/v1/question/link [get] func (qc *QuestionController) GetQuestionLink(ctx *gin.Context) { req := &schema.GetQuestionLinkReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) req.QuestionID = uid.DeShortID(req.QuestionID) questions, total, err := qc.questionService.GetQuestionLink(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) } ================================================ FILE: internal/controller/rank_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/rank" "github.com/gin-gonic/gin" ) // RankController rank controller type RankController struct { rankService *rank.RankService } // NewRankController new controller func NewRankController( rankService *rank.RankService) *RankController { return &RankController{rankService: rankService} } // GetRankPersonalWithPage user personal rank list // @Summary user personal rank list // @Description user personal rank list // @Tags Rank // @Produce json // @Param page query int false "page" // @Param page_size query int false "page size" // @Param username query string false "username" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetRankPersonalPageResp}} // @Router /answer/api/v1/personal/rank/page [get] func (cc *RankController) GetRankPersonalWithPage(ctx *gin.Context) { req := &schema.GetRankPersonalWithPageReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := cc.rankService.GetRankPersonalPage(ctx, req) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller/reason_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/reason" "github.com/gin-gonic/gin" ) // ReasonController answer controller type ReasonController struct { reasonService *reason.ReasonService } // NewReasonController new controller func NewReasonController(answerService *reason.ReasonService) *ReasonController { return &ReasonController{reasonService: answerService} } // Reasons godoc // @Summary get reasons by object type and action // @Description get reasons by object type and action // @Tags reason // @Accept json // @Produce json // @Security ApiKeyAuth // @Param object_type query string true "object_type" Enums(question, answer, comment, user) // @Param action query string true "action" Enums(status, close, flag, review) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/reasons [get] // @Router /answer/admin/api/reasons [get] func (rc *ReasonController) Reasons(ctx *gin.Context) { req := &schema.ReasonReq{} if handler.BindAndCheck(ctx, req) { return } reasons, err := rc.reasonService.GetReasons(ctx, *req) handler.HandleResponse(ctx, err, reasons) } ================================================ FILE: internal/controller/render_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" ) type RenderController struct { } func NewRenderController() *RenderController { return &RenderController{} } // GetRenderConfig godoc // @Summary GetRenderConfig // @Description GetRenderConfig // @Tags PluginRender // @Accept json // @Produce json // @Router /answer/api/v1/render/config [get] // @Success 200 {object} handler.RespBody{data=plugin.RenderConfig} func (c *RenderController) GetRenderConfig(ctx *gin.Context) { var resp *plugin.RenderConfig _ = plugin.CallRender(func(render plugin.Render) (err error) { resp = render.GetRenderConfig(ctx) return nil }) handler.HandleResponse(ctx, nil, resp) } ================================================ FILE: internal/controller/report_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/action" "github.com/apache/answer/internal/service/permission" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/internal/service/report" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // ReportController report controller type ReportController struct { reportService *report.ReportService rankService *rank.RankService actionService *action.CaptchaService } // NewReportController new controller func NewReportController( reportService *report.ReportService, rankService *rank.RankService, actionService *action.CaptchaService, ) *ReportController { return &ReportController{ reportService: reportService, rankService: rankService, actionService: actionService, } } // AddReport add report // @Summary add report // @Description add report
source (question, answer, comment, user) // @Tags Report // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.AddReportReq true "report" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/report [post] func (rc *ReportController) AddReport(ctx *gin.Context) { req := &schema.AddReportReq{} if handler.BindAndCheck(ctx, req) { return } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := rc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionReport, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, permission.ReportAdd, "") if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = rc.reportService.AddReport(ctx, req) if !isAdmin { rc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionReport, req.UserID) } handler.HandleResponse(ctx, err, nil) } // GetUnreviewedReportPostPage get unreviewed report post page // @Summary get unreviewed report post page // @Description get unreviewed report post page // @Tags Report // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetReportListPageResp}} // @Router /answer/api/v1/report/unreviewed/post [get] func (rc *ReportController) GetUnreviewedReportPostPage(ctx *gin.Context) { req := &schema.GetUnreviewedReportPostPageReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) resp, err := rc.reportService.GetUnreviewedReportPostPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // ReviewReport review report // @Summary review report // @Description review report // @Tags Report // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.ReviewReportReq true "flag" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/report/review [put] func (rc *ReportController) ReviewReport(ctx *gin.Context) { req := &schema.ReviewReportReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) if !req.IsAdmin { handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) return } err := rc.reportService.ReviewReport(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller/review_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/action" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/internal/service/review" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // ReviewController review controller type ReviewController struct { reviewService *review.ReviewService rankService *rank.RankService actionService *action.CaptchaService } // NewReviewController new controller func NewReviewController( reviewService *review.ReviewService, rankService *rank.RankService, actionService *action.CaptchaService, ) *ReviewController { return &ReviewController{ reviewService: reviewService, rankService: rankService, actionService: actionService, } } // GetUnreviewedPostPage get unreviewed post page // @Summary get unreviewed post page // @Description get unreviewed post page // @Tags Review // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page" // @Param object_id query string false "object_id" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUnreviewedPostPageResp}} // @Router /answer/api/v1/review/pending/post/page [get] func (rc *ReviewController) GetUnreviewedPostPage(ctx *gin.Context) { req := &schema.GetUnreviewedPostPageReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) req.ReviewerMapping = make(map[string]string) _ = plugin.CallReviewer(func(base plugin.Reviewer) error { info := base.Info() req.ReviewerMapping[info.SlugName] = info.Name.Translate(ctx) return nil }) resp, err := rc.reviewService.GetUnreviewedPostPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // UpdateReview update review // @Summary update review // @Description update review // @Tags Review // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UpdateReviewReq true "review" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/review/pending/post [put] func (rc *ReviewController) UpdateReview(ctx *gin.Context) { req := &schema.UpdateReviewReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) if !req.IsAdmin { handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) return } err := rc.reviewService.UpdateReview(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller/revision_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/permission" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/pkg/obj" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // RevisionController revision controller type RevisionController struct { revisionListService *content.RevisionService rankService *rank.RankService } // NewRevisionController new controller func NewRevisionController( revisionListService *content.RevisionService, rankService *rank.RankService, ) *RevisionController { return &RevisionController{ revisionListService: revisionListService, rankService: rankService, } } // GetRevisionList godoc // @Summary get revision list // @Description get revision list // @Tags Revision // @Produce json // @Param object_id query string true "object id" // @Success 200 {object} handler.RespBody{data=[]schema.GetRevisionResp} // @Router /answer/api/v1/revisions [get] func (rc *RevisionController) GetRevisionList(ctx *gin.Context) { objectID := ctx.Query("object_id") if objectID == "0" || objectID == "" { handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) return } objectID = uid.DeShortID(objectID) req := &schema.GetRevisionListReq{ ObjectID: objectID, IsAdmin: middleware.GetUserIsAdminModerator(ctx), UserID: middleware.GetLoginUserIDFromContext(ctx), } resp, err := rc.revisionListService.GetRevisionList(ctx, req) list := make([]schema.GetRevisionResp, 0) for _, item := range resp { if item.Status == entity.RevisionNormalStatus || item.Status == entity.RevisionReviewPassStatus { list = append(list, item) } } handler.HandleResponse(ctx, err, list) } // GetUnreviewedRevisionList godoc // @Summary get unreviewed revision list // @Description get unreviewed revision list // @Tags Revision // @Produce json // @Security ApiKeyAuth // @Param page query string true "page id" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUnreviewedRevisionResp}} // @Router /answer/api/v1/revisions/unreviewed [get] func (rc *RevisionController) GetUnreviewedRevisionList(ctx *gin.Context) { req := &schema.RevisionSearch{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.QuestionAudit, permission.AnswerAudit, permission.TagAudit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanReviewQuestion = canList[0] req.CanReviewAnswer = canList[1] req.CanReviewTag = canList[2] resp, err := rc.revisionListService.GetUnreviewedRevisionPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // RevisionAudit godoc // @Summary revision audit // @Description revision audit operation:approve or reject // @Tags Revision // @Produce json // @Security ApiKeyAuth // @Param data body schema.RevisionAuditReq true "audit" // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/revisions/audit [put] func (rc *RevisionController) RevisionAudit(ctx *gin.Context) { req := &schema.RevisionAuditReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.QuestionAudit, permission.AnswerAudit, permission.TagAudit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanReviewQuestion = canList[0] req.CanReviewAnswer = canList[1] req.CanReviewTag = canList[2] err = rc.revisionListService.RevisionAudit(ctx, req) handler.HandleResponse(ctx, err, gin.H{}) } // CheckCanUpdateRevision check can update revision // @Summary check can update revision // @Description check can update revision // @Tags Revision // @Accept json // @Produce json // @Security ApiKeyAuth // @Param id query string true "id" default(string) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/revisions/edit/check [get] func (rc *RevisionController) CheckCanUpdateRevision(ctx *gin.Context) { req := &schema.CheckCanQuestionUpdate{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) action := "" req.ID = uid.DeShortID(req.ID) objectTypeStr, _ := obj.GetObjectTypeStrByObjectID(req.ID) switch objectTypeStr { case constant.QuestionObjectType: action = permission.QuestionEdit case constant.AnswerObjectType: action = permission.AnswerEdit case constant.TagObjectType: action = permission.TagEdit default: handler.HandleResponse(ctx, errors.BadRequest(reason.ObjectNotFound), nil) return } can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, action, req.ID) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } resp, err := rc.revisionListService.CheckCanUpdateRevision(ctx, req) handler.HandleResponse(ctx, err, resp) } // GetReviewingType get reviewing type // @Summary get reviewing type // @Description get reviewing type // @Tags Revision // @Produce json // @Security ApiKeyAuth // @Success 200 {object} handler.RespBody{data=[]schema.GetReviewingTypeResp} // @Router /answer/api/v1/reviewing/type [get] func (rc *RevisionController) GetReviewingType(ctx *gin.Context) { req := &schema.GetReviewingTypeReq{} req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.QuestionAudit, permission.AnswerAudit, permission.TagAudit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanReviewQuestion = canList[0] req.CanReviewAnswer = canList[1] req.CanReviewTag = canList[2] req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) resp, err := rc.revisionListService.GetReviewingType(ctx, req) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller/search_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/action" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // SearchController tag controller type SearchController struct { searchService *content.SearchService actionService *action.CaptchaService } // NewSearchController new controller func NewSearchController( searchService *content.SearchService, actionService *action.CaptchaService, ) *SearchController { return &SearchController{ searchService: searchService, actionService: actionService, } } // Search godoc // @Summary search object // @Description search object // @Tags Search // @Produce json // @Security ApiKeyAuth // @Param q query string true "query string" // @Param order query string true "order" Enums(newest,active,score,relevance) // @Success 200 {object} handler.RespBody{data=schema.SearchResp} // @Router /answer/api/v1/search [get] func (sc *SearchController) Search(ctx *gin.Context) { dto := schema.SearchDTO{} if handler.BindAndCheck(ctx, &dto) { return } dto.UserID = middleware.GetLoginUserIDFromContext(ctx) unit := ctx.ClientIP() if dto.UserID != "" { unit = dto.UserID } isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := sc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionSearch, unit, dto.CaptchaID, dto.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } if !isAdmin { sc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionSearch, unit) } resp, err := sc.searchService.Search(ctx, &dto) handler.HandleResponse(ctx, err, resp) } // SearchDesc get search description // @Summary get search description // @Description get search description // @Tags Search // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SearchResp} // @Router /answer/api/v1/search/desc [get] func (sc *SearchController) SearchDesc(ctx *gin.Context) { var finder plugin.Search _ = plugin.CallSearch(func(search plugin.Search) error { finder = search return nil }) resp := &schema.SearchDescResp{} if finder != nil { resp.Name = finder.Info().Name.Translate(ctx) resp.Icon = finder.Description().Icon resp.Link = finder.Description().Link } handler.HandleResponse(ctx, nil, resp) } ================================================ FILE: internal/controller/siteinfo_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "net/http" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) type SiteInfoController struct { siteInfoService siteinfo_common.SiteInfoCommonService } // NewSiteInfoController new site info controller. func NewSiteInfoController(siteInfoService siteinfo_common.SiteInfoCommonService) *SiteInfoController { return &SiteInfoController{ siteInfoService: siteInfoService, } } // GetSiteInfo get site info // @Summary get site info // @Description get site info // @Tags site // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteInfoResp} // @Router /answer/api/v1/siteinfo [get] func (sc *SiteInfoController) GetSiteInfo(ctx *gin.Context) { var err error resp := &schema.SiteInfoResp{Version: constant.Version, Revision: constant.Revision} resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Error(err) } resp.Interface, err = sc.siteInfoService.GetSiteInterface(ctx) if err != nil { log.Error(err) } resp.UsersSettings, err = sc.siteInfoService.GetSiteUsersSettings(ctx) if err != nil { log.Error(err) } resp.Branding, err = sc.siteInfoService.GetSiteBranding(ctx) if err != nil { log.Error(err) } resp.Login, err = sc.siteInfoService.GetSiteLogin(ctx) if err != nil { log.Error(err) } resp.Theme, err = sc.siteInfoService.GetSiteTheme(ctx) if err != nil { log.Error(err) } resp.CustomCssHtml, err = sc.siteInfoService.GetSiteCustomCssHTML(ctx) if err != nil { log.Error(err) } resp.SiteSeo, err = sc.siteInfoService.GetSiteSeo(ctx) if err != nil { log.Error(err) } resp.SiteUsers, err = sc.siteInfoService.GetSiteUsers(ctx) if err != nil { log.Error(err) } resp.Questions, err = sc.siteInfoService.GetSiteQuestion(ctx) if err != nil { log.Error(err) } resp.Tags, err = sc.siteInfoService.GetSiteTag(ctx) if err != nil { log.Error(err) } resp.Advanced, err = sc.siteInfoService.GetSiteAdvanced(ctx) if err != nil { log.Error(err) } if legal, err := sc.siteInfoService.GetSiteSecurity(ctx); err == nil { resp.Legal = &schema.SiteLegalSimpleResp{ExternalContentDisplay: legal.ExternalContentDisplay} } if security, err := sc.siteInfoService.GetSiteSecurity(ctx); err == nil { resp.Security = security } if aiConf, err := sc.siteInfoService.GetSiteAI(ctx); err == nil { resp.AIEnabled = aiConf.Enabled } if mcpConf, err := sc.siteInfoService.GetSiteMCP(ctx); err == nil { resp.MCPEnabled = mcpConf.Enabled } handler.HandleResponse(ctx, nil, resp) } // GetSiteLegalInfo get site legal info // @Summary get site legal info // @Description get site legal info // @Tags site // @Param info_type query string true "legal information type" Enums(tos, privacy) // @Produce json // @Success 200 {object} handler.RespBody{data=schema.GetSiteLegalInfoResp} // @Router /answer/api/v1/siteinfo/legal [get] func (sc *SiteInfoController) GetSiteLegalInfo(ctx *gin.Context) { req := &schema.GetSiteLegalInfoReq{} if handler.BindAndCheck(ctx, req) { return } siteLegal, err := sc.siteInfoService.GetSitePolicies(ctx) if err != nil { handler.HandleResponse(ctx, err, nil) return } resp := &schema.GetSiteLegalInfoResp{} if req.IsTOS() { resp.TermsOfServiceOriginalText = siteLegal.TermsOfServiceOriginalText resp.TermsOfServiceParsedText = siteLegal.TermsOfServiceParsedText } else if req.IsPrivacy() { resp.PrivacyPolicyOriginalText = siteLegal.PrivacyPolicyOriginalText resp.PrivacyPolicyParsedText = siteLegal.PrivacyPolicyParsedText } handler.HandleResponse(ctx, nil, resp) } // GetManifestJson get manifest.json func (sc *SiteInfoController) GetManifestJson(ctx *gin.Context) { favicon := "favicon.ico" resp := &schema.GetManifestJsonResp{ ManifestVersion: 3, Version: constant.Version, Revision: constant.Revision, ShortName: "Answer", Name: "answer.apache.org", Icons: schema.CreateManifestJsonIcons(favicon), StartUrl: ".", Display: "standalone", ThemeColor: "#000000", BackgroundColor: "#ffffff", } branding, err := sc.siteInfoService.GetSiteBranding(ctx) if err != nil { log.Error(err) } else if len(branding.Favicon) > 0 { resp.Icons = schema.CreateManifestJsonIcons(branding.Favicon) } siteGeneral, err := sc.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Error(err) } else { resp.Name = siteGeneral.Name resp.ShortName = siteGeneral.Name } ctx.JSON(http.StatusOK, resp) } ================================================ FILE: internal/controller/tag_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/permission" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/internal/service/tag" "github.com/apache/answer/internal/service/tag_common" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // TagController tag controller type TagController struct { tagService *tag.TagService tagCommonService *tag_common.TagCommonService rankService *rank.RankService } // NewTagController new controller func NewTagController( tagService *tag.TagService, tagCommonService *tag_common.TagCommonService, rankService *rank.RankService, ) *TagController { return &TagController{tagService: tagService, tagCommonService: tagCommonService, rankService: rankService} } // SearchTagLike get tag list // @Summary get tag list // @Description get tag list // @Tags Tag // @Produce json // @Security ApiKeyAuth // @Param tag query string false "tag" // @Success 200 {object} handler.RespBody{data=[]schema.GetTagBasicResp} // @Router /answer/api/v1/question/tags [get] func (tc *TagController) SearchTagLike(ctx *gin.Context) { req := &schema.SearchTagLikeReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := tc.tagCommonService.SearchTagLike(ctx, req) handler.HandleResponse(ctx, err, resp) } // GetTagsBySlugName get tags list // @Summary get tags list // @Description get tags list by slug name // @Tags Tag // @Produce json // @Param tags query []string false "string collection" collectionFormat(csv) // @Success 200 {object} handler.RespBody{data=[]schema.GetTagBasicResp} // @Router /answer/api/v1/tags [get] func (tc *TagController) GetTagsBySlugName(ctx *gin.Context) { req := &schema.SearchTagsBySlugName{} if handler.BindAndCheck(ctx, req) { return } resp, err := tc.tagService.GetTagsBySlugName(ctx, req) handler.HandleResponse(ctx, err, resp) } // RemoveTag delete tag // @Summary delete tag // @Description delete tag // @Security ApiKeyAuth // @Tags Tag // @Accept json // @Produce json // @Param data body schema.RemoveTagReq true "tag" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/tag [delete] func (tc *TagController) RemoveTag(ctx *gin.Context) { req := &schema.RemoveTagReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagDelete, "") if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = tc.tagService.RemoveTag(ctx, req) handler.HandleResponse(ctx, err, nil) } // AddTag add tag // @Summary add tag // @Description add tag // @Security ApiKeyAuth // @Tags Tag // @Accept json // @Produce json // @Param data body schema.AddTagReq true "tag" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/tag [post] func (tc *TagController) AddTag(ctx *gin.Context) { req := &schema.AddTagReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.TagAdd, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !canList[0] { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } resp, err := tc.tagCommonService.AddTag(ctx, req) handler.HandleResponse(ctx, err, resp) } // UpdateTag update tag // @Summary update tag // @Description update tag // @Security ApiKeyAuth // @Tags Tag // @Accept json // @Produce json // @Param data body schema.UpdateTagReq true "tag" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/tag [put] func (tc *TagController) UpdateTag(ctx *gin.Context) { req := &schema.UpdateTagReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.TagEdit, permission.TagEditWithoutReview, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !canList[0] { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } req.NoNeedReview = canList[1] err = tc.tagService.UpdateTag(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) } else { handler.HandleResponse(ctx, err, &schema.UpdateTagResp{WaitForReview: !req.NoNeedReview}) } } // RecoverTag recover delete tag // @Summary recover delete tag // @Description recover delete tag // @Security ApiKeyAuth // @Tags Tag // @Accept json // @Produce json // @Param data body schema.RecoverTagReq true "tag" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/tag/recover [post] func (tc *TagController) RecoverTag(ctx *gin.Context) { req := &schema.RecoverTagReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.TagUnDelete, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !canList[0] { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = tc.tagService.RecoverTag(ctx, req) handler.HandleResponse(ctx, err, nil) } // GetTagInfo get tag one // @Summary get tag one // @Description get tag one // @Tags Tag // @Accept json // @Produce json // @Param tag_id query string true "tag id" // @Param tag_name query string true "tag name" // @Success 200 {object} handler.RespBody{data=schema.GetTagResp} // @Router /answer/api/v1/tag [get] func (tc *TagController) GetTagInfo(ctx *gin.Context) { req := &schema.GetTagInfoReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.TagEdit, permission.TagDelete, permission.TagUnDelete, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanEdit = canList[0] req.CanDelete = canList[1] req.CanRecover = canList[2] req.CanMerge = middleware.GetUserIsAdminModerator(ctx) resp, err := tc.tagService.GetTagInfo(ctx, req) handler.HandleResponse(ctx, err, resp) } // GetTagWithPage get tag page // @Summary get tag page // @Description get tag page // @Tags Tag // @Produce json // @Param page query int false "page size" // @Param page_size query int false "page size" // @Param slug_name query string false "slug_name" // @Param query_cond query string false "query condition" Enums(popular, name, newest) // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetTagPageResp}} // @Router /answer/api/v1/tags/page [get] func (tc *TagController) GetTagWithPage(ctx *gin.Context) { req := &schema.GetTagWithPageReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := tc.tagService.GetTagWithPage(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } if pager.ValPageOutOfRange(resp.Count, req.Page, req.PageSize) { handler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil) return } handler.HandleResponse(ctx, err, resp) } // GetFollowingTags get following tag list // @Summary get following tag list // @Description get following tag list // @Security ApiKeyAuth // @Tags Tag // @Produce json // @Success 200 {object} handler.RespBody{data=[]schema.GetFollowingTagsResp} // @Router /answer/api/v1/tags/following [get] func (tc *TagController) GetFollowingTags(ctx *gin.Context) { userID := middleware.GetLoginUserIDFromContext(ctx) resp, err := tc.tagService.GetFollowingTags(ctx, userID) handler.HandleResponse(ctx, err, resp) } // GetTagSynonyms get tag synonyms // @Summary get tag synonyms // @Description get tag synonyms // @Tags Tag // @Produce json // @Param tag_id query int true "tag id" // @Success 200 {object} handler.RespBody{data=schema.GetTagSynonymsResp} // @Router /answer/api/v1/tag/synonyms [get] func (tc *TagController) GetTagSynonyms(ctx *gin.Context) { req := &schema.GetTagSynonymsReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "") if err != nil { handler.HandleResponse(ctx, err, nil) return } req.CanEdit = can resp, err := tc.tagService.GetTagSynonyms(ctx, req) handler.HandleResponse(ctx, err, resp) } // UpdateTagSynonym update tag // @Summary update tag // @Description update tag // @Security ApiKeyAuth // @Tags Tag // @Accept json // @Produce json // @Param data body schema.UpdateTagSynonymReq true "tag" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/tag/synonym [put] func (tc *TagController) UpdateTagSynonym(ctx *gin.Context) { req := &schema.UpdateTagSynonymReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "") if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } err = tc.tagService.UpdateTagSynonym(ctx, req) handler.HandleResponse(ctx, err, nil) } // MergeTag merge tag // @Summary merge tag // @Description merge tag // @Security ApiKeyAuth // @Tags Tag // @Accept json // @Produce json // @Param data body schema.AddTagReq true "tag" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/tag/merge [post] func (tc *TagController) MergeTag(ctx *gin.Context) { req := &schema.MergeTagReq{} if handler.BindAndCheck(ctx, req) { return } isAdminModerator := middleware.GetUserIsAdminModerator(ctx) if !isAdminModerator { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) err := tc.tagService.MergeTag(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller/template_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "encoding/json" "fmt" "html/template" "net/http" "net/url" "regexp" "strings" "time" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/eventqueue" "github.com/apache/answer/plugin" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" templaterender "github.com/apache/answer/internal/controller/template_render" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/obj" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) var SiteUrl = "" type TemplateController struct { scriptPath []string cssPath string templateRenderController *templaterender.TemplateRenderController siteInfoService siteinfo_common.SiteInfoCommonService eventQueueService eventqueue.Service userService *content.UserService questionService *content.QuestionService } // NewTemplateController new controller func NewTemplateController( templateRenderController *templaterender.TemplateRenderController, siteInfoService siteinfo_common.SiteInfoCommonService, eventQueueService eventqueue.Service, userService *content.UserService, questionService *content.QuestionService, ) *TemplateController { script, css := GetStyle() return &TemplateController{ scriptPath: script, cssPath: css, templateRenderController: templateRenderController, siteInfoService: siteInfoService, eventQueueService: eventQueueService, userService: userService, questionService: questionService, } } func GetStyle() (script []string, css string) { file, err := ui.Build.ReadFile("build/index.html") if err != nil { return } scriptRegexp := regexp.MustCompile(``) scriptData := scriptRegexp.FindAllStringSubmatch(string(file), -1) for _, s := range scriptData { if len(s) == 2 { script = append(script, s[1]) } } cssRegexp := regexp.MustCompile(``) cssListData := cssRegexp.FindStringSubmatch(string(file)) if len(cssListData) == 2 { css = cssListData[1] } return } func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInfoResp { var err error resp := &schema.TemplateSiteInfoResp{} resp.General, err = tc.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Error(err) } SiteUrl = resp.General.SiteUrl resp.Interface, err = tc.siteInfoService.GetSiteInterface(ctx) if err != nil { log.Error(err) } resp.Branding, err = tc.siteInfoService.GetSiteBranding(ctx) if err != nil { log.Error(err) } resp.SiteSeo, err = tc.siteInfoService.GetSiteSeo(ctx) if err != nil { log.Error(err) } resp.CustomCssHtml, err = tc.siteInfoService.GetSiteCustomCssHTML(ctx) if err != nil { log.Error(err) } resp.Year = fmt.Sprintf("%d", time.Now().Year()) return resp } // Index question list func (tc *TemplateController) Index(ctx *gin.Context) { req := &schema.QuestionPageReq{ OrderCond: "newest", } if handler.BindAndCheck(ctx, req) { return } var page = req.Page data, count, err := tc.templateRenderController.Index(ctx, req) if err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) { tc.Page404(ctx) return } hotQuestionReq := &schema.QuestionPageReq{ Page: 1, PageSize: 6, OrderCond: "hot", InDays: 7, } hotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq) siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = siteInfo.General.SiteUrl UrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID siteInfo.Title = "" tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{ "data": data, "useTitle": UrlUseTitle, "page": templaterender.Paginator(page, req.PageSize, count), "path": "questions", "hotQuestion": hotQuestion, }) } func (tc *TemplateController) QuestionList(ctx *gin.Context) { req := &schema.QuestionPageReq{ OrderCond: "newest", } if handler.BindAndCheck(ctx, req) { return } var page = req.Page data, count, err := tc.templateRenderController.Index(ctx, req) if err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) { tc.Page404(ctx) return } hotQuestionReq := &schema.QuestionPageReq{ Page: 1, PageSize: 6, OrderCond: "hot", InDays: 7, } hotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq) siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl) if page > 1 { siteInfo.Canonical = fmt.Sprintf("%s/questions?page=%d", siteInfo.General.SiteUrl, page) } UrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID siteInfo.Title = fmt.Sprintf("%s - %s", translator.Tr(handler.GetLangByCtx(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name) tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{ "data": data, "useTitle": UrlUseTitle, "page": templaterender.Paginator(page, req.PageSize, count), "hotQuestion": hotQuestion, }) } func (tc *TemplateController) QuestionInfoRedirect(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) { questionID := ctx.Param("id") title := ctx.Param("title") answerID := uid.DeShortID(title) titleIsAnswerID := false needChangeShortID := false objectType, err := obj.GetObjectTypeStrByObjectID(answerID) if err == nil && objectType == constant.AnswerObjectType { titleIsAnswerID = true } siteSeo, err := tc.siteInfoService.GetSiteSeo(ctx) if err != nil { return false, "" } isShortID := uid.IsShortID(questionID) if siteSeo.IsShortLink() { if !isShortID { questionID = uid.EnShortID(questionID) needChangeShortID = true } if titleIsAnswerID { answerID = uid.EnShortID(answerID) } } else { if isShortID { needChangeShortID = true questionID = uid.DeShortID(questionID) } if titleIsAnswerID { answerID = uid.DeShortID(answerID) } } if _, err := tc.templateRenderController.AnswerDetail(ctx, answerID); err != nil { answerID = "" titleIsAnswerID = false } url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, questionID) if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID { if len(ctx.Request.URL.Query()) > 0 { url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery) } if needChangeShortID { return true, url } // not have title if titleIsAnswerID || len(title) == 0 { return false, "" } return true, url } else { detail, err := tc.templateRenderController.QuestionDetail(ctx, questionID) if err != nil { tc.Page404(ctx) return } url = fmt.Sprintf("%s/%s", url, htmltext.UrlTitle(detail.Title)) if titleIsAnswerID { url = fmt.Sprintf("%s/%s", url, answerID) } if len(ctx.Request.URL.Query()) > 0 { url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery) } // have title if len(title) > 0 && !titleIsAnswerID && correctTitle { if needChangeShortID { return true, url } return false, "" } return true, url } } // QuestionInfo question and answers info func (tc *TemplateController) QuestionInfo(ctx *gin.Context) { id := ctx.Param("id") title := ctx.Param("title") answerid := ctx.Param("answerid") shareUsername := ctx.Query("share") if checker.IsQuestionsIgnorePath(id) { // if id == "ask" { file, err := ui.Build.ReadFile("build/index.html") if err != nil { log.Error(err) tc.Page404(ctx) return } ctx.Header("content-type", "text/html;charset=utf-8") ctx.String(http.StatusOK, string(file)) return } correctTitle := false detail, err := tc.templateRenderController.QuestionDetail(ctx, id) if err != nil { tc.Page404(ctx) return } if len(shareUsername) > 0 { userInfo, err := tc.userService.GetOtherUserInfoByUsername( ctx, &schema.GetOtherUserInfoByUsernameReq{Username: shareUsername}) if err == nil { tc.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserShare, userInfo.ID). QID(id, detail.UserID).AID(answerid, "")) } } encodeTitle := htmltext.UrlTitle(detail.Title) if encodeTitle == title { correctTitle = true } siteInfo := tc.SiteInfo(ctx) jump, jumpurl := tc.QuestionInfoRedirect(ctx, siteInfo, correctTitle) if jump { ctx.Redirect(http.StatusFound, jumpurl) return } // answers answerReq := &schema.AnswerListReq{ QuestionID: id, Order: "", Page: 1, PageSize: 999, UserID: "", } answers, answerCount, err := tc.templateRenderController.AnswerList(ctx, answerReq) if err != nil { tc.Page404(ctx) return } // comments objectIDs := []string{uid.DeShortID(id)} for _, answer := range answers { answerID := uid.DeShortID(answer.ID) objectIDs = append(objectIDs, answerID) } comments, err := tc.templateRenderController.CommentList(ctx, objectIDs) if err != nil { tc.Page404(ctx) return } UrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID // related question userID := middleware.GetLoginUserIDFromContext(ctx) relatedQuestion, _, _ := tc.questionService.SimilarQuestion(ctx, id, userID) siteInfo.Canonical = fmt.Sprintf("%s/questions/%s/%s", siteInfo.General.SiteUrl, id, encodeTitle) if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID { siteInfo.Canonical = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id) } jsonLD := &schema.QAPageJsonLD{} jsonLD.Context = "https://schema.org" jsonLD.Type = "QAPage" jsonLD.MainEntity.Type = "Question" jsonLD.MainEntity.Name = detail.Title jsonLD.MainEntity.Text = detail.HTML jsonLD.MainEntity.AnswerCount = int(answerCount) jsonLD.MainEntity.UpvoteCount = detail.VoteCount jsonLD.MainEntity.DateCreated = time.Unix(detail.CreateTime, 0) jsonLD.MainEntity.Author.Type = "Person" jsonLD.MainEntity.Author.Name = detail.UserInfo.DisplayName jsonLD.MainEntity.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, detail.UserInfo.Username) answerList := make([]*schema.SuggestedAnswerItem, 0) for _, answer := range answers { if answer.Accepted == schema.AnswerAcceptedEnable { acceptedAnswerItem := &schema.AcceptedAnswerItem{} acceptedAnswerItem.Type = "Answer" acceptedAnswerItem.Text = answer.HTML acceptedAnswerItem.DateCreated = time.Unix(answer.CreateTime, 0) acceptedAnswerItem.UpvoteCount = answer.VoteCount acceptedAnswerItem.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID) acceptedAnswerItem.Author.Type = "Person" acceptedAnswerItem.Author.Name = answer.UserInfo.DisplayName acceptedAnswerItem.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, answer.UserInfo.Username) jsonLD.MainEntity.AcceptedAnswer = acceptedAnswerItem } else { item := &schema.SuggestedAnswerItem{} item.Type = "Answer" item.Text = answer.HTML item.DateCreated = time.Unix(answer.CreateTime, 0) item.UpvoteCount = answer.VoteCount item.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID) item.Author.Type = "Person" item.Author.Name = answer.UserInfo.DisplayName item.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, answer.UserInfo.Username) answerList = append(answerList, item) } } jsonLD.MainEntity.SuggestedAnswer = answerList jsonLDStr, err := json.Marshal(jsonLD) if err == nil { siteInfo.JsonLD = `` } siteInfo.Description = htmltext.FetchExcerpt(detail.HTML, "...", 240) tags := make([]string, 0) for _, tag := range detail.Tags { tags = append(tags, tag.DisplayName) } siteInfo.Keywords = strings.ReplaceAll(strings.Trim(fmt.Sprint(tags), "[]"), " ", ",") siteInfo.Title = fmt.Sprintf("%s - %s", detail.Title, siteInfo.General.Name) tc.html(ctx, http.StatusOK, "question-detail.html", siteInfo, gin.H{ "id": id, "answerid": answerid, "detail": detail, "answers": answers, "comments": comments, "noindex": detail.Show == entity.QuestionHide, "useTitle": UrlUseTitle, "relatedQuestion": relatedQuestion, }) } // TagList tags list func (tc *TemplateController) TagList(ctx *gin.Context) { req := &schema.GetTagWithPageReq{ PageSize: constant.DefaultPageSize, Page: 1, } if handler.BindAndCheck(ctx, req) { return } data, err := tc.templateRenderController.TagList(ctx, req) if err != nil || pager.ValPageOutOfRange(data.Count, req.Page, req.PageSize) { tc.Page404(ctx) return } page := templaterender.Paginator(req.Page, req.PageSize, data.Count) siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = fmt.Sprintf("%s/tags", siteInfo.General.SiteUrl) if req.Page > 1 { siteInfo.Canonical = fmt.Sprintf("%s/tags?page=%d", siteInfo.General.SiteUrl, req.Page) } siteInfo.Title = fmt.Sprintf("%s - %s", translator.Tr(handler.GetLangByCtx(ctx), constant.TagsListTitleTrKey), siteInfo.General.Name) tc.html(ctx, http.StatusOK, "tags.html", siteInfo, gin.H{ "page": page, "data": data, }) } // TagInfo taginfo func (tc *TemplateController) TagInfo(ctx *gin.Context) { tag := ctx.Param("tag") req := &schema.GetTamplateTagInfoReq{} if handler.BindAndCheck(ctx, req) { tc.Page404(ctx) return } nowPage := req.Page req.Name = tag tagInfo, questionList, questionCount, err := tc.templateRenderController.TagInfo(ctx, req) if err != nil { tc.Page404(ctx) return } page := templaterender.Paginator(nowPage, req.PageSize, questionCount) siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = fmt.Sprintf("%s/tags/%s", siteInfo.General.SiteUrl, tag) if req.Page > 1 { siteInfo.Canonical = fmt.Sprintf("%s/tags/%s?page=%d", siteInfo.General.SiteUrl, tag, req.Page) } siteInfo.Description = htmltext.FetchExcerpt(tagInfo.ParsedText, "...", 240) if len(tagInfo.ParsedText) == 0 { siteInfo.Description = translator.Tr(handler.GetLangByCtx(ctx), constant.TagHasNoDescription) } siteInfo.Keywords = tagInfo.DisplayName UrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID siteInfo.Title = fmt.Sprintf("'%s' %s - %s", tagInfo.DisplayName, translator.Tr(handler.GetLangByCtx(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name) tc.html(ctx, http.StatusOK, "tag-detail.html", siteInfo, gin.H{ "tag": tagInfo, "questionList": questionList, "questionCount": questionCount, "useTitle": UrlUseTitle, "page": page, }) } // UserInfo user info func (tc *TemplateController) UserInfo(ctx *gin.Context) { username := ctx.Param("username") if username == "" { tc.Page404(ctx) return } exist := checker.IsUsersIgnorePath(username) if exist { file, err := ui.Build.ReadFile("build/index.html") if err != nil { log.Error(err) tc.Page404(ctx) return } ctx.Header("content-type", "text/html;charset=utf-8") ctx.String(http.StatusOK, string(file)) return } req := &schema.GetOtherUserInfoByUsernameReq{} req.Username = username userinfo, err := tc.templateRenderController.UserInfo(ctx, req) if err != nil { tc.Page404(ctx) return } questionList, answerList, err := tc.questionService.SearchUserTopList(ctx, req.Username, "") if err != nil { tc.Page404(ctx) return } siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username) siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name) tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{ "userinfo": userinfo, "bio": template.HTML(userinfo.BioHTML), "topQuestions": questionList, "topAnswers": answerList, }) } func (tc *TemplateController) Page404(ctx *gin.Context) { tc.html(ctx, http.StatusNotFound, "404.html", tc.SiteInfo(ctx), gin.H{}) } func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteInfo *schema.TemplateSiteInfoResp, data gin.H) { prefix := "" cssPath := "" scriptPath := make([]string, len(tc.scriptPath)) _ = plugin.CallCDN(func(fn plugin.CDN) error { prefix = fn.GetStaticPrefix() return nil }) if prefix != "" { if prefix[len(prefix)-1:] == "/" { prefix = strings.TrimSuffix(prefix, "/") } cssPath = prefix + tc.cssPath for i, path := range tc.scriptPath { scriptPath[i] = prefix + path } } else { cssPath = tc.cssPath scriptPath = tc.scriptPath } data["siteinfo"] = siteInfo data["baseURL"] = "" if parsedUrl, err := url.Parse(siteInfo.General.SiteUrl); err == nil { data["baseURL"] = parsedUrl.Path } data["scriptPath"] = scriptPath data["cssPath"] = cssPath data["keywords"] = siteInfo.Keywords if siteInfo.Description == "" { siteInfo.Description = siteInfo.General.Description } data["title"] = siteInfo.Title if siteInfo.Title == "" { data["title"] = siteInfo.General.Name } data["description"] = siteInfo.Description data["language"] = handler.GetLangByCtx(ctx) data["timezone"] = siteInfo.Interface.TimeZone language := strings.ReplaceAll(siteInfo.Interface.Language, "_", "-") data["lang"] = language data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter data["Version"] = constant.Version data["Revision"] = constant.Revision _, ok := data["path"] if !ok { data["path"] = "" } ctx.Header("X-Frame-Options", "DENY") ctx.HTML(code, tpl, data) } func (tc *TemplateController) OpenSearch(ctx *gin.Context) { if tc.checkPrivateMode(ctx) { tc.Page404(ctx) return } tc.templateRenderController.OpenSearch(ctx) } func (tc *TemplateController) Sitemap(ctx *gin.Context) { if tc.checkPrivateMode(ctx) { tc.Page404(ctx) return } tc.templateRenderController.Sitemap(ctx) } func (tc *TemplateController) SitemapPage(ctx *gin.Context) { if tc.checkPrivateMode(ctx) { tc.Page404(ctx) return } page := 0 pageParam := ctx.Param("page") pageRegexp := regexp.MustCompile(`question-(.*).xml`) pageStr := pageRegexp.FindStringSubmatch(pageParam) if len(pageStr) != 2 { tc.Page404(ctx) return } page = converter.StringToInt(pageStr[1]) if page == 0 { tc.Page404(ctx) return } err := tc.templateRenderController.SitemapPage(ctx, page) if err != nil { tc.Page404(ctx) return } } func (tc *TemplateController) checkPrivateMode(ctx *gin.Context) bool { resp, err := tc.siteInfoService.GetSiteSecurity(ctx) if err != nil { log.Error(err) return false } if resp.LoginRequired { return true } return false } ================================================ FILE: internal/controller/template_render/answer.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package templaterender import ( "context" "github.com/apache/answer/internal/schema" ) func (t *TemplateRenderController) AnswerList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) { return t.answerService.SearchList(ctx, req) } func (t *TemplateRenderController) AnswerDetail(ctx context.Context, id string) (*schema.AnswerInfo, error) { return t.answerService.GetDetail(ctx, id) } ================================================ FILE: internal/controller/template_render/comment.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package templaterender import ( "context" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/schema" ) func (t *TemplateRenderController) CommentList( ctx context.Context, objectIDs []string, ) ( comments map[string][]*schema.GetCommentResp, err error, ) { comments = make(map[string][]*schema.GetCommentResp, len(objectIDs)) for _, objectID := range objectIDs { var ( req = &schema.GetCommentWithPageReq{ Page: 1, PageSize: 3, ObjectID: objectID, QueryCond: "vote", UserID: "", } pageModel *pager.PageModel ) pageModel, err = t.commentService.GetCommentWithPage(ctx, req) if err != nil { return } li := pageModel.List comments[objectID] = li.([]*schema.GetCommentResp) } return } ================================================ FILE: internal/controller/template_render/controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package templaterender import ( "math" "github.com/apache/answer/internal/service/content" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/comment" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/google/wire" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/tag" ) // ProviderSetTemplateRenderController is template render controller providers. var ProviderSetTemplateRenderController = wire.NewSet( NewTemplateRenderController, ) type TemplateRenderController struct { questionService *content.QuestionService userService *content.UserService tagService *tag.TagService answerService *content.AnswerService commentService *comment.CommentService siteInfoService siteinfo_common.SiteInfoCommonService questionRepo questioncommon.QuestionRepo } func NewTemplateRenderController( questionService *content.QuestionService, userService *content.UserService, tagService *tag.TagService, answerService *content.AnswerService, commentService *comment.CommentService, siteInfoService siteinfo_common.SiteInfoCommonService, questionRepo questioncommon.QuestionRepo, ) *TemplateRenderController { return &TemplateRenderController{ questionService: questionService, userService: userService, tagService: tagService, answerService: answerService, commentService: commentService, questionRepo: questionRepo, siteInfoService: siteInfoService, } } // Paginator page // page : now page // pageSize : Number per page // nums : Total // Returns the contents of the page in the format of 1, 2, 3, 4, and 5. If the contents are less than 5 pages, the page number is returned func Paginator(page, pageSize int, nums int64) *schema.Paginator { if pageSize == 0 { pageSize = 10 } var prevpage int // Previous page address var nextpage int // Address on the last page // Generate the total number of pages based on the total number of nums and the number of prepage pages totalpages := int(math.Ceil(float64(nums) / float64(pageSize))) // Total number of Pages if page > totalpages { page = totalpages } if page <= 0 { page = 1 } var pages []int switch { case page >= totalpages-5 && totalpages > 5: // The last 5 pages start := totalpages - 5 + 1 prevpage = page - 1 nextpage = int(math.Min(float64(totalpages), float64(page+1))) pages = make([]int, 5) for i := range pages { pages[i] = start + i } case page >= 3 && totalpages > 5: start := page - 3 + 1 pages = make([]int, 5) for i := range pages { pages[i] = start + i } prevpage = page - 1 nextpage = page + 1 default: pages = make([]int, int(math.Min(5, float64(totalpages)))) for i := range pages { pages[i] = i + 1 } prevpage = int(math.Max(float64(1), float64(page-1))) nextpage = page + 1 } paginator := &schema.Paginator{} paginator.Pages = pages paginator.Totalpages = totalpages paginator.Prevpage = prevpage paginator.Nextpage = nextpage paginator.Currpage = page return paginator } ================================================ FILE: internal/controller/template_render/question.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package templaterender import ( "html/template" "math" "net/http" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/schema" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionPageReq) ([]*schema.QuestionPageResp, int64, error) { return t.questionService.GetQuestionPage(ctx, req) } func (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfoResp, err error) { return t.questionService.GetQuestion(ctx, id, "", schema.QuestionPermission{}) } func (t *TemplateRenderController) Sitemap(ctx *gin.Context) { general, err := t.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Error("get site general failed:", err) return } siteInfo, err := t.siteInfoService.GetSiteSeo(ctx) if err != nil { log.Error("get site GetSiteSeo failed:", err) return } questions, err := t.questionRepo.SitemapQuestions(ctx, 1, constant.SitemapMaxSize) if err != nil { log.Errorf("get sitemap questions failed: %s", err) return } ctx.Header("Content-Type", "application/xml") if len(questions) < constant.SitemapMaxSize { ctx.HTML( http.StatusOK, "sitemap.xml", gin.H{ "xmlHeader": template.HTML(``), "list": questions, "general": general, "hastitle": siteInfo.Permalink == constant.PermalinkQuestionIDAndTitle || siteInfo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID, }, ) return } questionNum, err := t.questionRepo.GetQuestionCount(ctx) if err != nil { log.Error("GetQuestionCount error", err) return } var pageList []int totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize))) for i := 1; i <= totalPages; i++ { pageList = append(pageList, i) } ctx.HTML( http.StatusOK, "sitemap-list.xml", gin.H{ "xmlHeader": template.HTML(``), "page": pageList, "general": general, }, ) } func (t *TemplateRenderController) OpenSearch(ctx *gin.Context) { general, err := t.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Error("get site general failed:", err) return } favicon := general.SiteUrl + "/favicon.ico" branding, err := t.siteInfoService.GetSiteBranding(ctx) if err == nil && len(branding.Favicon) > 0 { favicon = branding.Favicon } ctx.Header("Content-Type", "application/xml") ctx.HTML( http.StatusOK, "opensearch.xml", gin.H{ "general": general, "favicon": favicon, }, ) } func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error { general, err := t.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Error("get site general failed:", err) return err } siteInfo, err := t.siteInfoService.GetSiteSeo(ctx) if err != nil { log.Error("get site GetSiteSeo failed:", err) return err } questions, err := t.questionRepo.SitemapQuestions(ctx, page, constant.SitemapMaxSize) if err != nil { log.Errorf("get sitemap questions failed: %s", err) return err } ctx.Header("Content-Type", "application/xml") ctx.HTML( http.StatusOK, "sitemap.xml", gin.H{ "xmlHeader": template.HTML(``), "list": questions, "general": general, "hastitle": siteInfo.Permalink == constant.PermalinkQuestionIDAndTitle || siteInfo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID, }, ) return nil } ================================================ FILE: internal/controller/template_render/tags.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package templaterender import ( "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/schema" "github.com/jinzhu/copier" "golang.org/x/net/context" ) func (q *TemplateRenderController) TagList(ctx context.Context, req *schema.GetTagWithPageReq) (resp *pager.PageModel, err error) { resp, err = q.tagService.GetTagWithPage(ctx, req) if err != nil { return } return } func (q *TemplateRenderController) TagInfo(ctx context.Context, req *schema.GetTamplateTagInfoReq) (resp *schema.GetTagResp, questionList []*schema.QuestionPageResp, questionCount int64, err error) { dto := &schema.GetTagInfoReq{} _ = copier.Copy(dto, req) resp, err = q.tagService.GetTagInfo(ctx, dto) if err != nil { return } searchQuestion := &schema.QuestionPageReq{} searchQuestion.Page = req.Page searchQuestion.PageSize = req.PageSize searchQuestion.OrderCond = "newest" searchQuestion.Tag = req.Name searchQuestion.LoginUserID = req.UserID questionList, questionCount, err = q.questionService.GetQuestionPage(ctx, searchQuestion) if err != nil { return } return resp, questionList, questionCount, err } ================================================ FILE: internal/controller/template_render/userinfo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package templaterender import ( "github.com/apache/answer/internal/schema" "golang.org/x/net/context" ) func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoByUsernameResp, err error) { return q.userService.GetOtherUserInfoByUsername(ctx, req) } ================================================ FILE: internal/controller/upload_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/uploader" "github.com/apache/answer/pkg/converter" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) const ( // file is uploaded by markdown(or something else) editor fileFromPost = "post" // file is used to upload the post attachment fileFromPostAttachment = "post_attachment" // file is used to change the user's avatar fileFromAvatar = "avatar" // file is logo/icon images fileFromBranding = "branding" ) // UploadController upload controller type UploadController struct { uploaderService uploader.UploaderService } // NewUploadController new controller func NewUploadController(uploaderService uploader.UploaderService) *UploadController { return &UploadController{ uploaderService: uploaderService, } } // UploadFile upload file // @Summary upload file // @Description upload file // @Tags Upload // @Accept multipart/form-data // @Security ApiKeyAuth // @Param source formData string true "identify the source of the file upload" Enums(post, post_attachment, avatar, branding) // @Param file formData file true "file" // @Success 200 {object} handler.RespBody{data=string} // @Router /answer/api/v1/file [post] func (uc *UploadController) UploadFile(ctx *gin.Context) { var ( url string err error ) source := ctx.PostForm("source") userID := middleware.GetLoginUserIDFromContext(ctx) switch source { case fileFromAvatar: url, err = uc.uploaderService.UploadAvatarFile(ctx, userID) case fileFromPost: url, err = uc.uploaderService.UploadPostFile(ctx, userID) case fileFromBranding: if !middleware.GetIsAdminFromContext(ctx) { handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) return } url, err = uc.uploaderService.UploadBrandingFile(ctx, userID) case fileFromPostAttachment: url, err = uc.uploaderService.UploadPostAttachment(ctx, userID) default: handler.HandleResponse(ctx, errors.BadRequest(reason.UploadFileSourceUnsupported), nil) return } if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, err, url) } // PostRender render post content // @Summary render post content // @Description render post content // @Tags Upload // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.PostRenderReq true "PostRenderReq" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/post/render [post] func (uc *UploadController) PostRender(ctx *gin.Context) { req := &schema.PostRenderReq{} if handler.BindAndCheck(ctx, req) { return } handler.HandleResponse(ctx, nil, converter.Markdown2HTML(req.Content)) } ================================================ FILE: internal/controller/user_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "net/url" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/action" "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/internal/service/user_notification_config" "github.com/apache/answer/pkg/checker" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // UserController user controller type UserController struct { userService *content.UserService authService *auth.AuthService actionService *action.CaptchaService emailService *export.EmailService siteInfoCommonService siteinfo_common.SiteInfoCommonService userNotificationConfigService *user_notification_config.UserNotificationConfigService } // NewUserController new controller func NewUserController( authService *auth.AuthService, userService *content.UserService, actionService *action.CaptchaService, emailService *export.EmailService, siteInfoCommonService siteinfo_common.SiteInfoCommonService, userNotificationConfigService *user_notification_config.UserNotificationConfigService, ) *UserController { return &UserController{ authService: authService, userService: userService, actionService: actionService, emailService: emailService, siteInfoCommonService: siteInfoCommonService, userNotificationConfigService: userNotificationConfigService, } } // GetUserInfoByUserID get user info, if user no login response http code is 200, but user info is null // @Summary GetUserInfoByUserID // @Description get user info, if user no login response http code is 200, but user info is null // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth // @Success 200 {object} handler.RespBody{data=schema.GetCurrentLoginUserInfoResp} // @Router /answer/api/v1/user/info [get] func (uc *UserController) GetUserInfoByUserID(ctx *gin.Context) { token := middleware.ExtractToken(ctx) if len(token) == 0 { handler.HandleResponse(ctx, nil, nil) return } // if user is no login return null in data userInfo, _ := uc.authService.GetUserCacheInfo(ctx, token) if userInfo == nil { handler.HandleResponse(ctx, nil, nil) return } resp, err := uc.userService.GetUserInfoByUserID(ctx, token, userInfo.UserID) uc.setVisitCookies(ctx, userInfo.VisitToken, false) handler.HandleResponse(ctx, err, resp) } // GetOtherUserInfoByUsername godoc // @Summary GetOtherUserInfoByUsername // @Description GetOtherUserInfoByUsername // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth // @Param username query string true "username" // @Success 200 {object} handler.RespBody{data=schema.GetOtherUserInfoResp} // @Router /answer/api/v1/personal/user/info [get] func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) { req := &schema.GetOtherUserInfoByUsernameReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) resp, err := uc.userService.GetOtherUserInfoByUsername(ctx, req) handler.HandleResponse(ctx, err, resp) } // UserEmailLogin godoc // @Summary UserEmailLogin // @Description UserEmailLogin // @Tags User // @Accept json // @Produce json // @Param data body schema.UserEmailLoginReq true "UserEmailLogin" // @Success 200 {object} handler.RespBody{data=schema.UserLoginResp} // @Router /answer/api/v1/user/login/email [post] func (uc *UserController) UserEmailLogin(ctx *gin.Context) { req := &schema.UserEmailLoginReq{} if handler.BindAndCheck(ctx, req) { return } isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionPassword, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } resp, err := uc.userService.EmailLogin(ctx, req) if err != nil { uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionPassword, ctx.ClientIP()) errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "e_mail", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailOrPasswordWrong), }) handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields) return } if !isAdmin { uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, ctx.ClientIP()) } if resp.Status == constant.UserSuspended { handler.HandleResponse(ctx, errors.Forbidden(reason.UserSuspended), &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUserSuspended}) return } uc.setVisitCookies(ctx, resp.VisitToken, true) handler.HandleResponse(ctx, nil, resp) } // RetrievePassWord godoc // @Summary RetrievePassWord // @Description RetrievePassWord // @Tags User // @Accept json // @Produce json // @Param data body schema.UserRetrievePassWordRequest true "UserRetrievePassWordRequest" // @Success 200 {string} string "" // @Router /answer/api/v1/user/password/reset [post] func (uc *UserController) RetrievePassWord(ctx *gin.Context) { req := &schema.UserRetrievePassWordRequest{} if handler.BindAndCheck(ctx, req) { return } isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } err := uc.userService.RetrievePassWord(ctx, req) handler.HandleResponse(ctx, err, nil) } // UseRePassWord godoc // @Summary UseRePassWord // @Description UseRePassWord // @Tags User // @Accept json // @Produce json // @Param data body schema.UserRePassWordRequest true "UserRePassWordRequest" // @Success 200 {string} string "" // @Router /answer/api/v1/user/password/replacement [post] func (uc *UserController) UseRePassWord(ctx *gin.Context) { req := &schema.UserRePassWordRequest{} if handler.BindAndCheck(ctx, req) { return } req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) if len(req.Content) == 0 { handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) return } err := uc.userService.UpdatePasswordWhenForgot(ctx, req) uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, ctx.ClientIP()) handler.HandleResponse(ctx, err, nil) } // UserLogout user logout // @Summary user logout // @Description user logout // @Security ApiKeyAuth // @Tags User // @Accept json // @Produce json // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/user/logout [get] func (uc *UserController) UserLogout(ctx *gin.Context) { accessToken := middleware.ExtractToken(ctx) if len(accessToken) == 0 { handler.HandleResponse(ctx, nil, nil) return } _ = uc.authService.RemoveUserCacheInfo(ctx, accessToken) _ = uc.authService.RemoveAdminUserCacheInfo(ctx, accessToken) visitToken, _ := ctx.Cookie(constant.UserVisitCookiesCacheKey) _ = uc.authService.RemoveUserVisitCacheInfo(ctx, visitToken) handler.HandleResponse(ctx, nil, nil) } // UserRegisterByEmail godoc // @Summary UserRegisterByEmail // @Description UserRegisterByEmail // @Tags User // @Accept json // @Produce json // @Param data body schema.UserRegisterReq true "UserRegisterReq" // @Success 200 {object} handler.RespBody{data=schema.UserLoginResp} // @Router /answer/api/v1/user/register/email [post] func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) { // check whether site allow register or not siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !siteInfo.AllowNewRegistrations || !siteInfo.AllowEmailRegistrations { handler.HandleResponse(ctx, errors.BadRequest(reason.NotAllowedRegistration), nil) return } req := &schema.UserRegisterReq{} if handler.BindAndCheck(ctx, req) { return } if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) { handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil) return } req.IP = ctx.ClientIP() isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, req.IP, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } resp, errFields, err := uc.userService.UserRegisterByEmail(ctx, req) if len(errFields) > 0 { for _, field := range errFields { field.ErrorMsg = translator. Tr(handler.GetLangByCtx(ctx), field.ErrorMsg) } handler.HandleResponse(ctx, err, errFields) } else { handler.HandleResponse(ctx, err, resp) } } // UserVerifyEmail godoc // @Summary UserVerifyEmail // @Description UserVerifyEmail // @Tags User // @Accept json // @Produce json // @Param code query string true "code" default() // @Success 200 {object} handler.RespBody{data=schema.UserLoginResp} // @Router /answer/api/v1/user/email/verification [post] func (uc *UserController) UserVerifyEmail(ctx *gin.Context) { req := &schema.UserVerifyEmailReq{} if handler.BindAndCheck(ctx, req) { return } req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) if len(req.Content) == 0 { handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) return } resp, err := uc.userService.UserVerifyEmail(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEmail, ctx.ClientIP()) handler.HandleResponse(ctx, err, resp) } // UserVerifyEmailSend godoc // @Summary UserVerifyEmailSend // @Description UserVerifyEmailSend // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth // @Param captcha_id query string false "captcha_id" default() // @Param captcha_code query string false "captcha_code" default() // @Success 200 {string} string "" // @Router /answer/api/v1/user/email/verification/send [post] func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) { req := &schema.UserVerifyEmailSendReq{} if handler.BindAndCheck(ctx, req) { return } userInfo := middleware.GetUserInfoFromContext(ctx) if userInfo == nil { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) return } isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } err := uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID) handler.HandleResponse(ctx, err, nil) } // UserModifyPassWord godoc // @Summary UserModifyPassWord // @Description UserModifyPassWord // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UserModifyPasswordReq true "UserModifyPasswordReq" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/user/password [put] func (uc *UserController) UserModifyPassWord(ctx *gin.Context) { req := &schema.UserModifyPasswordReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.AccessToken = middleware.ExtractToken(ctx) isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEditUserinfo, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEditUserinfo, req.UserID) } oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !oldPassVerification { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "old_pass", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.OldPasswordVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields) return } if req.OldPass == req.Pass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "pass", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.NewPasswordSameAsPreviousSetting), }) handler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields) return } err = uc.userService.UserModifyPassword(ctx, req) if err == nil { uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEditUserinfo, req.UserID) } handler.HandleResponse(ctx, err, nil) } // UserUpdateInfo update user info // @Summary UserUpdateInfo update user info // @Description UserUpdateInfo update user info // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth // @Param Authorization header string true "access-token" // @Param data body schema.UpdateInfoRequest true "UpdateInfoRequest" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/user/info [put] func (uc *UserController) UserUpdateInfo(ctx *gin.Context) { req := &schema.UpdateInfoRequest{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) errFields, err := uc.userService.UpdateInfo(ctx, req) for _, field := range errFields { field.ErrorMsg = translator.Tr(handler.GetLangByCtx(ctx), field.ErrorMsg) } handler.HandleResponse(ctx, err, errFields) } // UserUpdateInterface update user interface config // @Summary UserUpdateInterface update user interface config // @Description UserUpdateInterface update user interface config // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth // @Param Authorization header string true "access-token" // @Param data body schema.UpdateUserInterfaceRequest true "UpdateInfoRequest" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/user/interface [put] func (uc *UserController) UserUpdateInterface(ctx *gin.Context) { req := &schema.UpdateUserInterfaceRequest{} if handler.BindAndCheck(ctx, req) { return } req.UserId = middleware.GetLoginUserIDFromContext(ctx) err := uc.userService.UserUpdateInterface(ctx, req) handler.HandleResponse(ctx, err, nil) } // ActionRecord godoc // @Summary ActionRecord // @Description ActionRecord // @Tags User // @Param action query string true "action" Enums(login, e_mail, find_pass) // @Security ApiKeyAuth // @Success 200 {object} handler.RespBody{data=schema.ActionRecordResp} // @Router /answer/api/v1/user/action/record [get] func (uc *UserController) ActionRecord(ctx *gin.Context) { req := &schema.ActionRecordReq{} if handler.BindAndCheck(ctx, req) { return } userinfo := middleware.GetUserInfoFromContext(ctx) if userinfo != nil { req.UserID = userinfo.UserID } req.IP = ctx.ClientIP() resp := &schema.ActionRecordResp{} isAdmin := middleware.GetUserIsAdminModerator(ctx) if isAdmin { resp.Verify = false handler.HandleResponse(ctx, nil, resp) } else { resp, err := uc.actionService.ActionRecord(ctx, req) handler.HandleResponse(ctx, err, resp) } } // GetUserNotificationConfig get user's notification config // @Summary get user's notification config // @Description get user's notification config // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth // @Success 200 {object} handler.RespBody{data=schema.GetUserNotificationConfigResp} // @Router /answer/api/v1/user/notification/config [post] func (uc *UserController) GetUserNotificationConfig(ctx *gin.Context) { userID := middleware.GetLoginUserIDFromContext(ctx) resp, err := uc.userNotificationConfigService.GetUserNotificationConfig(ctx, userID) handler.HandleResponse(ctx, err, resp) } // UpdateUserNotificationConfig update user's notification config // @Summary update user's notification config // @Description update user's notification config // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UpdateUserNotificationConfigReq true "UpdateUserNotificationConfigReq" // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/user/notification/config [put] func (uc *UserController) UpdateUserNotificationConfig(ctx *gin.Context) { req := &schema.UpdateUserNotificationConfigReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) err := uc.userNotificationConfigService.UpdateUserNotificationConfig(ctx, req) handler.HandleResponse(ctx, err, nil) } // UserChangeEmailSendCode send email to the user email then change their email // @Summary send email to the user email then change their email // @Description send email to the user email then change their email // @Security ApiKeyAuth // @Tags User // @Accept json // @Produce json // @Param data body schema.UserChangeEmailSendCodeReq true "UserChangeEmailSendCodeReq" // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/user/email/change/code [post] func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { req := &schema.UserChangeEmailSendCodeReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) // If the user is not logged in, the api cannot be used. // If the user email is not verified, that also can use this api to modify the email. if len(req.UserID) == 0 { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) return } // check whether email allow register or not siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) { handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil) return } isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEditUserinfo, req.UserID, req.CaptchaID, req.CaptchaCode) uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEditUserinfo, req.UserID) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } resp, err := uc.userService.UserChangeEmailSendCode(ctx, req) if err != nil { handler.HandleResponse(ctx, err, resp) return } if !isAdmin { uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEditUserinfo, ctx.ClientIP()) } handler.HandleResponse(ctx, err, nil) } // UserChangeEmailVerify user change email verification // @Summary user change email verification // @Description user change email verification // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UserChangeEmailVerifyReq true "UserChangeEmailVerifyReq" // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/user/email [put] func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) { req := &schema.UserChangeEmailVerifyReq{} if handler.BindAndCheck(ctx, req) { return } req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) if len(req.Content) == 0 { handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) return } resp, err := uc.userService.UserChangeEmailVerify(ctx, req.Content) uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEmail, ctx.ClientIP()) handler.HandleResponse(ctx, err, resp) } // UserRanking get user ranking // @Summary get user ranking // @Description get user ranking // @Tags User // @Accept json // @Produce json // @Success 200 {object} handler.RespBody{data=schema.UserRankingResp} // @Router /answer/api/v1/user/ranking [get] func (uc *UserController) UserRanking(ctx *gin.Context) { resp, err := uc.userService.UserRanking(ctx) handler.HandleResponse(ctx, err, resp) } // UserStaff get user staff // @Summary get user staff // @Description get user staff // @Tags User // @Accept json // @Produce json // @Param username query string true "username" // @Param page_size query string true "page_size" // @Success 200 {object} handler.RespBody{data=schema.GetUserStaffResp} // @Router /answer/api/v1/user/staff [get] func (uc *UserController) UserStaff(ctx *gin.Context) { req := &schema.GetUserStaffReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := uc.userService.GetUserStaff(ctx, req) handler.HandleResponse(ctx, err, resp) } // UserUnsubscribeNotification unsubscribe notification // @Summary unsubscribe notification // @Description unsubscribe notification // @Tags User // @Accept json // @Produce json // @Param data body schema.UserUnsubscribeNotificationReq true "UserUnsubscribeNotificationReq" // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/user/notification/unsubscribe [put] func (uc *UserController) UserUnsubscribeNotification(ctx *gin.Context) { req := &schema.UserUnsubscribeNotificationReq{} if handler.BindAndCheck(ctx, req) { return } req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) if len(req.Content) == 0 { handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) return } err := uc.userService.UserUnsubscribeNotification(ctx, req) handler.HandleResponse(ctx, err, nil) } // SearchUserListByName godoc // @Summary SearchUserListByName // @Description SearchUserListByName // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth // @Param username query string true "username" // @Success 200 {object} handler.RespBody{data=schema.GetOtherUserInfoResp} // @Router /answer/api/v1/user/info/search [get] func (uc *UserController) SearchUserListByName(ctx *gin.Context) { req := &schema.GetOtherUserInfoByUsernameReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := uc.userService.SearchUserListByName(ctx, req) handler.HandleResponse(ctx, err, resp) } func (uc *UserController) setVisitCookies(ctx *gin.Context, visitToken string, force bool) { if !force { cookie, _ := ctx.Cookie(constant.UserVisitCookiesCacheKey) // If the cookie is the same as the visitToken, no need to set it again if cookie == visitToken { return } } general, err := uc.siteInfoCommonService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general error: %v", err) return } parsedURL, err := url.Parse(general.SiteUrl) if err != nil { log.Errorf("parse url error: %v", err) return } ctx.SetCookie(constant.UserVisitCookiesCacheKey, visitToken, constant.UserVisitCacheTime, "/", parsedURL.Hostname(), true, true) } ================================================ FILE: internal/controller/user_plugin_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "encoding/json" "net/http" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/segmentfault/pacman/errors" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/plugin_common" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" ) // UserPluginController role controller type UserPluginController struct { pluginCommonService *plugin_common.PluginCommonService } // NewUserPluginController new controller func NewUserPluginController(pluginCommonService *plugin_common.PluginCommonService) *UserPluginController { return &UserPluginController{pluginCommonService: pluginCommonService} } // GetUserPluginList get plugin list that used for user. // @Summary get plugin list that used for user. // @Description get plugin list that used for user. // @Tags UserPlugin // @Security ApiKeyAuth // @Accept json // @Produce json // @Success 200 {object} handler.RespBody{data=[]schema.GetUserPluginListResp} // @Router /answer/api/v1/user/plugin/configs [get] func (pc *UserPluginController) GetUserPluginList(ctx *gin.Context) { resp := make([]*schema.GetUserPluginListResp, 0) _ = plugin.CallUserConfig(func(base plugin.UserConfig) error { info := base.Info() if plugin.StatusManager.IsEnabled(info.SlugName) { resp = append(resp, &schema.GetUserPluginListResp{ Name: info.Name.Translate(ctx), SlugName: info.SlugName, }) } return nil }) handler.HandleResponse(ctx, nil, resp) } // GetUserPluginConfig get user plugin config // @Summary get user plugin config // @Description get user plugin config // @Tags UserPlugin // @Security ApiKeyAuth // @Produce json // @Param plugin_slug_name query string true "plugin_slug_name" // @Success 200 {object} handler.RespBody{data=schema.GetPluginConfigResp} // @Router /answer/api/v1/user/plugin/config [get] func (pc *UserPluginController) GetUserPluginConfig(ctx *gin.Context) { req := &schema.GetUserPluginConfigReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp := &schema.GetUserPluginConfigResp{} _ = plugin.CallUserConfig(func(fn plugin.UserConfig) error { if fn.Info().SlugName != req.PluginSlugName { return nil } info := fn.Info() resp.Name = info.Name.Translate(ctx) resp.SlugName = info.SlugName resp.SetConfigFields(ctx, fn.UserConfigFields()) return nil }) configValue, err := pc.pluginCommonService.GetUserPluginConfig(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } if len(configValue) > 0 { configValueMapping := make(map[string]any) _ = json.Unmarshal([]byte(configValue), &configValueMapping) for _, field := range resp.ConfigFields { if value, ok := configValueMapping[field.Name]; ok { field.Value = value } } } handler.HandleResponse(ctx, err, resp) } // UpdatePluginUserConfig update user plugin config // @Summary update user plugin config // @Description update user plugin config // @Tags UserPlugin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UpdateUserPluginConfigReq true "UpdatePluginConfigReq" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/user/plugin/config [put] func (pc *UserPluginController) UpdatePluginUserConfig(ctx *gin.Context) { req := &schema.UpdateUserPluginConfigReq{} if handler.BindAndCheck(ctx, req) { return } if !plugin.StatusManager.IsEnabled(req.PluginSlugName) { handler.HandleResponse(ctx, errors.New(http.StatusBadRequest, reason.RequestFormatError), nil) return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) configFields, _ := json.Marshal(req.ConfigFields) err := plugin.CallUserConfig(func(fn plugin.UserConfig) error { if fn.Info().SlugName == req.PluginSlugName { return fn.UserConfigReceiver(req.UserID, configFields) } return nil }) if err != nil { handler.HandleResponse(ctx, err, nil) return } err = pc.pluginCommonService.UpdatePluginUserConfig(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller/vote_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/action" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // VoteController activity controller type VoteController struct { VoteService *content.VoteService rankService *rank.RankService actionService *action.CaptchaService } // NewVoteController new controller func NewVoteController( voteService *content.VoteService, rankService *rank.RankService, actionService *action.CaptchaService, ) *VoteController { return &VoteController{ VoteService: voteService, rankService: rankService, actionService: actionService, } } // VoteUp godoc // @Summary vote up // @Description add vote // @Tags Activity // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.VoteReq true "vote" // @Success 200 {object} handler.RespBody{data=schema.VoteResp} // @Router /answer/api/v1/vote/up [post] func (vc *VoteController) VoteUp(ctx *gin.Context) { req := &schema.VoteReq{} if handler.BindAndCheck(ctx, req) { return } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) can, needRank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { lang := handler.GetLangByCtx(ctx) msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: needRank}) handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil) return } isAdmin := middleware.GetUserIsAdminModerator(ctx) if !isAdmin { captchaPass := vc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionVote, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } if !isAdmin { vc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionVote, req.UserID) } resp, err := vc.VoteService.VoteUp(ctx, req) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) } else { handler.HandleResponse(ctx, err, resp) } } // VoteDown godoc // @Summary vote down // @Description add vote // @Tags Activity // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.VoteReq true "vote" // @Success 200 {object} handler.RespBody{data=schema.VoteResp} // @Router /answer/api/v1/vote/down [post] func (vc *VoteController) VoteDown(ctx *gin.Context) { req := &schema.VoteReq{} if handler.BindAndCheck(ctx, req) { return } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) isAdmin := middleware.GetUserIsAdminModerator(ctx) can, needRank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !can { lang := handler.GetLangByCtx(ctx) msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: needRank}) handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil) return } if !isAdmin { captchaPass := vc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionVote, req.UserID, req.CaptchaID, req.CaptchaCode) if !captchaPass { errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "captcha_code", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed), }) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) return } } if !isAdmin { vc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionVote, req.UserID) } resp, err := vc.VoteService.VoteDown(ctx, req) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) } else { handler.HandleResponse(ctx, err, resp) } } // UserVotes user votes // @Summary get user personal votes // @Description get user personal votes // @Tags Activity // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page size" // @Param page_size query int false "page size" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetVoteWithPageResp}} // @Router /answer/api/v1/personal/vote/page [get] func (vc *VoteController) UserVotes(ctx *gin.Context) { req := schema.GetVoteWithPageReq{} if handler.BindAndCheck(ctx, &req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := vc.VoteService.ListUserVotes(ctx, req) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller_admin/ai_conversation_admin_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller_admin import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/ai_conversation" "github.com/apache/answer/internal/service/feature_toggle" "github.com/gin-gonic/gin" ) // AIConversationAdminController ai conversation admin controller type AIConversationAdminController struct { aiConversationService ai_conversation.AIConversationService featureToggleSvc *feature_toggle.FeatureToggleService } // NewAIConversationAdminController new AI conversation admin controller func NewAIConversationAdminController( aiConversationService ai_conversation.AIConversationService, featureToggleSvc *feature_toggle.FeatureToggleService, ) *AIConversationAdminController { return &AIConversationAdminController{ aiConversationService: aiConversationService, featureToggleSvc: featureToggleSvc, } } func (ctrl *AIConversationAdminController) ensureEnabled(ctx *gin.Context) bool { if ctrl.featureToggleSvc == nil { return true } if err := ctrl.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureAIChatbot); err != nil { handler.HandleResponse(ctx, err, nil) return false } return true } // GetConversationList gets conversation list // @Summary get conversation list for admin // @Description get conversation list for admin // @Tags ai-conversation-admin // @Accept json // @Produce json // @Param page query int false "page" // @Param page_size query int false "page size" // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.AIConversationAdminListItem}} // @Router /answer/admin/api/ai/conversation/page [get] func (ctrl *AIConversationAdminController) GetConversationList(ctx *gin.Context) { if !ctrl.ensureEnabled(ctx) { return } req := &schema.AIConversationAdminListReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := ctrl.aiConversationService.GetConversationListForAdmin(ctx, req) handler.HandleResponse(ctx, err, resp) } // GetConversationDetail get conversation detail // @Summary get conversation detail for admin // @Description get conversation detail for admin // @Tags ai-conversation-admin // @Accept json // @Produce json // @Param conversation_id query string true "conversation id" // @Success 200 {object} handler.RespBody{data=schema.AIConversationAdminDetailResp} // @Router /answer/admin/api/ai/conversation [get] func (ctrl *AIConversationAdminController) GetConversationDetail(ctx *gin.Context) { if !ctrl.ensureEnabled(ctx) { return } req := &schema.AIConversationAdminDetailReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := ctrl.aiConversationService.GetConversationDetailForAdmin(ctx, req) handler.HandleResponse(ctx, err, resp) } // DeleteConversation delete conversation // @Summary delete conversation for admin // @Description delete conversation and its related records for admin // @Tags ai-conversation-admin // @Accept json // @Produce json // @Param data body schema.AIConversationAdminDeleteReq true "apikey" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/ai/conversation [delete] func (ctrl *AIConversationAdminController) DeleteConversation(ctx *gin.Context) { if !ctrl.ensureEnabled(ctx) { return } req := &schema.AIConversationAdminDeleteReq{} if handler.BindAndCheck(ctx, req) { return } err := ctrl.aiConversationService.DeleteConversationForAdmin(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller_admin/badge_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller_admin import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/badge" "github.com/gin-gonic/gin" ) type BadgeController struct { badgeService *badge.BadgeService } func NewBadgeController(badgeService *badge.BadgeService) *BadgeController { return &BadgeController{ badgeService: badgeService, } } // GetBadgeList list all badges by page // @Summary list all badges by page // @Description list all badges by page // @Tags AdminBadge // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page" // @Param page_size query int false "page size" // @Param status query string false "badge status" Enums(, active, inactive) // @Param q query string false "search param" // @Success 200 {object} handler.RespBody{data=[]schema.GetBadgeListPagedResp} // @Router /answer/admin/api/badges [get] func (b *BadgeController) GetBadgeList(ctx *gin.Context) { req := &schema.GetBadgeListPagedReq{} if handler.BindAndCheck(ctx, req) { return } resp, total, err := b.badgeService.ListPaged(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) } // UpdateBadgeStatus update badge status // @Summary update badge status // @Description update badge status // @Tags AdminBadge // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UpdateBadgeStatusReq true "UpdateBadgeStatusReq" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/badge/status [put] func (b *BadgeController) UpdateBadgeStatus(ctx *gin.Context) { req := &schema.UpdateBadgeStatusReq{} if handler.BindAndCheck(ctx, req) { return } err := b.badgeService.UpdateStatus(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller_admin/controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller_admin import "github.com/google/wire" // ProviderSetController is controller providers. var ProviderSetController = wire.NewSet( NewUserAdminController, NewThemeController, NewSiteInfoController, NewRoleController, NewPluginController, NewBadgeController, NewAdminAPIKeyController, NewAIConversationAdminController, ) ================================================ FILE: internal/controller_admin/e_api_key_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller_admin import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/apikey" "github.com/gin-gonic/gin" ) // AdminAPIKeyController site info controller type AdminAPIKeyController struct { apiKeyService *apikey.APIKeyService } // NewAdminAPIKeyController new site info controller func NewAdminAPIKeyController(apiKeyService *apikey.APIKeyService) *AdminAPIKeyController { return &AdminAPIKeyController{ apiKeyService: apiKeyService, } } // GetAllAPIKeys get all api keys // @Summary get all api keys // @Description get all api keys // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=[]schema.GetAPIKeyResp} // @Router /answer/admin/api/api-key/all [get] func (sc *AdminAPIKeyController) GetAllAPIKeys(ctx *gin.Context) { resp, err := sc.apiKeyService.GetAPIKeyList(ctx, &schema.GetAPIKeyReq{}) handler.HandleResponse(ctx, err, resp) } // AddAPIKey add apikey // @Summary add apikey // @Description add apikey // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.AddAPIKeyReq true "apikey" // @Success 200 {object} handler.RespBody{data=schema.AddAPIKeyResp} // @Router /answer/admin/api/api-key [post] func (sc *AdminAPIKeyController) AddAPIKey(ctx *gin.Context) { req := &schema.AddAPIKeyReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := sc.apiKeyService.AddAPIKey(ctx, req) handler.HandleResponse(ctx, err, resp) } // UpdateAPIKey update apikey // @Summary update apikey // @Description update apikey // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.UpdateAPIKeyReq true "apikey" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/api-key [put] func (sc *AdminAPIKeyController) UpdateAPIKey(ctx *gin.Context) { req := &schema.UpdateAPIKeyReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) err := sc.apiKeyService.UpdateAPIKey(ctx, req) handler.HandleResponse(ctx, err, nil) } // DeleteAPIKey delete apikey // @Summary delete apikey // @Description delete apikey // @Security ApiKeyAuth // @Tags admin // @Param data body schema.DeleteAPIKeyReq true "apikey" // @Produce json // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/api-key [delete] func (sc *AdminAPIKeyController) DeleteAPIKey(ctx *gin.Context) { req := &schema.DeleteAPIKeyReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) err := sc.apiKeyService.DeleteAPIKey(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller_admin/plugin_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller_admin import ( "encoding/json" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/plugin_common" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" ) // PluginController role controller type PluginController struct { pluginCommonService *plugin_common.PluginCommonService } // NewPluginController new controller func NewPluginController(pluginCommonService *plugin_common.PluginCommonService) *PluginController { return &PluginController{pluginCommonService: pluginCommonService} } // GetAllPluginStatus get all plugins status // @Summary get all plugins status // @Description get all plugins status // @Tags Plugin // @Accept json // @Produce json // @Success 200 {object} handler.RespBody{data=[]schema.GetPluginListResp} // @Router /answer/api/v1/plugin/status [get] func (pc *PluginController) GetAllPluginStatus(ctx *gin.Context) { resp := make([]*schema.GetAllPluginStatusResp, 0) _ = plugin.CallBase(func(base plugin.Base) error { info := base.Info() resp = append(resp, &schema.GetAllPluginStatusResp{ SlugName: info.SlugName, Enabled: plugin.StatusManager.IsEnabled(info.SlugName), }) return nil }) handler.HandleResponse(ctx, nil, resp) } // GetPluginList get plugin list // @Summary get plugin list // @Description get plugin list // @Tags AdminPlugin // @Security ApiKeyAuth // @Accept json // @Produce json // @Param status query string false "status: active/inactive" // @Param have_config query boolean false "have config" // @Success 200 {object} handler.RespBody{data=[]schema.GetPluginListResp} // @Router /answer/admin/api/plugins [get] func (pc *PluginController) GetPluginList(ctx *gin.Context) { req := &schema.GetPluginListReq{} if handler.BindAndCheck(ctx, req) { return } pluginConfigMapping := make(map[string]bool) _ = plugin.CallConfig(func(fn plugin.Config) error { if len(fn.ConfigFields()) > 0 { pluginConfigMapping[fn.Info().SlugName] = true } return nil }) resp := make([]*schema.GetPluginListResp, 0) _ = plugin.CallBase(func(base plugin.Base) error { info := base.Info() resp = append(resp, &schema.GetPluginListResp{ Name: info.Name.Translate(ctx), SlugName: info.SlugName, Description: info.Description.Translate(ctx), Version: info.Version, Enabled: plugin.StatusManager.IsEnabled(info.SlugName), HaveConfig: pluginConfigMapping[info.SlugName], Link: info.Link, }) return nil }) if len(req.Status) > 0 { resp = pc.filterPluginByStatus(resp, req.Status) } if req.HaveConfig { resp = pc.filterNoConfigPlugin(resp) } handler.HandleResponse(ctx, nil, resp) } func (pc *PluginController) filterNoConfigPlugin(list []*schema.GetPluginListResp) []*schema.GetPluginListResp { resp := make([]*schema.GetPluginListResp, 0) for _, t := range list { if t.HaveConfig { resp = append(resp, t) } } return resp } func (pc *PluginController) filterPluginByStatus(list []*schema.GetPluginListResp, status schema.PluginStatus, ) []*schema.GetPluginListResp { resp := make([]*schema.GetPluginListResp, 0) for _, t := range list { if status == schema.PluginStatusActive && t.Enabled { resp = append(resp, t) } else if status == schema.PluginStatusInactive && !t.Enabled { resp = append(resp, t) } } return resp } // UpdatePluginStatus update plugin status // @Summary update plugin status // @Description update plugin status // @Tags AdminPlugin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UpdatePluginStatusReq true "UpdatePluginStatusReq" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/plugin/status [put] func (pc *PluginController) UpdatePluginStatus(ctx *gin.Context) { req := &schema.UpdatePluginStatusReq{} if handler.BindAndCheck(ctx, req) { return } plugin.StatusManager.Enable(req.PluginSlugName, req.Enabled) err := pc.pluginCommonService.UpdatePluginStatus(ctx) handler.HandleResponse(ctx, err, nil) } // GetPluginConfig get plugin config // @Summary get plugin config // @Description get plugin config // @Tags AdminPlugin // @Security ApiKeyAuth // @Produce json // @Param plugin_slug_name query string true "plugin_slug_name" // @Success 200 {object} handler.RespBody{data=schema.GetPluginConfigResp} // @Router /answer/admin/api/plugin/config [get] func (pc *PluginController) GetPluginConfig(ctx *gin.Context) { req := &schema.GetPluginConfigReq{} if handler.BindAndCheck(ctx, req) { return } resp := &schema.GetPluginConfigResp{} _ = plugin.CallBase(func(base plugin.Base) error { if base.Info().SlugName != req.PluginSlugName { return nil } info := base.Info() resp.Name = info.Name.Translate(ctx) resp.SlugName = info.SlugName resp.Description = info.Description.Translate(ctx) resp.Version = info.Version return nil }) _ = plugin.CallConfig(func(fn plugin.Config) error { if fn.Info().SlugName != req.PluginSlugName { return nil } resp.SetConfigFields(ctx, fn.ConfigFields()) return nil }) handler.HandleResponse(ctx, nil, resp) } // UpdatePluginConfig update plugin config // @Summary update plugin config // @Description update plugin config // @Tags AdminPlugin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param data body schema.UpdatePluginConfigReq true "UpdatePluginConfigReq" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/plugin/config [put] func (pc *PluginController) UpdatePluginConfig(ctx *gin.Context) { req := &schema.UpdatePluginConfigReq{} if handler.BindAndCheck(ctx, req) { return } configFields, _ := json.Marshal(req.ConfigFields) err := plugin.CallConfig(func(fn plugin.Config) error { if fn.Info().SlugName == req.PluginSlugName { return fn.ConfigReceiver(configFields) } return nil }) if err != nil { handler.HandleResponse(ctx, err, nil) return } err = pc.pluginCommonService.UpdatePluginConfig(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller_admin/role_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller_admin import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/schema" service "github.com/apache/answer/internal/service/role" "github.com/gin-gonic/gin" ) // RoleController role controller type RoleController struct { roleService *service.RoleService } // NewRoleController new controller func NewRoleController(roleService *service.RoleService) *RoleController { return &RoleController{roleService: roleService} } // GetRoleList get role list // @Summary get role list // @Description get role list // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=[]schema.GetRoleResp} // @Router /answer/admin/api/roles [get] func (rc *RoleController) GetRoleList(ctx *gin.Context) { req := &schema.GetRoleResp{} if handler.BindAndCheck(ctx, req) { return } resp, err := rc.roleService.GetRoleList(ctx) handler.HandleResponse(ctx, err, resp) } ================================================ FILE: internal/controller_admin/siteinfo_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller_admin import ( "html" "net/http" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/siteinfo" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) // SiteInfoController site info controller type SiteInfoController struct { siteInfoService *siteinfo.SiteInfoService } // NewSiteInfoController new site info controller func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController { return &SiteInfoController{ siteInfoService: siteInfoService, } } // GetGeneral get site general information // @Summary get site general information // @Description get site general information // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} // @Router /answer/admin/api/siteinfo/general [get] func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteGeneral(ctx) handler.HandleResponse(ctx, err, resp) } // GetInterface get site interface // @Summary get site interface // @Description get site interface // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceSettingsResp} // @Router /answer/admin/api/siteinfo/interface [get] func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) } // GetUsersSettings get site interface // @Summary get site interface // @Description get site interface // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteUsersSettingsResp} // @Router /answer/admin/api/siteinfo/users-settings [get] func (sc *SiteInfoController) GetUsersSettings(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteUsersSettings(ctx) handler.HandleResponse(ctx, err, resp) } // GetSiteBranding get site interface // @Summary get site interface // @Description get site interface // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteBrandingResp} // @Router /answer/admin/api/siteinfo/branding [get] func (sc *SiteInfoController) GetSiteBranding(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteBranding(ctx) handler.HandleResponse(ctx, err, resp) } // GetSiteTag get site tags setting // @Summary get site tags setting // @Description get site tags setting // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteTagsResp} // @Router /answer/admin/api/siteinfo/tag [get] func (sc *SiteInfoController) GetSiteTag(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteTag(ctx) handler.HandleResponse(ctx, err, resp) } // GetSiteQuestion get site questions setting // @Summary get site questions setting // @Description get site questions setting // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteQuestionsResp} // @Router /answer/admin/api/siteinfo/question [get] func (sc *SiteInfoController) GetSiteQuestion(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteQuestion(ctx) handler.HandleResponse(ctx, err, resp) } // GetSiteAdvanced get site advanced setting // @Summary get site advanced setting // @Description get site advanced setting // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteAdvancedResp} // @Router /answer/admin/api/siteinfo/advanced [get] func (sc *SiteInfoController) GetSiteAdvanced(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteAdvanced(ctx) handler.HandleResponse(ctx, err, resp) } // GetSitePolicies Get the policies information for the site // @Summary Get the policies information for the site // @Description Get the policies information for the site // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SitePoliciesResp} // @Router /answer/admin/api/siteinfo/polices [get] func (sc *SiteInfoController) GetSitePolicies(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSitePolicies(ctx) handler.HandleResponse(ctx, err, resp) } // GetSiteSecurity Get the security information for the site // @Summary Get the security information for the site // @Description Get the security information for the site // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteSecurityResp} // @Router /answer/admin/api/siteinfo/security [get] func (sc *SiteInfoController) GetSiteSecurity(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteSecurity(ctx) handler.HandleResponse(ctx, err, resp) } // GetSeo get site seo information // @Summary get site seo information // @Description get site seo information // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteSeoResp} // @Router /answer/admin/api/siteinfo/seo [get] func (sc *SiteInfoController) GetSeo(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSeo(ctx) handler.HandleResponse(ctx, err, resp) } // GetSiteLogin get site info login config // @Summary get site info login config // @Description get site info login config // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteLoginResp} // @Router /answer/admin/api/siteinfo/login [get] func (sc *SiteInfoController) GetSiteLogin(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteLogin(ctx) handler.HandleResponse(ctx, err, resp) } // GetSiteCustomCssHTML get site info custom html css config // @Summary get site info custom html css config // @Description get site info custom html css config // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteCustomCssHTMLResp} // @Router /answer/admin/api/siteinfo/custom-css-html [get] func (sc *SiteInfoController) GetSiteCustomCssHTML(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx) handler.HandleResponse(ctx, err, resp) } // GetSiteTheme get site info theme config // @Summary get site info theme config // @Description get site info theme config // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteThemeResp} // @Router /answer/admin/api/siteinfo/theme [get] func (sc *SiteInfoController) GetSiteTheme(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteTheme(ctx) handler.HandleResponse(ctx, err, resp) } // GetSiteUsers get site user config // @Summary get site user config // @Description get site user config // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteUsersResp} // @Router /answer/admin/api/siteinfo/users [get] func (sc *SiteInfoController) GetSiteUsers(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteUsers(ctx) handler.HandleResponse(ctx, err, resp) } // GetRobots get site robots information // @Summary get site robots information // @Description get site robots information // @Tags site // @Produce json // @Success 200 {string} txt "" // @Router /robots.txt [get] func (sc *SiteInfoController) GetRobots(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSeo(ctx) if err != nil { ctx.String(http.StatusOK, "") return } ctx.String(http.StatusOK, resp.Robots) } // GetCss get site custom CSS // @Summary get site custom CSS // @Description get site custom CSS // @Tags site // @Produce text/css // @Success 200 {string} css "" // @Router /custom.css [get] func (sc *SiteInfoController) GetCss(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx) if err != nil { ctx.String(http.StatusOK, "") return } ctx.Header("content-type", "text/css;charset=utf-8") ctx.String(http.StatusOK, resp.CustomCss) } // UpdateSeo update site seo information // @Summary update site seo information // @Description update site seo information // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteSeoReq true "seo" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/seo [put] func (sc *SiteInfoController) UpdateSeo(ctx *gin.Context) { req := schema.SiteSeoReq{} if handler.BindAndCheck(ctx, &req) { return } err := sc.siteInfoService.SaveSeo(ctx, req) handler.HandleResponse(ctx, err, nil) } // UpdateGeneral update site general information // @Summary update site general information // @Description update site general information // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteGeneralReq true "general" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/general [put] func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) { req := schema.SiteGeneralReq{} if handler.BindAndCheck(ctx, &req) { return } err := sc.siteInfoService.SaveSiteGeneral(ctx, req) req.Name = html.UnescapeString(req.Name) handler.HandleResponse(ctx, err, req) } // UpdateInterface update site interface // @Summary update site info interface // @Description update site info interface // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteInterfaceReq true "general" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/interface [put] func (sc *SiteInfoController) UpdateInterface(ctx *gin.Context) { req := schema.SiteInterfaceReq{} if handler.BindAndCheck(ctx, &req) { return } err := sc.siteInfoService.SaveSiteInterface(ctx, req) handler.HandleResponse(ctx, err, nil) } // UpdateUsersSettings update users settings // @Summary update site info users settings // @Description update site info users settings // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteUsersSettingsReq true "general" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/users-settings [put] func (sc *SiteInfoController) UpdateUsersSettings(ctx *gin.Context) { req := schema.SiteUsersSettingsReq{} if handler.BindAndCheck(ctx, &req) { return } err := sc.siteInfoService.SaveSiteUsersSettings(ctx, req) handler.HandleResponse(ctx, err, nil) } // UpdateBranding update site branding // @Summary update site info branding // @Description update site info branding // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteBrandingReq true "branding info" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/branding [put] func (sc *SiteInfoController) UpdateBranding(ctx *gin.Context) { req := &schema.SiteBrandingReq{} if handler.BindAndCheck(ctx, req) { return } currentBranding, getBrandingErr := sc.siteInfoService.GetSiteBranding(ctx) if getBrandingErr == nil { cleanUpErr := sc.siteInfoService.CleanUpRemovedBrandingFiles(ctx, req, currentBranding) if cleanUpErr != nil { log.Errorf("failed to clean up removed branding file(s): %v", cleanUpErr) } } else { log.Errorf("failed to get current site branding: %v", getBrandingErr) } saveErr := sc.siteInfoService.SaveSiteBranding(ctx, req) handler.HandleResponse(ctx, saveErr, nil) } // UpdateSiteQuestion update site question settings // @Summary update site question settings // @Description update site question settings // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteQuestionsReq true "questions settings" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/question [put] func (sc *SiteInfoController) UpdateSiteQuestion(ctx *gin.Context) { req := &schema.SiteQuestionsReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := sc.siteInfoService.SaveSiteQuestions(ctx, req) handler.HandleResponse(ctx, err, resp) } // UpdateSiteTag update site tag settings // @Summary update site tag settings // @Description update site tag settings // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteTagsReq true "tags settings" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/tag [put] func (sc *SiteInfoController) UpdateSiteTag(ctx *gin.Context) { req := &schema.SiteTagsReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := sc.siteInfoService.SaveSiteTags(ctx, req) handler.HandleResponse(ctx, err, resp) } // UpdateSiteAdvanced update site advanced info // @Summary update site advanced info // @Description update site advanced info // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteAdvancedReq true "advanced settings" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/advanced [put] func (sc *SiteInfoController) UpdateSiteAdvanced(ctx *gin.Context) { req := &schema.SiteAdvancedReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := sc.siteInfoService.SaveSiteAdvanced(ctx, req) handler.HandleResponse(ctx, err, resp) } // UpdateSitePolices update site policies configuration // @Summary update site policies configuration // @Description update site policies configuration // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SitePoliciesReq true "write info" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/polices [put] func (sc *SiteInfoController) UpdateSitePolices(ctx *gin.Context) { req := &schema.SitePoliciesReq{} if handler.BindAndCheck(ctx, req) { return } err := sc.siteInfoService.SaveSitePolicies(ctx, req) handler.HandleResponse(ctx, err, nil) } // UpdateSiteSecurity update site security configuration // @Summary update site security configuration // @Description update site security configuration // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteSecurityReq true "write info" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/security [put] func (sc *SiteInfoController) UpdateSiteSecurity(ctx *gin.Context) { req := &schema.SiteSecurityReq{} if handler.BindAndCheck(ctx, req) { return } err := sc.siteInfoService.SaveSiteSecurity(ctx, req) handler.HandleResponse(ctx, err, nil) } // UpdateSiteLogin update site login // @Summary update site login // @Description update site login // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteLoginReq true "login info" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/login [put] func (sc *SiteInfoController) UpdateSiteLogin(ctx *gin.Context) { req := &schema.SiteLoginReq{} if handler.BindAndCheck(ctx, req) { return } err := sc.siteInfoService.SaveSiteLogin(ctx, req) handler.HandleResponse(ctx, err, nil) } // UpdateSiteCustomCssHTML update site custom css html config // @Summary update site custom css html config // @Description update site custom css html config // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteCustomCssHTMLReq true "login info" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/custom-css-html [put] func (sc *SiteInfoController) UpdateSiteCustomCssHTML(ctx *gin.Context) { req := &schema.SiteCustomCssHTMLReq{} if handler.BindAndCheck(ctx, req) { return } err := sc.siteInfoService.SaveSiteCustomCssHTML(ctx, req) handler.HandleResponse(ctx, err, nil) } // SaveSiteTheme update site custom css html config // @Summary update site custom css html config // @Description update site custom css html config // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteThemeReq true "login info" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/theme [put] func (sc *SiteInfoController) SaveSiteTheme(ctx *gin.Context) { req := &schema.SiteThemeReq{} if handler.BindAndCheck(ctx, req) { return } err := sc.siteInfoService.SaveSiteTheme(ctx, req) handler.HandleResponse(ctx, err, nil) } // UpdateSiteUsers update site config about users // @Summary update site info config about users // @Description update site info config about users // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SiteUsersReq true "users info" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/siteinfo/users [put] func (sc *SiteInfoController) UpdateSiteUsers(ctx *gin.Context) { req := &schema.SiteUsersReq{} if handler.BindAndCheck(ctx, req) { return } err := sc.siteInfoService.SaveSiteUsers(ctx, req) handler.HandleResponse(ctx, err, nil) } // GetSMTPConfig get smtp config // @Summary GetSMTPConfig get smtp config // @Description GetSMTPConfig get smtp config // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.GetSMTPConfigResp} // @Router /answer/admin/api/setting/smtp [get] func (sc *SiteInfoController) GetSMTPConfig(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSMTPConfig(ctx) handler.HandleResponse(ctx, err, resp) } // UpdateSMTPConfig update smtp config // @Summary update smtp config // @Description update smtp config // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.UpdateSMTPConfigReq true "smtp config" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/setting/smtp [put] func (sc *SiteInfoController) UpdateSMTPConfig(ctx *gin.Context) { req := &schema.UpdateSMTPConfigReq{} if handler.BindAndCheck(ctx, req) { return } err := sc.siteInfoService.UpdateSMTPConfig(ctx, req) handler.HandleResponse(ctx, err, nil) } // GetPrivilegesConfig get privileges config // @Summary GetPrivilegesConfig get privileges config // @Description GetPrivilegesConfig get privileges config // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.GetPrivilegesConfigResp} // @Router /answer/admin/api/setting/privileges [get] func (sc *SiteInfoController) GetPrivilegesConfig(ctx *gin.Context) { resp, err := sc.siteInfoService.GetPrivilegesConfig(ctx) handler.HandleResponse(ctx, err, resp) } // UpdatePrivilegesConfig update privileges config // @Summary update privileges config // @Description update privileges config // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.UpdatePrivilegesConfigReq true "config" // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/setting/privileges [put] func (sc *SiteInfoController) UpdatePrivilegesConfig(ctx *gin.Context) { req := &schema.UpdatePrivilegesConfigReq{} if handler.BindAndCheck(ctx, req) { return } err := sc.siteInfoService.UpdatePrivilegesConfig(ctx, req) handler.HandleResponse(ctx, err, nil) } // GetAIConfig get AI configuration // @Summary get AI configuration // @Description get AI configuration // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteAIResp} // @Router /answer/admin/api/ai-config [get] func (sc *SiteInfoController) GetAIConfig(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteAI(ctx) handler.HandleResponse(ctx, err, resp) } // UpdateAIConfig update AI configuration // @Summary update AI configuration // @Description update AI configuration // @Security ApiKeyAuth // @Tags admin // @Param data body schema.SiteAIReq true "AI config" // @Produce json // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/ai-config [put] func (sc *SiteInfoController) UpdateAIConfig(ctx *gin.Context) { req := &schema.SiteAIReq{} if handler.BindAndCheck(ctx, req) { return } err := sc.siteInfoService.SaveSiteAI(ctx, req) handler.HandleResponse(ctx, err, nil) } // GetAIProvider get AI provider configuration // @Summary get AI provider configuration // @Description get AI provider configuration // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=[]schema.GetAIProviderResp} // @Router /answer/admin/api/ai-provider [get] func (sc *SiteInfoController) GetAIProvider(ctx *gin.Context) { resp, err := sc.siteInfoService.GetAIProvider(ctx) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, resp) } // RequestAIModels get AI models // @Summary get AI models // @Description get AI models // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=[]schema.GetAIModelResp} // @Router /answer/admin/api/ai-models [post] func (sc *SiteInfoController) RequestAIModels(ctx *gin.Context) { req := &schema.GetAIModelsReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := sc.siteInfoService.GetAIModels(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } handler.HandleResponse(ctx, nil, resp) } // GetMCPConfig get MCP configuration // @Summary get MCP configuration // @Description get MCP configuration // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteMCPResp} // @Router /answer/admin/api/mcp-config [get] func (sc *SiteInfoController) GetMCPConfig(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteMCP(ctx) handler.HandleResponse(ctx, err, resp) } // UpdateMCPConfig update MCP configuration // @Summary update MCP configuration // @Description update MCP configuration // @Security ApiKeyAuth // @Tags admin // @Param data body schema.SiteMCPReq true "MCP config" // @Produce json // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/mcp-config [put] func (sc *SiteInfoController) UpdateMCPConfig(ctx *gin.Context) { req := &schema.SiteMCPReq{} if handler.BindAndCheck(ctx, req) { return } err := sc.siteInfoService.SaveSiteMCP(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/controller_admin/theme_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller_admin import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/schema" "github.com/gin-gonic/gin" ) type ThemeController struct{} // NewThemeController new theme controller. func NewThemeController() *ThemeController { return &ThemeController{} } // GetThemeOptions godoc // @Summary Get theme options // @Description Get theme options // @Security ApiKeyAuth // @Tags admin // @Produce json // @Success 200 {object} handler.RespBody{} // @Router /answer/admin/api/theme/options [get] func (t *ThemeController) GetThemeOptions(ctx *gin.Context) { handler.HandleResponse(ctx, nil, schema.GetThemeOptions) } ================================================ FILE: internal/controller_admin/user_backyard_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller_admin import ( "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/user_admin" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // UserAdminController user controller type UserAdminController struct { userService *user_admin.UserAdminService } // NewUserAdminController new controller func NewUserAdminController(userService *user_admin.UserAdminService) *UserAdminController { return &UserAdminController{userService: userService} } // UpdateUserStatus update user // @Summary update user // @Description update user // @Security ApiKeyAuth // @Tags admin // @Accept json // @Produce json // @Param data body schema.UpdateUserStatusReq true "user" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/user/status [put] func (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) { if u, ok := plugin.GetUserCenter(); ok && u.Description().UserStatusAgentEnabled { handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) return } req := &schema.UpdateUserStatusReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) err := uc.userService.UpdateUserStatus(ctx, req) handler.HandleResponse(ctx, err, nil) } // UpdateUserRole update user role // @Summary update user role // @Description update user role // @Security ApiKeyAuth // @Tags admin // @Accept json // @Produce json // @Param data body schema.UpdateUserRoleReq true "user" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/user/role [put] func (uc *UserAdminController) UpdateUserRole(ctx *gin.Context) { req := &schema.UpdateUserRoleReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) err := uc.userService.UpdateUserRole(ctx, req) handler.HandleResponse(ctx, err, nil) } // AddUser add user // @Summary add user // @Description add user // @Security ApiKeyAuth // @Tags admin // @Accept json // @Produce json // @Param data body schema.AddUserReq true "user" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/user [post] func (uc *UserAdminController) AddUser(ctx *gin.Context) { req := &schema.AddUserReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) err := uc.userService.AddUser(ctx, req) handler.HandleResponse(ctx, err, nil) } // AddUsers add users // @Summary add users // @Description add users // @Security ApiKeyAuth // @Tags admin // @Accept json // @Produce json // @Param data body schema.AddUsersReq true "user" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/users [post] func (uc *UserAdminController) AddUsers(ctx *gin.Context) { req := &schema.AddUsersReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := uc.userService.AddUsers(ctx, req) handler.HandleResponse(ctx, err, resp) } // UpdateUserPassword update user password // @Summary update user password // @Description update user password // @Security ApiKeyAuth // @Tags admin // @Accept json // @Produce json // @Param data body schema.UpdateUserPasswordReq true "user" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/user/password [put] func (uc *UserAdminController) UpdateUserPassword(ctx *gin.Context) { req := &schema.UpdateUserPasswordReq{} if handler.BindAndCheck(ctx, req) { return } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) err := uc.userService.UpdateUserPassword(ctx, req) handler.HandleResponse(ctx, err, nil) } // EditUserProfile edit user profile // @Summary edit user profile // @Description edit user profile // @Security ApiKeyAuth // @Tags admin // @Accept json // @Produce json // @Param data body schema.EditUserProfileReq true "user" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/user/profile [put] func (uc *UserAdminController) EditUserProfile(ctx *gin.Context) { req := &schema.EditUserProfileReq{} if handler.BindAndCheck(ctx, req) { return } req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) if !req.IsAdmin { handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) return } errFields, err := uc.userService.EditUserProfile(ctx, req) for _, field := range errFields { field.ErrorMsg = translator.Tr(handler.GetLangByCtx(ctx), field.ErrorMsg) } handler.HandleResponse(ctx, err, errFields) } // GetUserPage get user page // @Summary get user page // @Description get user page // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param page query int false "page size" // @Param page_size query int false "page size" // @Param query query string false "search query: email, username or id:[id]" // @Param staff query bool false "staff user" // @Param status query string false "user status" Enums(suspended, deleted, inactive) // @Success 200 {object} handler.RespBody{data=pager.PageModel{records=[]schema.GetUserPageResp}} // @Router /answer/admin/api/users/page [get] func (uc *UserAdminController) GetUserPage(ctx *gin.Context) { req := &schema.GetUserPageReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := uc.userService.GetUserPage(ctx, req) handler.HandleResponse(ctx, err, resp) } // GetUserActivation get user activation // @Summary get user activation // @Description get user activation // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param user_id query string true "user id" // @Success 200 {object} handler.RespBody{data=schema.GetUserActivationResp} // @Router /answer/admin/api/user/activation [get] func (uc *UserAdminController) GetUserActivation(ctx *gin.Context) { req := &schema.GetUserActivationReq{} if handler.BindAndCheck(ctx, req) { return } resp, err := uc.userService.GetUserActivation(ctx, req) handler.HandleResponse(ctx, err, resp) } // SendUserActivation send user activation // @Summary send user activation // @Description send user activation // @Security ApiKeyAuth // @Tags admin // @Produce json // @Param data body schema.SendUserActivationReq true "SendUserActivationReq" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/users/activation [post] func (uc *UserAdminController) SendUserActivation(ctx *gin.Context) { req := &schema.SendUserActivationReq{} if handler.BindAndCheck(ctx, req) { return } err := uc.userService.SendUserActivation(ctx, req) handler.HandleResponse(ctx, err, nil) } // DeletePermanently delete permanently // @Summary delete permanently // @Description delete permanently // @Security ApiKeyAuth // @Tags admin // @Accept json // @Produce json // @Param data body schema.DeletePermanentlyReq true "DeletePermanentlyReq" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/delete/permanently [delete] func (uc *UserAdminController) DeletePermanently(ctx *gin.Context) { req := &schema.DeletePermanentlyReq{} if handler.BindAndCheck(ctx, req) { return } err := uc.userService.DeletePermanently(ctx, req) handler.HandleResponse(ctx, err, nil) } ================================================ FILE: internal/entity/activity_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" const ( ActivityAvailable = 0 ActivityCancelled = 1 ) // Activity activity type Activity struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"` UserID string `xorm:"not null index BIGINT(20) user_id"` TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"` ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"` OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"` ActivityType int `xorm:"not null INT(11) activity_type"` Cancelled int `xorm:"not null default 0 TINYINT(4) cancelled"` Rank int `xorm:"not null default 0 INT(11) rank"` HasRank int `xorm:"not null default 0 TINYINT(4) has_rank"` RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"` } type ActivityRankSum struct { Rank int `xorm:"not null default 0 INT(11) rank"` } type ActivityUserRankStat struct { UserID string `xorm:"user_id"` Rank int `xorm:"rank_amount"` } type ActivityUserVoteStat struct { UserID string `xorm:"user_id"` VoteCount int `xorm:"vote_count"` } // TableName activity table name func (Activity) TableName() string { return "activity" } ================================================ FILE: internal/entity/ai_conversation.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // AIConversation AI type AIConversation struct { ID int `xorm:"not null pk autoincr INT(11) id"` CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` ConversationID string `xorm:"not null unique VARCHAR(255) conversation_id"` Topic string `xorm:"not null MEDIUMTEXT topic"` UserID string `xorm:"not null default 0 BIGINT(20) user_id"` } // TableName returns the table name func (AIConversation) TableName() string { return "ai_conversation" } ================================================ FILE: internal/entity/ai_conversation_record.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // AIConversationRecord AI Conversation Record type AIConversationRecord struct { ID int `xorm:"not null pk autoincr INT(11) id"` CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` ConversationID string `xorm:"not null VARCHAR(255) conversation_id"` ChatCompletionID string `xorm:"not null VARCHAR(255) chat_completion_id"` Role string `xorm:"not null default '' VARCHAR(128) role"` Content string `xorm:"not null MEDIUMTEXT content"` Helpful int `xorm:"not null default 0 INT(11) helpful"` Unhelpful int `xorm:"not null default 0 INT(11) unhelpful"` } // TableName returns the table name func (AIConversationRecord) TableName() string { return "ai_conversation_record" } ================================================ FILE: internal/entity/answer_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" const ( AnswerSearchOrderByDefault = "default" AnswerSearchOrderByTime = "updated" AnswerSearchOrderByVote = "vote" AnswerSearchOrderByTimeAsc = "created" AnswerStatusAvailable = 1 AnswerStatusDeleted = 10 AnswerStatusPending = 11 ) var AdminAnswerSearchStatus = map[string]int{ "available": AnswerStatusAvailable, "deleted": AnswerStatusDeleted, "pending": AnswerStatusPending, } // Answer answer type Answer struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"` ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` Status int `xorm:"not null default 1 INT(11) status"` Accepted int `xorm:"not null default 1 INT(11) adopted"` CommentCount int `xorm:"not null default 0 INT(11) comment_count"` VoteCount int `xorm:"not null default 0 INT(11) vote_count"` RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` } type AnswerSearch struct { Answer IncludeDeleted bool `json:"include_deleted"` LoginUserID string `json:"login_user_id"` Order string `json:"order_by"` // default or updated Page int `json:"page" form:"page"` // Query number of pages PageSize int `json:"page_size" form:"page_size"` // Search page size } type PersonalAnswerPageQueryCond struct { Page int PageSize int UserID string Order string ShowPending bool } // TableName answer table name func (Answer) TableName() string { return "answer" } ================================================ FILE: internal/entity/api_key_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import ( "time" ) // APIKey entity type APIKey struct { ID int `xorm:"not null pk autoincr INT(11) id"` CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` LastUsedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP last_used_at"` Description string `xorm:"not null MEDIUMTEXT description"` AccessKey string `xorm:"not null unique VARCHAR(255) access_key"` Scope string `xorm:"not null VARCHAR(255) scope"` UserID string `xorm:"not null default 0 BIGINT(20) user_id"` Hidden int `xorm:"not null default 0 INT(11) hidden"` } // TableName category table name func (c *APIKey) TableName() string { return "api_key" } ================================================ FILE: internal/entity/auth_user_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity // UserCacheInfo User Cache Information type UserCacheInfo struct { UserID string `json:"user_id"` UserStatus int `json:"user_status"` EmailStatus int `json:"email_status"` RoleID int `json:"role_id"` ExternalID string `json:"external_id"` VisitToken string `json:"visit_token"` } ================================================ FILE: internal/entity/badge_award_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" const ( IsBadgeNotDeleted = 0 IsBadgeDeleted = 1 BadgeEmptyAwardKey = "0" ) // BadgeAward badge_award type BadgeAward struct { ID string `xorm:"not null pk BIGINT(20) id"` CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` UserID string `xorm:"not null index BIGINT(20) user_id"` BadgeID string `xorm:"not null index BIGINT(20) badge_id"` AwardKey string `xorm:"not null index VARCHAR(64) award_key"` BadgeGroupID int64 `xorm:"not null index BIGINT(20) badge_group_id"` IsBadgeDeleted int8 `xorm:"not null TINYINT(1) is_badge_deleted"` } // TableName badge_award table name func (BadgeAward) TableName() string { return "badge_award" } type BadgeEarnedCount struct { BadgeID string `xorm:"badge_id"` EarnedCount int64 `xorm:"earned_count"` } // TableName badge_award table name func (BadgeEarnedCount) TableName() string { return "badge_award" } type BadgeAwardRecent struct { Created time.Time `xorm:"created"` BadgeID string `xorm:"badge_id"` AwardKey string `xorm:"award_key"` EarnedCount int64 `xorm:"earned_count"` IsBadgeDeleted int8 `xorm:"is_badge_deleted"` } // TableName badge_award table name func (BadgeAwardRecent) TableName() string { return "badge_award" } ================================================ FILE: internal/entity/badge_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import ( "time" "github.com/tidwall/gjson" ) type BadgeLevel int const ( BadgeStatusActive = 1 BadgeStatusDeleted = 10 BadgeStatusInactive = 11 BadgeLevelBronze BadgeLevel = 1 BadgeLevelSilver BadgeLevel = 2 BadgeLevelGold BadgeLevel = 3 BadgeSingleAward = 1 BadgeMultiAward = 2 ) // Badge badge type Badge struct { ID string `xorm:"not null pk BIGINT(20) id"` CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` Name string `xorm:"not null default '' VARCHAR(256) name"` Icon string `xorm:"not null default '' VARCHAR(1024) icon"` AwardCount int `xorm:"not null default 0 INT(11) award_count"` Description string `xorm:"not null MEDIUMTEXT description"` Status int8 `xorm:"not null default 1 INT(11) status"` BadgeGroupID int64 `xorm:"not null default 0 BIGINT(20) badge_group_id"` Level BadgeLevel `xorm:"not null default 1 TINYINT(4) level"` Single int8 `xorm:"not null default 1 TINYINT(4) single"` Collect string `xorm:"not null default '' VARCHAR(128) collect"` Handler string `xorm:"not null default '' VARCHAR(128) handler"` Param string `xorm:"not null TEXT param"` } // TableName badge table name func (b *Badge) TableName() string { return "badge" } func (b *Badge) GetIntParam(key string) int64 { return gjson.Get(b.Param, key).Int() } func (b *Badge) GetStringParam(key string) string { return gjson.Get(b.Param, key).String() } ================================================ FILE: internal/entity/badge_group_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // BadgeGroup badge_group type BadgeGroup struct { ID string `json:"id" xorm:"not null pk autoincr BIGINT(20) id"` Name string `json:"name" xorm:"not null default '' VARCHAR(256) name"` CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` } // TableName badge_group table name func (BadgeGroup) TableName() string { return "badge_group" } ================================================ FILE: internal/entity/captcha_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity const ( CaptchaActionEmail = "email" CaptchaActionPassword = "password" CaptchaActionEditUserinfo = "edit_userinfo" CaptchaActionQuestion = "question" CaptchaActionAnswer = "answer" CaptchaActionComment = "comment" CaptchaActionEdit = "edit" CaptchaActionInvitationAnswer = "invitation_answer" CaptchaActionSearch = "search" CaptchaActionReport = "report" CaptchaActionDelete = "delete" CaptchaActionVote = "vote" ) type ActionRecordInfo struct { LastTime int64 `json:"last_time"` Num int `json:"num"` Config string `json:"config"` } ================================================ FILE: internal/entity/collection_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // Collection collection type Collection struct { ID string `xorm:"not null pk default 0 BIGINT(20) id"` CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` ObjectID string `xorm:"not null default 0 BIGINT(20) object_id"` UserCollectionGroupID string `xorm:"not null default 0 BIGINT(20) user_collection_group_id"` } type CollectionSearch struct { Collection Page int `json:"page" form:"page"` // Query number of pages PageSize int `json:"page_size" form:"page_size"` // Search page size } // TableName collection table name func (Collection) TableName() string { return "collection" } ================================================ FILE: internal/entity/collection_group_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // CollectionGroup collection group type CollectionGroup struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` Name string `xorm:"not null default '' VARCHAR(50) name"` DefaultGroup int `xorm:"not null default 1 INT(11) default_group"` } // TableName collection group table name func (CollectionGroup) TableName() string { return "collection_group" } ================================================ FILE: internal/entity/comment_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import ( "database/sql" "fmt" "time" "github.com/apache/answer/pkg/converter" ) const ( CommentStatusAvailable = 1 CommentStatusDeleted = 10 CommentStatusPending = 11 ) // Comment comment type Comment struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 BIGINT(20) user_id"` ReplyUserID sql.NullInt64 `xorm:"BIGINT(20) reply_user_id"` ReplyCommentID sql.NullInt64 `xorm:"BIGINT(20) reply_comment_id"` ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"` VoteCount int `xorm:"not null default 0 INT(11) vote_count"` Status int `xorm:"not null default 0 TINYINT(4) status"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"` ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` } // TableName comment table name func (c *Comment) TableName() string { return "comment" } // GetReplyUserID get reply user id func (c *Comment) GetReplyUserID() string { if c.ReplyUserID.Valid { return fmt.Sprintf("%d", c.ReplyUserID.Int64) } return "" } // GetReplyCommentID get reply comment id func (c *Comment) GetReplyCommentID() string { if c.ReplyCommentID.Valid { return fmt.Sprintf("%d", c.ReplyCommentID.Int64) } return "" } // SetReplyUserID set reply user id func (c *Comment) SetReplyUserID(str string) { if len(str) > 0 { c.ReplyUserID = sql.NullInt64{Int64: converter.StringToInt64(str), Valid: true} } else { c.ReplyUserID = sql.NullInt64{Valid: false} } } // SetReplyCommentID set reply comment id func (c *Comment) SetReplyCommentID(str string) { if len(str) > 0 { c.ReplyCommentID = sql.NullInt64{Int64: converter.StringToInt64(str), Valid: true} } else { c.ReplyCommentID = sql.NullInt64{Valid: false} } } // GetMentionUsernameList get mention username list func (c *Comment) GetMentionUsernameList() []string { return converter.GetMentionUsernameList(c.OriginalText) } ================================================ FILE: internal/entity/config_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import ( "encoding/json" "github.com/segmentfault/pacman/log" "github.com/apache/answer/pkg/converter" ) // Config config type Config struct { ID int `xorm:"not null pk autoincr INT(11) id"` Key string `xorm:"unique VARCHAR(128) key"` Value string `xorm:"TEXT value"` } // TableName config table name func (c *Config) TableName() string { return "config" } func (c *Config) BuildByJSON(data []byte) { cf := &Config{} _ = json.Unmarshal(data, cf) c.ID = cf.ID c.Key = cf.Key c.Value = cf.Value } func (c *Config) JsonString() string { data, _ := json.Marshal(c) return string(data) } // GetIntValue get int value func (c *Config) GetIntValue() int { if len(c.Value) == 0 { log.Warnf("config value is empty, key: %s, value: %s", c.Key, c.Value) } return converter.StringToInt(c.Value) } // GetArrayStringValue get array string value func (c *Config) GetArrayStringValue() []string { var arr []string _ = json.Unmarshal([]byte(c.Value), &arr) return arr } // GetByteValue get byte value func (c *Config) GetByteValue() []byte { return []byte(c.Value) } ================================================ FILE: internal/entity/file_record_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" const ( FileRecordStatusAvailable = 1 FileRecordStatusDeleted = 10 ) // FileRecord file record type FileRecord struct { ID int `xorm:"not null pk autoincr INT(10) id"` CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP updated TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 BIGINT(20) user_id"` FilePath string `xorm:"not null VARCHAR(256) file_path"` FileURL string `xorm:"not null VARCHAR(1024) file_url"` ObjectID string `xorm:"not null default 0 INDEX BIGINT(20) object_id"` Source string `xorm:"not null VARCHAR(128) source"` Status int `xorm:"not null default 0 TINYINT(4) status"` } // TableName file record table name func (FileRecord) TableName() string { return "file_record" } ================================================ FILE: internal/entity/meta_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" const ( QuestionEditSummaryKey = "question.edit.summary" QuestionCloseReasonKey = "question.close.reason" AnswerEditSummaryKey = "answer.edit.summary" TagEditSummaryKey = "tag.edit.summary" ObjectReactSummaryKey = "object.react.summary" ) // Meta meta type Meta struct { ID int `xorm:"not null pk autoincr INT(10) id"` CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP updated TIMESTAMP updated_at"` ObjectID string `xorm:"not null default 0 INDEX BIGINT(20) object_id"` Key string `xorm:"not null VARCHAR(100) key"` Value string `xorm:"not null MEDIUMTEXT value"` } // TableName meta table name func (Meta) TableName() string { return "meta" } ================================================ FILE: internal/entity/notification_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // Notification notification type Notification struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` ObjectID string `xorm:"not null default 0 INDEX BIGINT(20) object_id"` Content string `xorm:"not null TEXT content"` Type int `xorm:"not null default 0 INT(11) type"` MsgType int `xorm:"not null default 0 INT(11) msg_type"` IsRead int `xorm:"not null default 1 INT(11) is_read"` Status int `xorm:"not null default 1 INT(11) status"` } // TableName notification table name func (Notification) TableName() string { return "notification" } ================================================ FILE: internal/entity/plugin_config_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity // PluginConfig plugin config type PluginConfig struct { ID int `xorm:"not null pk autoincr INT(11) id"` PluginSlugName string `xorm:"unique VARCHAR(128) plugin_slug_name"` Value string `xorm:"TEXT value"` } // TableName config table name func (PluginConfig) TableName() string { return "plugin_config" } ================================================ FILE: internal/entity/plugin_kv_storage_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity type PluginKVStorage struct { ID int `xorm:"not null pk autoincr INT(11) id"` PluginSlugName string `xorm:"not null VARCHAR(128) UNIQUE(uk_psg) plugin_slug_name"` Group string `xorm:"not null VARCHAR(128) UNIQUE(uk_psg) 'group'"` Key string `xorm:"not null VARCHAR(128) UNIQUE(uk_psg) 'key'"` Value string `xorm:"not null TEXT value"` } func (PluginKVStorage) TableName() string { return "plugin_kv_storage" } ================================================ FILE: internal/entity/plugin_user_config_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity // PluginUserConfig plugin config type PluginUserConfig struct { ID int `xorm:"not null pk autoincr INT(11) id"` UserID string `xorm:"not null default 0 BIGINT(20) UNIQUE(uk_up) user_id"` PluginSlugName string `xorm:"VARCHAR(128) UNIQUE(uk_up) plugin_slug_name"` Value string `xorm:"TEXT value"` } // TableName config table name func (PluginUserConfig) TableName() string { return "plugin_user_config" } ================================================ FILE: internal/entity/power_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // Power power type Power struct { ID int `xorm:"not null pk autoincr INT(11) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` Name string `xorm:"not null default '' VARCHAR(50) name"` PowerType string `xorm:"not null default '' VARCHAR(100) power_type"` Description string `xorm:"not null default '' VARCHAR(200) description"` } // TableName power table name func (Power) TableName() string { return "power" } ================================================ FILE: internal/entity/question_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import ( "time" ) const ( QuestionStatusAvailable = 1 QuestionStatusClosed = 2 QuestionStatusDeleted = 10 QuestionStatusPending = 11 QuestionUnPin = 1 QuestionPin = 2 QuestionShow = 1 QuestionHide = 2 ) var AdminQuestionSearchStatus = map[string]int{ "available": QuestionStatusAvailable, "closed": QuestionStatusClosed, "deleted": QuestionStatusDeleted, "pending": QuestionStatusPending, } var AdminQuestionSearchStatusIntToString = map[int]string{ QuestionStatusAvailable: "available", QuestionStatusClosed: "closed", QuestionStatusDeleted: "deleted", QuestionStatusPending: "pending", } // Question question type Question struct { ID string `xorm:"not null pk BIGINT(20) id"` CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` InviteUserID string `xorm:"TEXT invite_user_id"` LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` Title string `xorm:"not null default '' VARCHAR(150) title"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"` ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` Pin int `xorm:"not null default 1 INT(11) pin"` Show int `xorm:"not null default 1 INT(11) show"` Status int `xorm:"not null default 1 INT(11) status"` ViewCount int `xorm:"not null default 0 INT(11) view_count"` UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"` VoteCount int `xorm:"not null default 0 INT(11) vote_count"` AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` HotScore int `xorm:"not null default 0 INT(11) hot_score"` CollectionCount int `xorm:"not null default 0 INT(11) collection_count"` FollowCount int `xorm:"not null default 0 INT(11) follow_count"` AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` LinkedCount int `xorm:"not null default 0 INT(11) linked_count"` } // TableName question table name func (Question) TableName() string { return "question" } // QuestionWithTagsRevision question type QuestionWithTagsRevision struct { Question Tags []*TagSimpleInfoForRevision `json:"tags"` } // TagSimpleInfoForRevision tag simple info for revision type TagSimpleInfoForRevision struct { ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"` MainTagID int64 `xorm:"not null default 0 BIGINT(20) main_tag_id"` MainTagSlugName string `xorm:"not null default '' VARCHAR(35) main_tag_slug_name"` SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"` DisplayName string `xorm:"not null default '' VARCHAR(35) display_name"` Recommend bool `xorm:"not null default false BOOL recommend"` Reserved bool `xorm:"not null default false BOOL reserved"` RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` } ================================================ FILE: internal/entity/question_link_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import ( "time" ) const ( QuestionLinkStatusAvailable = 1 QuestionLinkStatusDeleted = 2 ) type QuestionLink struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` FromQuestionID string `xorm:"not null default 0 BIGINT(20) index from_question_id"` FromAnswerID string `xorm:"BIGINT(20) from_answer_id"` ToQuestionID string `xorm:"not null default 0 BIGINT(20) index to_question_id"` ToAnswerID string `xorm:"BIGINT(20) to_answer_id"` Status int `xorm:"not null default 1 INT(11) status"` } func (QuestionLink) TableName() string { return "question_link" } ================================================ FILE: internal/entity/report_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" const ( ReportStatusPending = 1 ReportStatusCompleted = 2 ReportStatusIgnore = 3 ReportStatusDeleted = 10 ) var ( ReportStatus = map[string]int{ "pending": ReportStatusPending, "completed": ReportStatusCompleted, "deleted": ReportStatusDeleted, } ) // Report report type Report struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` UserID string `xorm:"not null BIGINT(20) user_id"` ObjectID string `xorm:"not null BIGINT(20) object_id"` ReportedUserID string `xorm:"not null default 0 BIGINT(20) reported_user_id"` ObjectType int `xorm:"not null default 0 INT(11) object_type"` ReportType int `xorm:"not null default 0 INT(11) report_type"` Content string `xorm:"not null TEXT content"` FlaggedType int `xorm:"not null default 0 INT(11) flagged_type"` FlaggedContent string `xorm:"TEXT flagged_content"` Status int `xorm:"not null default 1 INT(11) status"` } // TableName report table name func (Report) TableName() string { return "report" } ================================================ FILE: internal/entity/review_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" const ( ReviewStatusPending = 1 ReviewStatusApproved = 2 ReviewStatusRejected = 3 ) // Review review type Review struct { ID int `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` UserID string `xorm:"not null BIGINT(20) user_id"` ObjectID string `xorm:"not null BIGINT(20) object_id"` ObjectType int `xorm:"not null default 0 INT(11) object_type"` ReviewerUserID string `xorm:"not null default 0 BIGINT(20) reviewer_user_id"` Submitter string `xorm:"not null default '' VARCHAR(100) submitter"` Reason string `xorm:"not null TEXT reason"` Status int `xorm:"not null default 0 INT(11) status"` } // TableName review table name func (Review) TableName() string { return "review" } ================================================ FILE: internal/entity/revision_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import ( "time" ) const ( // RevisionNormalStatus this revision is normal RevisionNormalStatus = 0 // RevisionUnreviewedStatus this revision is unreviewed RevisionUnreviewedStatus = 1 // RevisionReviewPassStatus this revision is reviewed and approved by operator RevisionReviewPassStatus = 2 // RevisionReviewRejectStatus this revision is reviewed and rejected by operator RevisionReviewRejectStatus = 3 ) // Revision revision type Revision struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 BIGINT(20) user_id"` ObjectType int `xorm:"not null default 0 INT(11) object_type"` ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` Title string `xorm:"not null default '' VARCHAR(255) title"` Content string `xorm:"not null MEDIUMTEXT content"` Log string `xorm:"VARCHAR(255) log"` Status int `xorm:"not null default 1 INT(11) status"` ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"` } // TableName revision table name func (Revision) TableName() string { return "revision" } ================================================ FILE: internal/entity/role_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // Role role type Role struct { ID int `xorm:"not null pk autoincr INT(11) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` Name string `xorm:"not null default '' VARCHAR(50) name"` Description string `xorm:"not null default '' VARCHAR(200) description"` } // TableName user table name func (Role) TableName() string { return "role" } ================================================ FILE: internal/entity/role_power_rel_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // RolePowerRel role power rel type RolePowerRel struct { ID int `xorm:"not null pk autoincr INT(11) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` RoleID int `xorm:"not null default 0 INT(11) role_id"` PowerType string `xorm:"not null default '' VARCHAR(200) power_type"` } // TableName role power rel table name func (RolePowerRel) TableName() string { return "role_power_rel" } ================================================ FILE: internal/entity/site_info.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // SiteInfo site information setting type SiteInfo struct { ID string `xorm:"not null pk autoincr INT(11) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` Type string `xorm:"not null VARCHAR(64) type"` Content string `xorm:"not null MEDIUMTEXT content"` Status int `xorm:"not null default 1 INT(11) status"` } // TableName table name func (*SiteInfo) TableName() string { return "site_info" } ================================================ FILE: internal/entity/tag_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" const ( TagStatusAvailable = 1 TagStatusDeleted = 10 ) var TagStatusDisplayMapping = map[int]string{ TagStatusAvailable: "available", TagStatusDeleted: "deleted", } // Tag tag type Tag struct { ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` MainTagID int64 `xorm:"not null default 0 BIGINT(20) main_tag_id"` MainTagSlugName string `xorm:"not null default '' VARCHAR(35) main_tag_slug_name"` SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"` DisplayName string `xorm:"not null default '' VARCHAR(35) display_name"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"` ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` FollowCount int `xorm:"not null default 0 INT(11) follow_count"` QuestionCount int `xorm:"not null default 0 INT(11) question_count"` Status int `xorm:"not null default 1 INT(11) status"` Recommend bool `xorm:"not null default false BOOL recommend"` Reserved bool `xorm:"not null default false BOOL reserved"` RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` UserID string `xorm:"not null default 0 BIGINT(20) user_id"` } // TableName tag table name func (Tag) TableName() string { return "tag" } ================================================ FILE: internal/entity/tag_rel_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" const ( TagRelStatusAvailable = 1 TagRelStatusHide = 2 TagRelStatusDeleted = 10 ) // TagRel tag relation type TagRel struct { ID int64 `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` ObjectID string `xorm:"not null INDEX UNIQUE(s) BIGINT(20) object_id"` TagID string `xorm:"not null INDEX UNIQUE(s) BIGINT(20) tag_id"` Status int `xorm:"not null default 1 INT(11) status"` } // TableName tag list table name func (TagRel) TableName() string { return "tag_rel" } ================================================ FILE: internal/entity/uniqid_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity // Uniqid uniqid type Uniqid struct { ID int64 `xorm:"not null pk autoincr BIGINT(20) id"` UniqidType int `xorm:"not null default 0 INT(11) uniqid_type"` } // TableName uniqid table name func (Uniqid) TableName() string { return "uniqid" } ================================================ FILE: internal/entity/user_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" const ( UserStatusAvailable = 1 UserStatusSuspended = 9 UserStatusDeleted = 10 ) const ( EmailStatusAvailable = 1 EmailStatusToBeVerified = 2 ) const ( UserAdminFlag = 1 ) // PermanentSuspensionTime is a fixed time representing permanent suspension (2099-12-31 23:59:59) var PermanentSuspensionTime = time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC) // User user type User struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` SuspendedAt time.Time `xorm:"TIMESTAMP suspended_at"` SuspendedUntil time.Time `xorm:"DATETIME suspended_until"` DeletedAt time.Time `xorm:"TIMESTAMP deleted_at"` LastLoginDate time.Time `xorm:"TIMESTAMP last_login_date"` Username string `xorm:"not null default '' VARCHAR(50) UNIQUE username"` Pass string `xorm:"not null default '' VARCHAR(255) pass"` EMail string `xorm:"not null VARCHAR(100) e_mail"` MailStatus int `xorm:"not null default 2 TINYINT(4) mail_status"` NoticeStatus int `xorm:"not null default 2 INT(11) notice_status"` FollowCount int `xorm:"not null default 0 INT(11) follow_count"` AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` QuestionCount int `xorm:"not null default 0 INT(11) question_count"` Rank int `xorm:"not null default 0 INT(11) rank"` Status int `xorm:"not null default 1 INT(11) status"` AuthorityGroup int `xorm:"not null default 1 INT(11) authority_group"` DisplayName string `xorm:"not null default '' VARCHAR(30) display_name"` Avatar string `xorm:"not null default '' VARCHAR(2048) avatar"` Mobile string `xorm:"not null VARCHAR(20) mobile"` Bio string `xorm:"not null TEXT bio"` BioHTML string `xorm:"not null TEXT bio_html"` Website string `xorm:"not null default '' VARCHAR(255) website"` Location string `xorm:"not null default '' VARCHAR(100) location"` IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"` IsAdmin bool `xorm:"not null default false BOOL is_admin"` Language string `xorm:"not null default '' VARCHAR(100) language"` ColorScheme string `xorm:"not null default '' VARCHAR(100) color_scheme"` } // TableName user table name func (User) TableName() string { return "user" } type UserSearch struct { User Page int `json:"page" form:"page"` // Query number of pages PageSize int `json:"page_size" form:"page_size"` // Search page size } ================================================ FILE: internal/entity/user_external_login_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // UserExternalLogin user external login type UserExternalLogin struct { ID int64 `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 BIGINT(20) user_id"` Provider string `xorm:"not null default '' VARCHAR(100) provider"` ExternalID string `xorm:"not null default '' VARCHAR(128) external_id"` MetaInfo string `xorm:"TEXT meta_info"` } // TableName table name func (UserExternalLogin) TableName() string { return "user_external_login" } ================================================ FILE: internal/entity/user_notification_config_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // UserNotificationConfig user notification config type UserNotificationConfig struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 INDEX UNIQUE(uk_us) BIGINT(20) INDEX user_id"` Source string `xorm:"not null default '' INDEX UNIQUE(uk_us) VARCHAR(64) source"` Channels string `xorm:"not null TEXT channels"` Enabled bool `xorm:"not null default false BOOL enabled"` } // TableName notification table name func (UserNotificationConfig) TableName() string { return "user_notification_config" } ================================================ FILE: internal/entity/user_role_rel_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity import "time" // UserRoleRel role type UserRoleRel struct { ID int `xorm:"not null pk autoincr INT(11) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 BIGINT(20) user_id"` RoleID int `xorm:"not null default 0 INT(11) role_id"` } // TableName user role rel table name func (UserRoleRel) TableName() string { return "user_role_rel" } ================================================ FILE: internal/entity/version_entity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package entity // Version version type Version struct { ID int `xorm:"not null pk autoincr INT(11) id"` VersionNumber int64 `xorm:"not null default 0 INT(11) version_number"` } // TableName config table name func (Version) TableName() string { return "version" } ================================================ FILE: internal/install/install_controller.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package install import ( "encoding/json" "net/http" "os" "path/filepath" "time" "github.com/apache/answer/configs" "github.com/apache/answer/internal/base/conf" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/path" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/cli" "github.com/apache/answer/internal/migrations" "github.com/apache/answer/internal/schema" "github.com/gin-gonic/gin" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" ) // LangOptions get installation language options // @Summary get installation language options // @Description get installation language options // @Tags Lang // @Produce json // @Success 200 {object} handler.RespBody{data=[]translator.LangOption} // @Router /installation/language/options [get] func LangOptions(ctx *gin.Context) { handler.HandleResponse(ctx, nil, translator.LanguageOptions) } // GetLangMapping get installation language config mapping // @Summary get installation language config mapping // @Description get installation language config mapping // @Tags Lang // @Param lang query string true "installation language" // @Produce json // @Success 200 {object} handler.RespBody{} // @Router /installation/language/config [get] func GetLangMapping(ctx *gin.Context) { t, err := translator.NewTranslator(&translator.I18n{BundleDir: path.I18nPath}) if err != nil { handler.HandleResponse(ctx, err, nil) return } lang := ctx.Query("lang") trData, _ := t.Dump(i18n.Language(lang)) var resp map[string]any _ = json.Unmarshal(trData, &resp) handler.HandleResponse(ctx, nil, resp) } // CheckConfigFileAndRedirectToInstallPage if config file not exist try to redirect to install page // @Summary if config file not exist try to redirect to install page // @Description if config file not exist try to redirect to install page // @Tags installation // @Accept json // @Produce json // @Router / [get] func CheckConfigFileAndRedirectToInstallPage(ctx *gin.Context) { if cli.CheckConfigFile(confPath) { ctx.Redirect(http.StatusFound, "/50x") } else { ctx.Redirect(http.StatusFound, "/install") } } // CheckConfigFile check config file if exist when installation // @Summary check config file if exist when installation // @Description check config file if exist when installation // @Tags installation // @Accept json // @Produce json // @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} // @Router /installation/config-file/check [post] func CheckConfigFile(ctx *gin.Context) { resp := &CheckConfigFileResp{} resp.ConfigFileExist = cli.CheckConfigFile(confPath) if !resp.ConfigFileExist { handler.HandleResponse(ctx, nil, resp) return } allConfig, err := conf.ReadConfig(confPath) if err != nil { log.Error(err) err = errors.BadRequest(reason.ReadConfigFailed) handler.HandleResponse(ctx, err, nil) return } resp.DBConnectionSuccess = cli.CheckDBConnection(allConfig.Data.Database) if resp.DBConnectionSuccess { resp.DbTableExist = cli.CheckDBTableExist(allConfig.Data.Database) } handler.HandleResponse(ctx, nil, resp) } // CheckDatabase check database if exist when installation // @Summary check database if exist when installation // @Description check database if exist when installation // @Tags installation // @Accept json // @Produce json // @Param data body install.CheckDatabaseReq true "CheckDatabaseReq" // @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} // @Router /installation/db/check [post] func CheckDatabase(ctx *gin.Context) { req := &CheckDatabaseReq{} if handler.BindAndCheck(ctx, req) { return } resp := &CheckDatabaseResp{} dataConf := &data.Database{ Driver: req.DbType, Connection: req.GetConnection(), } resp.ConnectionSuccess = cli.CheckDBConnection(dataConf) if !resp.ConnectionSuccess { handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) return } handler.HandleResponse(ctx, nil, resp) } // InitEnvironment init environment // @Summary init environment // @Description init environment // @Tags installation // @Accept json // @Produce json // @Param data body install.CheckDatabaseReq true "CheckDatabaseReq" // @Success 200 {object} handler.RespBody{} // @Router /installation/init [post] func InitEnvironment(ctx *gin.Context) { req := &CheckDatabaseReq{} if handler.BindAndCheck(ctx, req) { return } // check config file if exist if cli.CheckConfigFile(confPath) { log.Debug("config file already exists") handler.HandleResponse(ctx, nil, nil) return } if err := cli.InstallConfigFile(confPath); err != nil { handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{ Success: false, CreateConfigFailed: true, DefaultConfig: string(configs.Config), ErrType: schema.ErrTypeAlert.ErrType, }) return } c, err := conf.ReadConfig(confPath) if err != nil { log.Errorf("read config failed %s", err) handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) return } c.Data.Database.Driver = req.DbType c.Data.Database.Connection = req.GetConnection() c.Data.Cache.FilePath = filepath.Join(path.CacheDir, path.DefaultCacheFileName) c.I18n.BundleDir = path.I18nPath c.ServiceConfig.UploadPath = path.UploadFilePath if err := conf.RewriteConfig(confPath, c); err != nil { log.Errorf("rewrite config failed %s", err) handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) return } handler.HandleResponse(ctx, nil, nil) } // InitBaseInfo init base info // @Summary init base info // @Description init base info // @Tags installation // @Accept json // @Produce json // @Param data body install.InitBaseInfoReq true "InitBaseInfoReq" // @Success 200 {object} handler.RespBody{} // @Router /installation/base-info [post] func InitBaseInfo(ctx *gin.Context) { req := &InitBaseInfoReq{} if handler.BindAndCheck(ctx, req) { return } req.FormatSiteUrl() c, err := conf.ReadConfig(confPath) if err != nil { log.Errorf("read config failed %s", err) handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) return } if cli.CheckDBTableExist(c.Data.Database) { log.Warn("database is already initialized") handler.HandleResponse(ctx, nil, nil) return } engine, err := data.NewDB(false, c.Data.Database) if err != nil { log.Errorf("init database failed %s", err) handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), nil) } inputData := &migrations.InitNeedUserInputData{} _ = copier.Copy(inputData, req) if err := migrations.NewMentor(ctx, engine, inputData).InitDB(); err != nil { log.Error("init database error: ", err.Error()) handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), schema.ErrTypeAlert) return } handler.HandleResponse(ctx, nil, nil) go func() { time.Sleep(1 * time.Second) os.Exit(0) }() } ================================================ FILE: internal/install/install_from_env.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package install import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "os" "github.com/gin-gonic/gin" "github.com/tidwall/gjson" ) type Env struct { AutoInstall string `json:"auto_install"` DbType string `json:"db_type"` DbUsername string `json:"db_username"` DbPassword string `json:"db_password"` DbHost string `json:"db_host"` DbName string `json:"db_name"` DbFile string `json:"db_file"` Language string `json:"lang"` SiteName string `json:"site_name"` SiteURL string `json:"site_url"` ContactEmail string `json:"contact_email"` AdminName string `json:"name"` AdminPassword string `json:"password"` AdminEmail string `json:"email"` LoginRequired bool `json:"login_required"` ExternalContentDisplay string `json:"external_content_display"` } func TryToInstallByEnv() (installByEnv bool, err error) { env := loadEnv() if len(env.AutoInstall) == 0 { return false, nil } fmt.Println("[auto-install] try to install by environment variable") return true, initByEnv(env) } func loadEnv() (env *Env) { return &Env{ AutoInstall: os.Getenv("AUTO_INSTALL"), DbType: os.Getenv("DB_TYPE"), DbUsername: os.Getenv("DB_USERNAME"), DbPassword: os.Getenv("DB_PASSWORD"), DbHost: os.Getenv("DB_HOST"), DbName: os.Getenv("DB_NAME"), DbFile: os.Getenv("DB_FILE"), Language: os.Getenv("LANGUAGE"), SiteName: os.Getenv("SITE_NAME"), SiteURL: os.Getenv("SITE_URL"), ContactEmail: os.Getenv("CONTACT_EMAIL"), AdminName: os.Getenv("ADMIN_NAME"), AdminPassword: os.Getenv("ADMIN_PASSWORD"), AdminEmail: os.Getenv("ADMIN_EMAIL"), ExternalContentDisplay: os.Getenv("EXTERNAL_CONTENT_DISPLAY"), } } func initByEnv(env *Env) (err error) { gin.SetMode(gin.TestMode) if err = dbCheck(env); err != nil { return err } if err = initConfigAndDb(env); err != nil { return err } if err = initBaseInfo(env); err != nil { return err } return nil } func dbCheck(env *Env) (err error) { req := &CheckDatabaseReq{ DbType: env.DbType, DbUsername: env.DbUsername, DbPassword: env.DbPassword, DbHost: env.DbHost, DbName: env.DbName, DbFile: env.DbFile, } return requestAPI(req, "POST", "/installation/db/check", CheckDatabase) } func initConfigAndDb(env *Env) (err error) { req := &CheckDatabaseReq{ DbType: env.DbType, DbUsername: env.DbUsername, DbPassword: env.DbPassword, DbHost: env.DbHost, DbName: env.DbName, DbFile: env.DbFile, } return requestAPI(req, "POST", "/installation/init", InitEnvironment) } func initBaseInfo(env *Env) (err error) { req := &InitBaseInfoReq{ Language: env.Language, SiteName: env.SiteName, SiteURL: env.SiteURL, ContactEmail: env.ContactEmail, AdminName: env.AdminName, AdminPassword: env.AdminPassword, AdminEmail: env.AdminEmail, LoginRequired: env.LoginRequired, ExternalContentDisplay: env.ExternalContentDisplay, } return requestAPI(req, "POST", "/installation/base-info", InitBaseInfo) } func requestAPI(req any, method, url string, handlerFunc gin.HandlerFunc) error { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) body, _ := json.Marshal(req) c.Request, _ = http.NewRequest(method, url, bytes.NewBuffer(body)) if method == "POST" { c.Request.Header.Set("Content-Type", "application/json") } handlerFunc(c) if w.Code != http.StatusOK { return errors.New(gjson.Get(w.Body.String(), "msg").String()) } return nil } ================================================ FILE: internal/install/install_main.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package install import ( "fmt" "os" "github.com/apache/answer/internal/base/path" "github.com/apache/answer/internal/base/translator" "github.com/joho/godotenv" ) var ( port string confPath = "" ) func init() { _ = godotenv.Load() port = os.Getenv("INSTALL_PORT") } func Run(configPath string) { confPath = configPath // initialize translator for return internationalization error when installing. _, err := translator.NewTranslator(&translator.I18n{BundleDir: path.I18nPath}) if err != nil { panic(err) } // try to install by env if installByEnv, err := TryToInstallByEnv(); installByEnv && err != nil { fmt.Printf("[auto-install] try to init by env fail: %v\n", err) } installServer := NewInstallHTTPServer() if len(port) == 0 { port = "80" } fmt.Printf("[SUCCESS] answer installation service will run at: http://localhost:%s/install/ \n", port) if err = installServer.Run(":" + port); err != nil { panic(err) } } ================================================ FILE: internal/install/install_req.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package install import ( "fmt" "net/url" "strings" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/dir" "github.com/segmentfault/pacman/errors" "xorm.io/xorm/schemas" ) // CheckConfigFileResp check config file if exist or not response type CheckConfigFileResp struct { ConfigFileExist bool `json:"config_file_exist"` DBConnectionSuccess bool `json:"db_connection_success"` DbTableExist bool `json:"db_table_exist"` } // CheckDatabaseReq check database type CheckDatabaseReq struct { DbType string `validate:"required,oneof=postgres sqlite3 mysql" json:"db_type"` DbUsername string `json:"db_username"` DbPassword string `json:"db_password"` DbHost string `json:"db_host"` DbName string `json:"db_name"` DbFile string `json:"db_file"` Ssl bool `json:"ssl_enabled"` SslMode string `json:"ssl_mode"` SslRootCert string `json:"ssl_root_cert"` SslKey string `json:"ssl_key"` SslCert string `json:"ssl_cert"` } // GetConnection get connection string func (r *CheckDatabaseReq) GetConnection() string { if r.DbType == string(schemas.SQLITE) { return r.DbFile } if r.DbType == string(schemas.MYSQL) { return fmt.Sprintf("%s:%s@tcp(%s)/%s", r.DbUsername, r.DbPassword, r.DbHost, r.DbName) } if r.DbType == string(schemas.POSTGRES) { host, port := parsePgSQLHostPort(r.DbHost) switch { case !r.Ssl: return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", host, port, r.DbUsername, r.DbPassword, r.DbName) case r.SslMode == "require": return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", host, port, r.DbUsername, r.DbPassword, r.DbName, r.SslMode) case r.SslMode == "verify-ca" || r.SslMode == "verify-full": connection := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", host, port, r.DbUsername, r.DbPassword, r.DbName, r.SslMode) if len(r.SslRootCert) > 0 && dir.CheckFileExist(r.SslRootCert) { connection += fmt.Sprintf(" sslrootcert=%s", r.SslRootCert) } if len(r.SslCert) > 0 && dir.CheckFileExist(r.SslCert) { connection += fmt.Sprintf(" sslcert=%s", r.SslCert) } if len(r.SslKey) > 0 && dir.CheckFileExist(r.SslKey) { connection += fmt.Sprintf(" sslkey=%s", r.SslKey) } return connection } } return "" } func parsePgSQLHostPort(dbHost string) (host string, port string) { if strings.Contains(dbHost, ":") { idx := strings.LastIndex(dbHost, ":") host, port = dbHost[:idx], dbHost[idx+1:] } else if len(dbHost) > 0 { host = dbHost } if host == "" { host = "127.0.0.1" } if port == "" { port = "5432" } return host, port } // CheckDatabaseResp check database response type CheckDatabaseResp struct { ConnectionSuccess bool `json:"connection_success"` } // InitEnvironmentResp init environment response type InitEnvironmentResp struct { Success bool `json:"success"` CreateConfigFailed bool `json:"create_config_failed"` DefaultConfig string `json:"default_config"` ErrType string `json:"err_type"` } // InitBaseInfoReq init base info request type InitBaseInfoReq struct { Language string `validate:"required,gt=0,lte=30" json:"lang"` SiteName string `validate:"required,sanitizer,gt=0,lte=30" json:"site_name"` SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"` ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"` AdminName string `validate:"required,gte=2,lte=30" json:"name"` AdminPassword string `validate:"required,gte=8,lte=32" json:"password"` AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"` LoginRequired bool `json:"login_required"` ExternalContentDisplay string `validate:"required,oneof=always_display ask_before_display" json:"external_content_display"` } func (r *InitBaseInfoReq) Check() (errFields []*validator.FormErrorField, err error) { if checker.IsInvalidUsername(r.AdminName) { errField := &validator.FormErrorField{ ErrorField: "name", ErrorMsg: reason.UsernameInvalid, } errFields = append(errFields, errField) return errFields, errors.BadRequest(reason.UsernameInvalid) } return } func (r *InitBaseInfoReq) FormatSiteUrl() { parsedUrl, err := url.Parse(r.SiteURL) if err != nil { return } r.SiteURL = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host) if len(parsedUrl.Path) > 0 { r.SiteURL += parsedUrl.Path r.SiteURL = strings.TrimSuffix(r.SiteURL, "/") } } ================================================ FILE: internal/install/install_server.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package install import ( "embed" "fmt" "io/fs" "net/http" "github.com/apache/answer/configs" "github.com/apache/answer/internal/base/conf" "github.com/apache/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" "gopkg.in/yaml.v3" ) const UIStaticPath = "build/static" type _resource struct { fs embed.FS } // Open to implement the interface by http.FS required func (r *_resource) Open(name string) (fs.File, error) { name = fmt.Sprintf(UIStaticPath+"/%s", name) log.Debugf("open static path %s", name) return r.fs.Open(name) } // NewInstallHTTPServer new install http server. func NewInstallHTTPServer() *gin.Engine { gin.SetMode(gin.ReleaseMode) r := gin.New() c := &conf.AllConfig{} _ = yaml.Unmarshal(configs.Config, c) r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") }) r.StaticFS(c.UI.BaseURL+"/static", http.FS(&_resource{ fs: ui.Build, })) // read default config file and extract ui config installApi := r.Group("") installApi.GET(c.UI.BaseURL+"/", CheckConfigFileAndRedirectToInstallPage) installApi.GET(c.UI.BaseURL+"/install", WebPage) installApi.GET(c.UI.BaseURL+"/50x", WebPage) installApi.GET(c.UI.APIBaseURL+"/installation/language/config", GetLangMapping) installApi.GET(c.UI.APIBaseURL+"/installation/language/options", LangOptions) installApi.POST(c.UI.APIBaseURL+"/installation/db/check", CheckDatabase) installApi.POST(c.UI.APIBaseURL+"/installation/config-file/check", CheckConfigFile) installApi.POST(c.UI.APIBaseURL+"/installation/init", InitEnvironment) installApi.POST(c.UI.APIBaseURL+"/installation/base-info", InitBaseInfo) r.NoRoute(func(ctx *gin.Context) { ctx.Redirect(http.StatusFound, "/50x") }) return r } func WebPage(c *gin.Context) { filePath := "" var file []byte var err error filePath = "build/index.html" c.Header("content-type", "text/html;charset=utf-8") file, err = ui.Build.ReadFile(filePath) if err != nil { log.Error(err) c.Status(http.StatusNotFound) return } c.String(http.StatusOK, string(file)) } ================================================ FILE: internal/migrations/init.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/repo/revision" "github.com/apache/answer/internal/repo/unique" "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/log" "github.com/apache/answer/internal/entity" "golang.org/x/crypto/bcrypt" "xorm.io/xorm" ) type Mentor struct { ctx context.Context engine *xorm.Engine userData *InitNeedUserInputData err error Done bool } func NewMentor(ctx context.Context, engine *xorm.Engine, data *InitNeedUserInputData) *Mentor { return &Mentor{ctx: ctx, engine: engine, userData: data} } type InitNeedUserInputData struct { Language string SiteName string SiteURL string ContactEmail string AdminName string AdminPassword string AdminEmail string LoginRequired bool ExternalContentDisplay string } func (m *Mentor) InitDB() error { m.do("check table exist", m.checkTableExist) m.do("sync table", m.syncTable) m.do("init version table", m.initVersionTable) m.do("init admin user", m.initAdminUser) m.do("init config", m.initConfig) m.do("init default privileges config", m.initDefaultRankPrivileges) m.do("init role", m.initRole) m.do("init power", m.initPower) m.do("init role power rel", m.initRolePowerRel) m.do("init admin user role rel", m.initAdminUserRoleRel) m.do("init site info interface", m.initSiteInfoInterface) m.do("init site info users settings", m.initSiteInfoUsersSettings) m.do("init site info general config", m.initSiteInfoGeneralData) m.do("init site info login config", m.initSiteInfoLoginConfig) m.do("init site info theme config", m.initSiteInfoThemeConfig) m.do("init site info seo config", m.initSiteInfoSEOConfig) m.do("init site info user config", m.initSiteInfoUsersConfig) m.do("init site info privilege rank", m.initSiteInfoPrivilegeRank) m.do("init site info write", m.initSiteInfoAdvanced) m.do("init site info write", m.initSiteInfoQuestions) m.do("init site info write", m.initSiteInfoTags) m.do("init site info security", m.initSiteInfoSecurityConfig) m.do("init default content", m.initDefaultContent) m.do("init default badges", m.initDefaultBadges) m.do("init default ai config", m.initSiteInfoAI) m.do("init default MCP config", m.initSiteInfoMCP) return m.err } func (m *Mentor) do(taskName string, fn func()) { if m.err != nil || m.Done { return } fn() if m.err != nil { m.err = fmt.Errorf("%s failed: %s", taskName, m.err) } } func (m *Mentor) checkTableExist() { m.Done, m.err = m.engine.Context(m.ctx).IsTableExist(&entity.Version{}) if m.Done { fmt.Println("[database] already exists") } } func (m *Mentor) syncTable() { m.err = m.engine.Context(m.ctx).Sync(tables...) } func (m *Mentor) initVersionTable() { _, m.err = m.engine.Context(m.ctx).Insert(&entity.Version{ID: 1, VersionNumber: ExpectedVersion()}) } func (m *Mentor) initAdminUser() { generateFromPassword, _ := bcrypt.GenerateFromPassword([]byte(m.userData.AdminPassword), bcrypt.DefaultCost) _, m.err = m.engine.Context(m.ctx).Insert(&entity.User{ ID: "1", Username: m.userData.AdminName, Pass: string(generateFromPassword), EMail: m.userData.AdminEmail, MailStatus: 1, NoticeStatus: 1, Status: 1, Rank: 1, DisplayName: m.userData.AdminName, }) } func (m *Mentor) initConfig() { _, m.err = m.engine.Context(m.ctx).Insert(defaultConfigTable) } func (m *Mentor) initDefaultRankPrivileges() { chooseOption := schema.DefaultPrivilegeOptions.Choose(schema.PrivilegeLevel2) for _, privilege := range chooseOption.Privileges { _, err := m.engine.Context(m.ctx).Update( &entity.Config{Value: fmt.Sprintf("%d", privilege.Value)}, &entity.Config{Key: privilege.Key}, ) if err != nil { log.Error(err) } } } func (m *Mentor) initRole() { _, m.err = m.engine.Context(m.ctx).Insert(roles) } func (m *Mentor) initPower() { _, m.err = m.engine.Context(m.ctx).Insert(powers) } func (m *Mentor) initRolePowerRel() { _, m.err = m.engine.Context(m.ctx).Insert(rolePowerRels) } func (m *Mentor) initAdminUserRoleRel() { _, m.err = m.engine.Context(m.ctx).Insert(adminUserRoleRel) } func (m *Mentor) initSiteInfoInterface() { now := time.Now() zoneName, offset := now.In(time.Local).Zone() localTimezone := "UTC" for _, tz := range constant.Timezones { loc, err := time.LoadLocation(tz) if err != nil { continue } tzNow := now.In(loc) tzName, tzOffset := tzNow.Zone() if tzName == zoneName && tzOffset == offset { localTimezone = tz break } } interfaceData := map[string]string{ "language": m.userData.Language, "time_zone": localTimezone, } interfaceDataBytes, _ := json.Marshal(interfaceData) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "interface_settings", Content: string(interfaceDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoUsersSettings() { usersSettings := map[string]any{ "default_avatar": "gravatar", "gravatar_base_url": "https://www.gravatar.com/avatar/", } usersSettingsDataBytes, _ := json.Marshal(usersSettings) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "users_settings", Content: string(usersSettingsDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoGeneralData() { generalData := map[string]string{ "name": m.userData.SiteName, "site_url": m.userData.SiteURL, "contact_email": m.userData.ContactEmail, } generalDataBytes, _ := json.Marshal(generalData) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "general", Content: string(generalDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoLoginConfig() { loginConfig := map[string]any{ "allow_new_registrations": true, "allow_email_registrations": true, "allow_password_login": true, } loginConfigDataBytes, _ := json.Marshal(loginConfig) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "login", Content: string(loginConfigDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoSecurityConfig() { securityConfig := map[string]any{ "login_required": m.userData.LoginRequired, "external_content_display": m.userData.ExternalContentDisplay, "check_update": true, } securityConfigDataBytes, _ := json.Marshal(securityConfig) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "security", Content: string(securityConfigDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoThemeConfig() { themeConfig := fmt.Sprintf(`{"theme":"default","theme_config":{"default":{"navbar_style":"#0033ff","primary_color":"#0033ff"}},"layout":"%s"}`, constant.ThemeLayoutFullWidth) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "theme", Content: themeConfig, Status: 1, }) } func (m *Mentor) initSiteInfoSEOConfig() { seoData := map[string]any{ "permalink": constant.PermalinkQuestionID, "robots": defaultSEORobotTxt + m.userData.SiteURL + "/sitemap.xml", } seoDataBytes, _ := json.Marshal(seoData) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "seo", Content: string(seoDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoUsersConfig() { usersData := map[string]any{ "default_avatar": "gravatar", "gravatar_base_url": "https://www.gravatar.com/avatar/", "allow_update_display_name": true, "allow_update_username": true, "allow_update_avatar": true, "allow_update_bio": true, "allow_update_website": true, "allow_update_location": true, } usersDataBytes, _ := json.Marshal(usersData) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "users", Content: string(usersDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoPrivilegeRank() { privilegeRankData := map[string]any{ "level": schema.PrivilegeLevel2, } privilegeRankDataBytes, _ := json.Marshal(privilegeRankData) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "privileges", Content: string(privilegeRankDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoAdvanced() { advancedData := map[string]any{ "max_image_size": 4, "max_attachment_size": 8, "max_image_megapixel": 40, "authorized_image_extensions": []string{"jpg", "jpeg", "png", "gif", "webp"}, "authorized_attachment_extensions": []string{}, } advancedDataBytes, _ := json.Marshal(advancedData) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "advanced", Content: string(advancedDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoQuestions() { questionsData := map[string]any{ "min_tags": 1, "min_content": 6, "restrict_answer": true, } questionsDataBytes, _ := json.Marshal(questionsData) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "questions", Content: string(questionsDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoTags() { tagsData := map[string]any{ "required_tag": false, "recommend_tags": []string{}, "reserved_tags": []string{}, } tagsDataBytes, _ := json.Marshal(tagsData) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "tags", Content: string(tagsDataBytes), Status: 1, }) } func (m *Mentor) initDefaultContent() { uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine}) revisionRepo := revision.NewRevisionRepo(&data.Data{DB: m.engine}, uniqueIDRepo) now := time.Now() tagId, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Tag{}.TableName()) if err != nil { m.err = err return } q1Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Question{}.TableName()) if err != nil { m.err = err return } a1Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Answer{}.TableName()) if err != nil { m.err = err return } q2Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Question{}.TableName()) if err != nil { m.err = err return } a2Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Answer{}.TableName()) if err != nil { m.err = err return } tag := &entity.Tag{ ID: tagId, SlugName: "support", DisplayName: "support", OriginalText: "For general support questions.", ParsedText: "

For general support questions.

", UserID: "1", QuestionCount: 2, Status: entity.TagStatusAvailable, RevisionID: "0", } q1 := &entity.Question{ ID: q1Id, CreatedAt: now, UserID: "1", LastEditUserID: "1", Title: "What is a tag?", OriginalText: "When asking a question, we need to choose tags. What are tags and why should I use them?", ParsedText: "

When asking a question, we need to choose tags. What are tags and why should I use them?

", Pin: entity.QuestionUnPin, Show: entity.QuestionShow, Status: entity.QuestionStatusAvailable, AnswerCount: 1, AcceptedAnswerID: "0", LastAnswerID: a1Id, PostUpdateTime: now, RevisionID: "0", } a1 := &entity.Answer{ ID: a1Id, CreatedAt: now, QuestionID: q1Id, UserID: "1", LastEditUserID: "0", OriginalText: "Tags help to organize content and make searching easier. It helps your question get more attention from people interested in that tag. Tags also send notifications. If you are interested in some topic, follow that tag to get updates.", ParsedText: "

Tags help to organize content and make searching easier. It helps your question get more attention from people interested in that tag. Tags also send notifications. If you are interested in some topic, follow that tag to get updates.

", Status: entity.AnswerStatusAvailable, RevisionID: "0", } q2 := &entity.Question{ ID: q2Id, CreatedAt: now, UserID: "1", LastEditUserID: "1", Title: "What is reputation and how do I earn them?", OriginalText: "I see that each user has reputation points, What is it and how do I earn them?", ParsedText: "

I see that each user has reputation points, What is it and how do I earn them?

", Pin: entity.QuestionUnPin, Show: entity.QuestionShow, Status: entity.QuestionStatusAvailable, AnswerCount: 1, AcceptedAnswerID: "0", LastAnswerID: a2Id, PostUpdateTime: now, RevisionID: "0", } a2 := &entity.Answer{ ID: a2Id, CreatedAt: now, QuestionID: q2Id, UserID: "1", LastEditUserID: "0", OriginalText: "Your reputation points show how much the community values your knowledge. You earn points when someone find your question or answer helpful. You also get points when the person who asked the question thinks you did a good job and accepts your answer.", ParsedText: "

Your reputation points show how much the community values your knowledge. You earn points when someone find your question or answer helpful. You also get points when the person who asked the question thinks you did a good job and accepts your answer.

", Status: entity.AnswerStatusAvailable, RevisionID: "0", } _, m.err = m.engine.Context(m.ctx).Insert(tag) if m.err != nil { return } tagContent, err := json.Marshal(tag) if err != nil { m.err = err return } m.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{ UserID: tag.UserID, ObjectID: tag.ID, Title: tag.SlugName, Content: string(tagContent), Status: entity.RevisionReviewPassStatus, }, true) if m.err != nil { return } tagForRevision := &entity.TagSimpleInfoForRevision{ ID: tag.ID, MainTagID: tag.MainTagID, MainTagSlugName: tag.MainTagSlugName, SlugName: tag.SlugName, DisplayName: tag.DisplayName, Recommend: tag.Recommend, Reserved: tag.Reserved, RevisionID: tag.RevisionID, } _, m.err = m.engine.Context(m.ctx).Insert(q1) if m.err != nil { return } q1Revision := &entity.QuestionWithTagsRevision{ Question: *q1, Tags: []*entity.TagSimpleInfoForRevision{tagForRevision}, } q1Content, err := json.Marshal(q1Revision) if err != nil { m.err = err return } m.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{ UserID: q1.UserID, ObjectID: q1.ID, Title: q1.Title, Content: string(q1Content), Status: entity.RevisionReviewPassStatus, }, true) if m.err != nil { return } _, m.err = m.engine.Context(m.ctx).Insert(a1) if m.err != nil { return } a1Content, err := json.Marshal(a1) if err != nil { m.err = err return } m.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{ UserID: a1.UserID, ObjectID: a1.ID, Content: string(a1Content), Status: entity.RevisionReviewPassStatus, }, true) if m.err != nil { return } _, m.err = m.engine.Context(m.ctx).Insert(entity.TagRel{ ObjectID: q1.ID, TagID: tag.ID, Status: entity.TagRelStatusAvailable, }) if m.err != nil { return } _, m.err = m.engine.Context(m.ctx).Insert(q2) if m.err != nil { return } q2Revision := &entity.QuestionWithTagsRevision{ Question: *q2, Tags: []*entity.TagSimpleInfoForRevision{tagForRevision}, } q2Content, err := json.Marshal(q2Revision) if err != nil { m.err = err return } m.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{ UserID: q2.UserID, ObjectID: q2.ID, Title: q2.Title, Content: string(q2Content), Status: entity.RevisionReviewPassStatus, }, true) if m.err != nil { return } _, m.err = m.engine.Context(m.ctx).Insert(a2) if m.err != nil { return } a2Content, err := json.Marshal(a2) if err != nil { m.err = err return } m.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{ UserID: a2.UserID, ObjectID: a2.ID, Content: string(a2Content), Status: entity.RevisionReviewPassStatus, }, true) if m.err != nil { return } _, m.err = m.engine.Context(m.ctx).Insert(entity.TagRel{ ObjectID: q2.ID, TagID: tag.ID, Status: entity.TagRelStatusAvailable, }) if m.err != nil { return } } func (m *Mentor) initDefaultBadges() { uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine}) _, m.err = m.engine.Context(m.ctx).Insert(defaultBadgeGroupTable) if m.err != nil { return } for _, badge := range defaultBadgeTable { badge.ID, m.err = uniqueIDRepo.GenUniqueIDStr(m.ctx, new(entity.Badge).TableName()) if m.err != nil { return } if _, m.err = m.engine.Context(m.ctx).Insert(badge); m.err != nil { return } } } func (m *Mentor) initSiteInfoAI() { content := &schema.SiteAIReq{ PromptConfig: &schema.AIPromptConfig{ ZhCN: constant.DefaultAIPromptConfigZhCN, EnUS: constant.DefaultAIPromptConfigEnUS, }, } writeDataBytes, _ := json.Marshal(content) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: constant.SiteTypeAI, Content: string(writeDataBytes), Status: 1, }) } func (m *Mentor) initSiteInfoMCP() { content := &schema.SiteMCPReq{ Enabled: true, } writeDataBytes, _ := json.Marshal(content) _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: constant.SiteTypeMCP, Content: string(writeDataBytes), Status: 1, }) } ================================================ FILE: internal/migrations/init_data.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/permission" ) const ( defaultSEORobotTxt = `User-agent: * Disallow: /admin Disallow: /search Disallow: /install Disallow: /review Disallow: /users/login Disallow: /users/register Disallow: /users/account-recovery Disallow: /users/oauth/* Disallow: /users/*/* Disallow: /answer/api Disallow: /*?code* Disallow: /swagger/* Sitemap: ` ) var ( tables = []any{ &entity.Activity{}, &entity.Answer{}, &entity.Collection{}, &entity.CollectionGroup{}, &entity.Comment{}, &entity.Config{}, &entity.Meta{}, &entity.Notification{}, &entity.Question{}, &entity.QuestionLink{}, &entity.Report{}, &entity.Revision{}, &entity.SiteInfo{}, &entity.Tag{}, &entity.TagRel{}, &entity.Uniqid{}, &entity.User{}, &entity.Version{}, &entity.Role{}, &entity.RolePowerRel{}, &entity.Power{}, &entity.UserRoleRel{}, &entity.PluginConfig{}, &entity.UserExternalLogin{}, &entity.UserNotificationConfig{}, &entity.PluginUserConfig{}, &entity.Review{}, &entity.Badge{}, &entity.BadgeGroup{}, &entity.BadgeAward{}, &entity.FileRecord{}, &entity.PluginKVStorage{}, &entity.APIKey{}, &entity.AIConversation{}, &entity.AIConversationRecord{}, } roles = []*entity.Role{ {ID: 1, Name: "User", Description: "Default with no special access."}, {ID: 2, Name: "Admin", Description: "Have the full power to access the site."}, {ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."}, } powers = []*entity.Power{ {ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"}, {ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"}, {ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"}, {ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"}, {ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"}, {ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"}, {ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"}, {ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"}, {ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"}, {ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"}, {ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"}, {ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"}, {ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"}, {ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"}, {ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"}, {ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"}, {ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"}, {ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"}, {ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"}, {ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"}, {ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"}, {ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"}, {ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"}, {ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"}, {ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"}, {ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"}, {ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"}, {ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"}, {ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"}, {ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"}, {ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"}, {ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"}, {ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"}, {ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"}, {ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"}, {ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"}, {ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"}, {ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"}, {ID: 39, Name: "recover answer", PowerType: permission.AnswerUnDelete, Description: "recover deleted answer"}, {ID: 40, Name: "recover question", PowerType: permission.QuestionUnDelete, Description: "recover deleted question"}, {ID: 41, Name: "recover tag", PowerType: permission.TagUnDelete, Description: "recover deleted tag"}, } rolePowerRels = []*entity.RolePowerRel{ {RoleID: 2, PowerType: permission.AdminAccess}, {RoleID: 2, PowerType: permission.QuestionAdd}, {RoleID: 2, PowerType: permission.QuestionEdit}, {RoleID: 2, PowerType: permission.QuestionEditWithoutReview}, {RoleID: 2, PowerType: permission.QuestionDelete}, {RoleID: 2, PowerType: permission.QuestionClose}, {RoleID: 2, PowerType: permission.QuestionReopen}, {RoleID: 2, PowerType: permission.QuestionVoteUp}, {RoleID: 2, PowerType: permission.QuestionVoteDown}, {RoleID: 2, PowerType: permission.AnswerAdd}, {RoleID: 2, PowerType: permission.AnswerEdit}, {RoleID: 2, PowerType: permission.AnswerEditWithoutReview}, {RoleID: 2, PowerType: permission.AnswerDelete}, {RoleID: 2, PowerType: permission.AnswerAccept}, {RoleID: 2, PowerType: permission.AnswerVoteUp}, {RoleID: 2, PowerType: permission.AnswerVoteDown}, {RoleID: 2, PowerType: permission.CommentAdd}, {RoleID: 2, PowerType: permission.CommentEdit}, {RoleID: 2, PowerType: permission.CommentDelete}, {RoleID: 2, PowerType: permission.CommentVoteUp}, {RoleID: 2, PowerType: permission.CommentVoteDown}, {RoleID: 2, PowerType: permission.ReportAdd}, {RoleID: 2, PowerType: permission.TagAdd}, {RoleID: 2, PowerType: permission.TagEdit}, {RoleID: 2, PowerType: permission.TagEditSlugName}, {RoleID: 2, PowerType: permission.TagEditWithoutReview}, {RoleID: 2, PowerType: permission.TagDelete}, {RoleID: 2, PowerType: permission.TagSynonym}, {RoleID: 2, PowerType: permission.LinkUrlLimit}, {RoleID: 2, PowerType: permission.VoteDetail}, {RoleID: 2, PowerType: permission.AnswerAudit}, {RoleID: 2, PowerType: permission.QuestionAudit}, {RoleID: 2, PowerType: permission.TagAudit}, {RoleID: 2, PowerType: permission.TagUseReservedTag}, {RoleID: 2, PowerType: permission.QuestionPin}, {RoleID: 2, PowerType: permission.QuestionHide}, {RoleID: 2, PowerType: permission.QuestionUnPin}, {RoleID: 2, PowerType: permission.QuestionShow}, {RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer}, {RoleID: 2, PowerType: permission.AnswerUnDelete}, {RoleID: 2, PowerType: permission.QuestionUnDelete}, {RoleID: 2, PowerType: permission.TagUnDelete}, {RoleID: 3, PowerType: permission.QuestionAdd}, {RoleID: 3, PowerType: permission.QuestionEdit}, {RoleID: 3, PowerType: permission.QuestionEditWithoutReview}, {RoleID: 3, PowerType: permission.QuestionDelete}, {RoleID: 3, PowerType: permission.QuestionClose}, {RoleID: 3, PowerType: permission.QuestionReopen}, {RoleID: 3, PowerType: permission.QuestionVoteUp}, {RoleID: 3, PowerType: permission.QuestionVoteDown}, {RoleID: 3, PowerType: permission.AnswerAdd}, {RoleID: 3, PowerType: permission.AnswerEdit}, {RoleID: 3, PowerType: permission.AnswerEditWithoutReview}, {RoleID: 3, PowerType: permission.AnswerDelete}, {RoleID: 3, PowerType: permission.AnswerAccept}, {RoleID: 3, PowerType: permission.AnswerVoteUp}, {RoleID: 3, PowerType: permission.AnswerVoteDown}, {RoleID: 3, PowerType: permission.CommentAdd}, {RoleID: 3, PowerType: permission.CommentEdit}, {RoleID: 3, PowerType: permission.CommentDelete}, {RoleID: 3, PowerType: permission.CommentVoteUp}, {RoleID: 3, PowerType: permission.CommentVoteDown}, {RoleID: 3, PowerType: permission.ReportAdd}, {RoleID: 3, PowerType: permission.TagAdd}, {RoleID: 3, PowerType: permission.TagEdit}, {RoleID: 3, PowerType: permission.TagEditSlugName}, {RoleID: 3, PowerType: permission.TagEditWithoutReview}, {RoleID: 3, PowerType: permission.TagDelete}, {RoleID: 3, PowerType: permission.TagSynonym}, {RoleID: 3, PowerType: permission.LinkUrlLimit}, {RoleID: 3, PowerType: permission.VoteDetail}, {RoleID: 3, PowerType: permission.AnswerAudit}, {RoleID: 3, PowerType: permission.QuestionAudit}, {RoleID: 3, PowerType: permission.TagAudit}, {RoleID: 3, PowerType: permission.TagUseReservedTag}, {RoleID: 3, PowerType: permission.QuestionPin}, {RoleID: 3, PowerType: permission.QuestionHide}, {RoleID: 3, PowerType: permission.QuestionUnPin}, {RoleID: 3, PowerType: permission.QuestionShow}, {RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer}, {RoleID: 3, PowerType: permission.AnswerUnDelete}, {RoleID: 3, PowerType: permission.QuestionUnDelete}, {RoleID: 3, PowerType: permission.TagUnDelete}, } adminUserRoleRel = &entity.UserRoleRel{ UserID: "1", RoleID: 2, } defaultConfigTable = []*entity.Config{ {ID: 1, Key: "answer.accepted", Value: `15`}, {ID: 2, Key: "answer.voted_up", Value: `10`}, {ID: 3, Key: "question.voted_up", Value: `10`}, {ID: 4, Key: "tag.edit_accepted", Value: `2`}, {ID: 5, Key: "answer.accept", Value: `2`}, {ID: 6, Key: "answer.voted_down_cancel", Value: `2`}, {ID: 7, Key: "question.voted_down_cancel", Value: `2`}, {ID: 8, Key: "answer.vote_down_cancel", Value: `1`}, {ID: 9, Key: "question.vote_down_cancel", Value: `1`}, {ID: 10, Key: "user.activated", Value: `1`}, {ID: 11, Key: "edit.accepted", Value: `2`}, {ID: 12, Key: "answer.vote_down", Value: `-1`}, {ID: 13, Key: "question.voted_down", Value: `-2`}, {ID: 14, Key: "answer.voted_down", Value: `-2`}, {ID: 15, Key: "answer.accept_cancel", Value: `-2`}, {ID: 16, Key: "answer.deleted", Value: `-5`}, {ID: 17, Key: "question.voted_up_cancel", Value: `-10`}, {ID: 18, Key: "answer.voted_up_cancel", Value: `-10`}, {ID: 19, Key: "answer.accepted_cancel", Value: `-15`}, {ID: 20, Key: "object.reported", Value: `-100`}, {ID: 21, Key: "edit.rejected", Value: `-2`}, {ID: 22, Key: "daily_rank_limit", Value: `200`}, {ID: 23, Key: "daily_rank_limit.exclude", Value: `["answer.accepted"]`}, {ID: 24, Key: "user.follow", Value: `0`}, {ID: 25, Key: "comment.vote_up", Value: `0`}, {ID: 26, Key: "comment.vote_up_cancel", Value: `0`}, {ID: 27, Key: "question.vote_down", Value: `0`}, {ID: 28, Key: "question.vote_up", Value: `0`}, {ID: 29, Key: "question.vote_up_cancel", Value: `0`}, {ID: 30, Key: "answer.vote_up", Value: `0`}, {ID: 31, Key: "answer.vote_up_cancel", Value: `0`}, {ID: 32, Key: "question.follow", Value: `0`}, {ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe"}`}, {ID: 35, Key: "tag.follow", Value: `0`}, {ID: 36, Key: "rank.question.add", Value: `1`}, {ID: 37, Key: "rank.question.edit", Value: `200`}, {ID: 38, Key: "rank.question.delete", Value: `-1`}, {ID: 39, Key: "rank.question.vote_up", Value: `15`}, {ID: 40, Key: "rank.question.vote_down", Value: `125`}, {ID: 41, Key: "rank.answer.add", Value: `1`}, {ID: 42, Key: "rank.answer.edit", Value: `200`}, {ID: 43, Key: "rank.answer.delete", Value: `-1`}, {ID: 44, Key: "rank.answer.accept", Value: `-1`}, {ID: 45, Key: "rank.answer.vote_up", Value: `15`}, {ID: 46, Key: "rank.answer.vote_down", Value: `125`}, {ID: 47, Key: "rank.comment.add", Value: `1`}, {ID: 48, Key: "rank.comment.edit", Value: `-1`}, {ID: 49, Key: "rank.comment.delete", Value: `-1`}, {ID: 50, Key: "rank.report.add", Value: `1`}, {ID: 51, Key: "rank.tag.add", Value: `1500`}, {ID: 52, Key: "rank.tag.edit", Value: `100`}, {ID: 53, Key: "rank.tag.delete", Value: `-1`}, {ID: 54, Key: "rank.tag.synonym", Value: `20000`}, {ID: 55, Key: "rank.link.url_limit", Value: `10`}, {ID: 56, Key: "rank.vote.detail", Value: `0`}, {ID: 57, Key: "reason.spam", Value: `{"name":"spam","description":"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."}`}, {ID: 58, Key: "reason.rude_or_abusive", Value: `{"name":"rude or abusive","description":"A reasonable person would find this content inappropriate for respectful discourse."}`}, {ID: 59, Key: "reason.something", Value: `{"name":"something else","description":"This post requires staff attention for another reason not listed above.","content_type":"textarea"}`}, {ID: 60, Key: "reason.a_duplicate", Value: `{"name":"a duplicate","description":"This question has been asked before and already has an answer.","content_type":"text"}`}, {ID: 61, Key: "reason.not_a_answer", Value: `{"name":"not a answer","description":"This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether.","content_type":""}`}, {ID: 62, Key: "reason.no_longer_needed", Value: `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`}, {ID: 63, Key: "reason.community_specific", Value: `{"name":"a community-specific reason","description":"This question doesn't meet a community guideline."}`}, {ID: 64, Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only."}`}, {ID: 65, Key: "reason.normal", Value: `{"name":"normal","description":"A normal post available to everyone."}`}, {ID: 66, Key: "reason.normal.user", Value: `{"name":"normal","description":"A normal user can ask and answer questions."}`}, {ID: 67, Key: "reason.closed", Value: `{"name":"closed","description":"A closed question can't answer, but still can edit, vote and comment."}`}, {ID: 68, Key: "reason.deleted", Value: `{"name":"deleted","description":"All reputation gained and lost will be restored."}`}, {ID: 69, Key: "reason.deleted.user", Value: `{"name":"deleted","description":"Delete profile, authentication associations."}`}, {ID: 70, Key: "reason.suspended", Value: `{"name":"suspended","description":"A suspended user can't log in."}`}, {ID: 71, Key: "reason.inactive", Value: `{"name":"inactive","description":"An inactive user must re-validate their email."}`}, {ID: 72, Key: "reason.looks_ok", Value: `{"name":"looks ok","description":"This post is good as-is and not low quality."}`}, {ID: 73, Key: "reason.needs_edit", Value: `{"name":"needs edit, and I did it","description":"Improve and correct problems with this post yourself."}`}, {ID: 74, Key: "reason.needs_close", Value: `{"name":"needs close","description":"A closed question can't answer, but still can edit, vote and comment."}`}, {ID: 75, Key: "reason.needs_delete", Value: `{"name":"needs delete","description":"All reputation gained and lost will be restored."}`}, {ID: 76, Key: "question.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.a_duplicate"]`}, {ID: 77, Key: "answer.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.not_a_answer"]`}, {ID: 78, Key: "comment.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.no_longer_needed"]`}, {ID: 79, Key: "question.close.reasons", Value: `["reason.a_duplicate","reason.community_specific","reason.not_clarity","reason.something"]`}, {ID: 80, Key: "question.status.reasons", Value: `["reason.normal","reason.closed","reason.deleted"]`}, {ID: 81, Key: "answer.status.reasons", Value: `["reason.normal","reason.deleted"]`}, {ID: 82, Key: "comment.status.reasons", Value: `["reason.normal","reason.deleted"]`}, {ID: 83, Key: "user.status.reasons", Value: `["reason.normal.user","reason.suspended","reason.deleted.user","reason.inactive"]`}, {ID: 84, Key: "question.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`}, {ID: 85, Key: "answer.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, {ID: 86, Key: "comment.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, {ID: 87, Key: "question.asked", Value: `0`}, {ID: 88, Key: "question.closed", Value: `0`}, {ID: 89, Key: "question.reopened", Value: `0`}, {ID: 90, Key: "question.answered", Value: `0`}, {ID: 91, Key: "question.commented", Value: `0`}, {ID: 92, Key: "question.accept", Value: `0`}, {ID: 93, Key: "question.edited", Value: `0`}, {ID: 94, Key: "question.rollback", Value: `0`}, {ID: 95, Key: "question.deleted", Value: `0`}, {ID: 96, Key: "question.undeleted", Value: `0`}, {ID: 97, Key: "answer.answered", Value: `0`}, {ID: 98, Key: "answer.commented", Value: `0`}, {ID: 99, Key: "answer.edited", Value: `0`}, {ID: 100, Key: "answer.rollback", Value: `0`}, {ID: 101, Key: "answer.undeleted", Value: `0`}, {ID: 102, Key: "tag.created", Value: `0`}, {ID: 103, Key: "tag.edited", Value: `0`}, {ID: 104, Key: "tag.rollback", Value: `0`}, {ID: 105, Key: "tag.deleted", Value: `0`}, {ID: 106, Key: "tag.undeleted", Value: `0`}, {ID: 107, Key: "rank.comment.vote_up", Value: `1`}, {ID: 108, Key: "rank.comment.vote_down", Value: `1`}, {ID: 109, Key: "rank.question.edit_without_review", Value: `2000`}, {ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`}, {ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`}, {ID: 112, Key: "rank.answer.audit", Value: `2000`}, {ID: 113, Key: "rank.question.audit", Value: `2000`}, {ID: 114, Key: "rank.tag.audit", Value: `20000`}, {ID: 115, Key: "rank.question.close", Value: `-1`}, {ID: 116, Key: "rank.question.reopen", Value: `-1`}, {ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`}, {ID: 118, Key: "plugin.status", Value: `{}`}, {ID: 119, Key: "question.pin", Value: `0`}, {ID: 120, Key: "question.unpin", Value: `0`}, {ID: 121, Key: "question.show", Value: `0`}, {ID: 122, Key: "question.hide", Value: `0`}, {ID: 123, Key: "rank.question.pin", Value: `-1`}, {ID: 124, Key: "rank.question.unpin", Value: `-1`}, {ID: 125, Key: "rank.question.show", Value: `-1`}, {ID: 126, Key: "rank.question.hide", Value: `-1`}, {ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`}, {ID: 128, Key: "rank.answer.undeleted", Value: `-1`}, {ID: 129, Key: "rank.question.undeleted", Value: `-1`}, {ID: 130, Key: "rank.tag.undeleted", Value: `-1`}, {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"}]`}, } defaultBadgeGroupTable = []*entity.BadgeGroup{ {ID: "1", Name: "badge.default_badge_groups.getting_started.name"}, {ID: "2", Name: "badge.default_badge_groups.community.name"}, {ID: "3", Name: "badge.default_badge_groups.posting.name"}, } defaultBadgeTable = []*entity.Badge{ { Name: "badge.default_badges.autobiographer.name", Icon: "person-badge-fill", Description: "badge.default_badges.autobiographer.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Handler: "FirstUpdateUserProfile", }, { Name: "badge.default_badges.editor.name", Icon: "pencil-fill", Description: "badge.default_badges.editor.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Handler: "FirstPostEdit", }, { Name: "badge.default_badges.first_flag.name", Icon: "flag-fill", Description: "badge.default_badges.first_flag.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Handler: "FirstFlaggedPost", }, { Name: "badge.default_badges.first_upvote.name", Icon: "hand-thumbs-up-fill", Description: "badge.default_badges.first_upvote.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Handler: "FirstVotedPost", }, { Name: "badge.default_badges.first_reaction.name", Icon: "emoji-smile-fill", Description: "badge.default_badges.first_reaction.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Handler: "FirstReactedPost", }, { Name: "badge.default_badges.first_share.name", Icon: "share-fill", Description: "badge.default_badges.first_share.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Handler: "FirstSharedPost", }, { Name: "badge.default_badges.scholar.name", Icon: "check-circle-fill", Description: "badge.default_badges.scholar.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Handler: "FirstAcceptAnswer", }, { Name: "badge.default_badges.solved.name", Icon: "check-square-fill", Description: "badge.default_badges.solved.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 2, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Handler: "ReachAnswerAcceptedAmount", Param: `{"amount":"1"}`, }, { Name: "badge.default_badges.nice_answer.name", Icon: "chat-square-text-fill", Description: "badge.default_badges.nice_answer.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 3, Level: entity.BadgeLevelBronze, Single: entity.BadgeMultiAward, Handler: "ReachAnswerVote", Param: `{"amount":"10"}`, }, { Name: "badge.default_badges.good_answer.name", Icon: "chat-square-text-fill", Description: "badge.default_badges.good_answer.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 3, Level: entity.BadgeLevelSilver, Single: entity.BadgeMultiAward, Handler: "ReachAnswerVote", Param: `{"amount":"25"}`, }, { Name: "badge.default_badges.great_answer.name", Icon: "chat-square-text-fill", Description: "badge.default_badges.great_answer.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 3, Level: entity.BadgeLevelGold, Single: entity.BadgeMultiAward, Handler: "ReachAnswerVote", Param: `{"amount":"50"}`, }, { Name: "badge.default_badges.nice_question.name", Icon: "question-circle-fill", Description: "badge.default_badges.nice_question.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 3, Level: entity.BadgeLevelBronze, Single: entity.BadgeMultiAward, Handler: "ReachQuestionVote", Param: `{"amount":"10"}`, }, { Name: "badge.default_badges.good_question.name", Icon: "question-circle-fill", Description: "badge.default_badges.good_question.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 3, Level: entity.BadgeLevelSilver, Single: entity.BadgeMultiAward, Handler: "ReachQuestionVote", Param: `{"amount":"25"}`, }, { Name: "badge.default_badges.great_question.name", Icon: "question-circle-fill", Description: "badge.default_badges.great_question.desc", Status: entity.BadgeStatusActive, BadgeGroupID: 3, Level: entity.BadgeLevelGold, Single: entity.BadgeMultiAward, Handler: "ReachQuestionVote", Param: `{"amount":"50"}`, }, } ) ================================================ FILE: internal/migrations/migrations.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) const minDBVersion = 0 // Migration describes on migration from lower version to high version type Migration interface { Version() string Description() string Migrate(ctx context.Context, x *xorm.Engine) error ShouldCleanCache() bool } type migration struct { version string description string migrate func(ctx context.Context, x *xorm.Engine) error shouldCleanCache bool } // Version returns the migration's version func (m *migration) Version() string { return m.version } // Description returns the migration's description func (m *migration) Description() string { return m.description } // Migrate executes the migration func (m *migration) Migrate(ctx context.Context, x *xorm.Engine) error { return m.migrate(ctx, x) } // ShouldCleanCache should clean the cache func (m *migration) ShouldCleanCache() bool { return m.shouldCleanCache } // NewMigration creates a new migration func NewMigration(version, desc string, fn func(ctx context.Context, x *xorm.Engine) error, shouldCleanCache bool) Migration { return &migration{version: version, description: desc, migrate: fn, shouldCleanCache: shouldCleanCache} } // Use noopMigration when there is a migration that has been no-oped var noopMigration = func(_ context.Context, _ *xorm.Engine) error { return nil } var migrations = []Migration{ // 0->1 NewMigration("v0.0.1", "this is first version, no operation", noopMigration, false), NewMigration("v0.3.0", "add user language", addUserLanguage, false), NewMigration("v0.4.1", "add recommend and reserved tag fields", addTagRecommendedAndReserved, false), NewMigration("v0.5.0", "add activity timeline", addActivityTimeline, false), NewMigration("v0.6.0", "add user role", addRoleFeatures, false), NewMigration("v1.0.0", "add theme and private mode", addThemeAndPrivateMode, true), NewMigration("v1.0.2", "add new answer notification", addNewAnswerNotification, true), NewMigration("v1.0.5", "add plugin", addPlugin, false), NewMigration("v1.0.7", "add user pin hide features", addRolePinAndHideFeatures, true), NewMigration("v1.0.8", "update accept answer rank", updateAcceptAnswerRank, true), NewMigration("v1.0.9", "add login limitations", addLoginLimitations, true), NewMigration("v1.1.0-beta.1", "update user pin hide features", updateRolePinAndHideFeatures, true), NewMigration("v1.1.0-beta.2", "update question post time", updateQuestionPostTime, true), NewMigration("v1.1.0", "add gravatar base url", updateCount, true), NewMigration("v1.1.1", "update the length of revision content", updateTheLengthOfRevisionContent, false), NewMigration("v1.1.2", "add notification config", addNoticeConfig, true), NewMigration("v1.1.3", "set default user notification config", setDefaultUserNotificationConfig, false), NewMigration("v1.2.0", "add recover answer permission", addRecoverPermission, true), NewMigration("v1.2.1", "add password login control", addPasswordLoginControl, true), NewMigration("v1.2.5", "add notification plugin and theme config", addNotificationPluginAndThemeConfig, true), NewMigration("v1.3.0", "add review", addReview, false), NewMigration("v1.3.6", "add hot score to question table", addQuestionHotScore, true), NewMigration("v1.4.0", "add badge/badge_group/badge_award table", addBadges, true), NewMigration("v1.4.1", "add question link", addQuestionLink, true), NewMigration("v1.4.2", "add the number of question links", addQuestionLinkedCount, true), NewMigration("v1.4.5", "add file record", addFileRecord, true), NewMigration("v1.5.1", "add plugin kv storage", addPluginKVStorage, true), NewMigration("v1.6.0", "move user config to interface", moveUserConfigToInterface, true), NewMigration("v1.7.0", "add optional tags", addOptionalTags, true), NewMigration("v1.7.2", "expand avatar column length", expandAvatarColumnLength, false), NewMigration("v1.8.0", "change admin menu", updateAdminMenuSettings, true), NewMigration("v1.8.1", "ai feat", aiFeat, true), } func GetMigrations() []Migration { return migrations } // GetCurrentDBVersion returns the current db version func GetCurrentDBVersion(engine *xorm.Engine) (int64, error) { if err := engine.Sync(new(entity.Version)); err != nil { return -1, fmt.Errorf("sync version failed: %v", err) } currentVersion := &entity.Version{ID: 1} has, err := engine.Get(currentVersion) if err != nil { return -1, fmt.Errorf("get first version failed: %v", err) } if !has { _, err := engine.InsertOne(&entity.Version{ID: 1, VersionNumber: 0}) if err != nil { return -1, fmt.Errorf("insert first version failed: %v", err) } return 0, nil } return currentVersion.VersionNumber, nil } // ExpectedVersion returns the expected db version func ExpectedVersion() int64 { return int64(minDBVersion + len(migrations)) } // Migrate database to current version func Migrate(debug bool, dbConf *data.Database, cacheConf *data.CacheConf, upgradeToSpecificVersion string) error { cache, cacheCleanup, err := data.NewCache(cacheConf) if err != nil { fmt.Println("new cache failed:", err.Error()) } engine, err := data.NewDB(debug, dbConf) if err != nil { fmt.Println("new database failed: ", err.Error()) return err } defer func() { _ = engine.Close() }() currentDBVersion, err := GetCurrentDBVersion(engine) if err != nil { return err } expectedVersion := ExpectedVersion() if len(upgradeToSpecificVersion) > 0 { fmt.Printf("[migrate] user set upgrade to version: %s\n", upgradeToSpecificVersion) for i, m := range migrations { if m.Version() == upgradeToSpecificVersion { currentDBVersion = int64(i) break } } } for currentDBVersion < expectedVersion { fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n", currentDBVersion, currentDBVersion+1, expectedVersion) migrationFunc := migrations[currentDBVersion] fmt.Printf("[migrate] try to migrate Answer version %s, description: %s\n", migrationFunc.Version(), migrationFunc.Description()) if err := migrationFunc.Migrate(context.Background(), engine); err != nil { fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error()) return err } if migrationFunc.ShouldCleanCache() { if err := cache.Flush(context.Background()); err != nil { fmt.Printf("[migrate] flush cache failed: %s\n", err.Error()) } } fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1) if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil { fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error()) return err } currentDBVersion++ } if cache != nil { cacheCleanup() } return nil } ================================================ FILE: internal/migrations/v1.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "xorm.io/xorm" ) func addUserLanguage(ctx context.Context, x *xorm.Engine) error { type User struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` Username string `xorm:"not null default '' VARCHAR(50) UNIQUE username"` Language string `xorm:"not null default '' VARCHAR(100) language"` } return x.Context(ctx).Sync(new(User)) } ================================================ FILE: internal/migrations/v10.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/tidwall/gjson" "xorm.io/xorm" ) func addLoginLimitations(ctx context.Context, x *xorm.Engine) error { loginSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeLogin, } exist, err := x.Context(ctx).Get(loginSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { content := &schema.SiteLoginReq{} _ = json.Unmarshal([]byte(loginSiteInfo.Content), content) content.AllowEmailRegistrations = true content.AllowEmailDomains = make([]string, 0) data, _ := json.Marshal(content) loginSiteInfo.Content = string(data) _, err = x.Context(ctx).ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo) if err != nil { return fmt.Errorf("update site info failed: %w", err) } } interfaceSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeInterface, } exist, err = x.Context(ctx).Get(interfaceSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } siteUsers := &schema.SiteUsersReq{ AllowUpdateDisplayName: true, AllowUpdateUsername: true, AllowUpdateAvatar: true, AllowUpdateBio: true, AllowUpdateWebsite: true, AllowUpdateLocation: true, } if exist { siteUsers.DefaultAvatar = gjson.Get(interfaceSiteInfo.Content, "default_avatar").String() } data, _ := json.Marshal(siteUsers) exist, err = x.Context(ctx).Get(&entity.SiteInfo{Type: constant.SiteTypeUsers}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if !exist { usersSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeUsers, Content: string(data), Status: 1, } _, err = x.Context(ctx).Insert(usersSiteInfo) if err != nil { return fmt.Errorf("insert site info failed: %w", err) } } return nil } ================================================ FILE: internal/migrations/v11.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) func updateRolePinAndHideFeatures(ctx context.Context, x *xorm.Engine) error { defaultConfigTable := []*entity.Config{ {ID: 119, Key: "question.pin", Value: `0`}, {ID: 120, Key: "question.unpin", Value: `0`}, {ID: 121, Key: "question.show", Value: `0`}, {ID: 122, Key: "question.hide", Value: `0`}, {ID: 123, Key: "rank.question.pin", Value: `-1`}, {ID: 124, Key: "rank.question.unpin", Value: `-1`}, {ID: 125, Key: "rank.question.show", Value: `-1`}, {ID: 126, Key: "rank.question.hide", Value: `-1`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil { log.Errorf("update %+v config failed: %s", c, err) return fmt.Errorf("update config failed: %w", err) } continue } if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { log.Errorf("insert %+v config failed: %s", c, err) return fmt.Errorf("add config failed: %w", err) } } return nil } ================================================ FILE: internal/migrations/v12.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "time" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) type QuestionPostTime struct { ID string `xorm:"not null pk BIGINT(20) id"` CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` Title string `xorm:"not null default '' VARCHAR(150) title"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"` ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` Status int `xorm:"not null default 1 INT(11) status"` Pin int `xorm:"not null default 1 INT(11) pin"` Show int `xorm:"not null default 1 INT(11) show"` ViewCount int `xorm:"not null default 0 INT(11) view_count"` UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"` VoteCount int `xorm:"not null default 0 INT(11) vote_count"` AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` CollectionCount int `xorm:"not null default 0 INT(11) collection_count"` FollowCount int `xorm:"not null default 0 INT(11) follow_count"` AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` } func (QuestionPostTime) TableName() string { return "question" } func updateQuestionPostTime(ctx context.Context, x *xorm.Engine) error { questionList := make([]QuestionPostTime, 0) err := x.Context(ctx).Find(&questionList, &entity.Question{}) if err != nil { return fmt.Errorf("get questions failed: %w", err) } for _, item := range questionList { if item.PostUpdateTime.IsZero() { if !item.UpdatedAt.IsZero() { item.PostUpdateTime = item.UpdatedAt } else if !item.CreatedAt.IsZero() { item.PostUpdateTime = item.CreatedAt } if _, err = x.Context(ctx).Update(item, &QuestionPostTime{ID: item.ID}); err != nil { log.Errorf("update %+v config failed: %s", item, err) return fmt.Errorf("update question failed: %w", err) } } } return nil } ================================================ FILE: internal/migrations/v13.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "time" "xorm.io/builder" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/permission" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) func updateCount(ctx context.Context, x *xorm.Engine) error { fns := []func(ctx context.Context, x *xorm.Engine) error{ inviteAnswer, addPrivilegeForInviteSomeoneToAnswer, addGravatarBaseURL, updateQuestionCount, updateTagCount, updateUserQuestionCount, updateUserAnswerCount, inBoxData, } for _, fn := range fns { if err := fn(ctx, x); err != nil { return err } } return nil } func addGravatarBaseURL(ctx context.Context, x *xorm.Engine) error { usersSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeUsers, } exist, err := x.Context(ctx).Get(usersSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { content := &schema.SiteUsersReq{} _ = json.Unmarshal([]byte(usersSiteInfo.Content), content) content.GravatarBaseURL = "https://www.gravatar.com/avatar/" data, _ := json.Marshal(content) usersSiteInfo.Content = string(data) _, err = x.Context(ctx).ID(usersSiteInfo.ID).Cols("content").Update(usersSiteInfo) if err != nil { return fmt.Errorf("update site info failed: %w", err) } } return nil } func addPrivilegeForInviteSomeoneToAnswer(ctx context.Context, x *xorm.Engine) error { // add rank for invite to answer powers := []*entity.Power{ {ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"}, } for _, power := range powers { exist, err := x.Context(ctx).Get(&entity.Power{PowerType: power.PowerType}) if err != nil { return err } if exist { _, err = x.Context(ctx).ID(power.ID).Update(power) } else { _, err = x.Context(ctx).Insert(power) } if err != nil { return err } } rolePowerRels := []*entity.RolePowerRel{ {RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer}, {RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer}, } for _, rel := range rolePowerRels { exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) if err != nil { return err } if exist { continue } _, err = x.Context(ctx).Insert(rel) if err != nil { return err } } defaultConfigTable := []*entity.Config{ {ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil { return fmt.Errorf("update config failed: %w", err) } continue } if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { return fmt.Errorf("add config failed: %w", err) } } return nil } func updateQuestionCount(ctx context.Context, x *xorm.Engine) error { // question answer count answers := make([]AnswerV13, 0) err := x.Context(ctx).Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable}) if err != nil { return fmt.Errorf("get answers failed: %w", err) } questionAnswerCount := make(map[string]int) for _, answer := range answers { _, ok := questionAnswerCount[answer.QuestionID] if !ok { questionAnswerCount[answer.QuestionID] = 1 } else { questionAnswerCount[answer.QuestionID]++ } } questionList := make([]QuestionV13, 0) err = x.Context(ctx).Find(&questionList, &QuestionV13{}) if err != nil { return fmt.Errorf("get questions failed: %w", err) } for _, item := range questionList { _, ok := questionAnswerCount[item.ID] if ok { item.AnswerCount = questionAnswerCount[item.ID] if _, err = x.Context(ctx).Cols("answer_count").Update(item, &QuestionV13{ID: item.ID}); err != nil { log.Errorf("update %+v config failed: %s", item, err) return fmt.Errorf("update question failed: %w", err) } } } return nil } // updateTagCount update tag count func updateTagCount(ctx context.Context, x *xorm.Engine) error { tagRelList := make([]entity.TagRel, 0) err := x.Context(ctx).Find(&tagRelList, &entity.TagRel{}) if err != nil { return fmt.Errorf("get tag rel failed: %w", err) } questionIDs := make([]string, 0) questionsAvailableMap := make(map[string]bool) questionsHideMap := make(map[string]bool) for _, item := range tagRelList { questionIDs = append(questionIDs, item.ObjectID) questionsAvailableMap[item.ObjectID] = false questionsHideMap[item.ObjectID] = false } questionList := make([]QuestionV13, 0) err = x.Context(ctx).In("id", questionIDs).And(builder.Lt{"question.status": entity.QuestionStatusDeleted}).Find(&questionList, &QuestionV13{}) if err != nil { return fmt.Errorf("get questions failed: %w", err) } for _, question := range questionList { _, ok := questionsAvailableMap[question.ID] if ok { questionsAvailableMap[question.ID] = true if question.Show == entity.QuestionHide { questionsHideMap[question.ID] = true } } } for id, ok := range questionsHideMap { if ok { if _, err = x.Context(ctx).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}, &entity.TagRel{ObjectID: id}); err != nil { log.Errorf("update %+v config failed: %s", id, err) } } } for id, ok := range questionsAvailableMap { if !ok { if _, err = x.Context(ctx).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}, &entity.TagRel{ObjectID: id}); err != nil { log.Errorf("update %+v config failed: %s", id, err) } } } // select tag count newTagRelList := make([]entity.TagRel, 0) err = x.Context(ctx).Find(&newTagRelList, &entity.TagRel{Status: entity.TagRelStatusAvailable}) if err != nil { return fmt.Errorf("get tag rel failed: %w", err) } tagCountMap := make(map[string]int) for _, v := range newTagRelList { _, ok := tagCountMap[v.TagID] if !ok { tagCountMap[v.TagID] = 1 } else { tagCountMap[v.TagID]++ } } TagList := make([]entity.Tag, 0) err = x.Context(ctx).Find(&TagList, &entity.Tag{}) if err != nil { return fmt.Errorf("get tag failed: %w", err) } for _, tag := range TagList { _, ok := tagCountMap[tag.ID] if ok { tag.QuestionCount = tagCountMap[tag.ID] if _, err = x.Context(ctx).Update(tag, &entity.Tag{ID: tag.ID}); err != nil { log.Errorf("update %+v tag failed: %s", tag.ID, err) return fmt.Errorf("update tag failed: %w", err) } } else { tag.QuestionCount = 0 if _, err = x.Context(ctx).Cols("question_count").Update(tag, &entity.Tag{ID: tag.ID}); err != nil { log.Errorf("update %+v tag failed: %s", tag.ID, err) return fmt.Errorf("update tag failed: %w", err) } } } return nil } // updateUserQuestionCount update user question count func updateUserQuestionCount(ctx context.Context, x *xorm.Engine) error { questionList := make([]QuestionV13, 0) err := x.Context(ctx).Where(builder.Lt{"status": entity.QuestionStatusDeleted}).Find(&questionList, &QuestionV13{}) if err != nil { return fmt.Errorf("get question failed: %w", err) } userQuestionCountMap := make(map[string]int) for _, question := range questionList { _, ok := userQuestionCountMap[question.UserID] if !ok { userQuestionCountMap[question.UserID] = 1 } else { userQuestionCountMap[question.UserID]++ } } userList := make([]entity.User, 0) err = x.Context(ctx).Find(&userList, &entity.User{}) if err != nil { return fmt.Errorf("get user failed: %w", err) } for _, user := range userList { _, ok := userQuestionCountMap[user.ID] if ok { user.QuestionCount = userQuestionCountMap[user.ID] if _, err = x.Context(ctx).Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil { log.Errorf("update %+v user failed: %s", user.ID, err) return fmt.Errorf("update user failed: %w", err) } } else { user.QuestionCount = 0 if _, err = x.Context(ctx).Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil { log.Errorf("update %+v user failed: %s", user.ID, err) return fmt.Errorf("update user failed: %w", err) } } } return nil } type AnswerV13 struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` Status int `xorm:"not null default 1 INT(11) status"` Accepted int `xorm:"not null default 1 INT(11) adopted"` } func (AnswerV13) TableName() string { return "answer" } // updateUserAnswerCount update user answer count func updateUserAnswerCount(ctx context.Context, x *xorm.Engine) error { answers := make([]AnswerV13, 0) err := x.Context(ctx).Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable}) if err != nil { return fmt.Errorf("get answers failed: %w", err) } userAnswerCount := make(map[string]int) for _, answer := range answers { _, ok := userAnswerCount[answer.UserID] if !ok { userAnswerCount[answer.UserID] = 1 } else { userAnswerCount[answer.UserID]++ } } userList := make([]entity.User, 0) err = x.Context(ctx).Find(&userList, &entity.User{}) if err != nil { return fmt.Errorf("get user failed: %w", err) } for _, user := range userList { _, ok := userAnswerCount[user.ID] if ok { user.AnswerCount = userAnswerCount[user.ID] if _, err = x.Context(ctx).Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil { log.Errorf("update %+v user failed: %s", user.ID, err) return fmt.Errorf("update user failed: %w", err) } } else { user.AnswerCount = 0 if _, err = x.Context(ctx).Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil { log.Errorf("update %+v user failed: %s", user.ID, err) return fmt.Errorf("update user failed: %w", err) } } } return nil } type QuestionV13 struct { ID string `xorm:"not null pk BIGINT(20) id"` CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` InviteUserID string `xorm:"TEXT invite_user_id"` LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` Title string `xorm:"not null default '' VARCHAR(150) title"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"` ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` Status int `xorm:"not null default 1 INT(11) status"` Pin int `xorm:"not null default 1 INT(11) pin"` Show int `xorm:"not null default 1 INT(11) show"` ViewCount int `xorm:"not null default 0 INT(11) view_count"` UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"` VoteCount int `xorm:"not null default 0 INT(11) vote_count"` AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` CollectionCount int `xorm:"not null default 0 INT(11) collection_count"` FollowCount int `xorm:"not null default 0 INT(11) follow_count"` AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` } func (QuestionV13) TableName() string { return "question" } func inviteAnswer(ctx context.Context, x *xorm.Engine) error { err := x.Context(ctx).Sync(new(QuestionV13)) if err != nil { return err } return nil } // inBoxData Classify messages func inBoxData(ctx context.Context, x *xorm.Engine) error { type Notification struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` ObjectID string `xorm:"not null default 0 INDEX BIGINT(20) object_id"` Content string `xorm:"not null TEXT content"` Type int `xorm:"not null default 0 INT(11) type"` MsgType int `xorm:"not null default 0 INT(11) msg_type"` IsRead int `xorm:"not null default 1 INT(11) is_read"` Status int `xorm:"not null default 1 INT(11) status"` } err := x.Context(ctx).Sync(new(Notification)) if err != nil { return err } msglist := make([]entity.Notification, 0) err = x.Context(ctx).Find(&msglist, &entity.Notification{}) if err != nil { return fmt.Errorf("get Notification failed: %w", err) } for _, v := range msglist { Content := &schema.NotificationContent{} err := json.Unmarshal([]byte(v.Content), Content) if err != nil { continue } _, ok := constant.NotificationMsgTypeMapping[Content.NotificationAction] if ok { v.MsgType = constant.NotificationMsgTypeMapping[Content.NotificationAction] if _, err = x.Context(ctx).Update(v, &entity.Notification{ID: v.ID}); err != nil { log.Errorf("update %+v Notification failed: %s", v.ID, err) } } } return nil } ================================================ FILE: internal/migrations/v14.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "time" "xorm.io/xorm/schemas" "xorm.io/xorm" ) func updateTheLengthOfRevisionContent(ctx context.Context, x *xorm.Engine) (err error) { sess := x.Context(ctx) if x.Dialect().URI().DBType == schemas.MYSQL { _, err = sess.Exec("ALTER TABLE `revision` CHANGE `content` `content` MEDIUMTEXT NOT NULL;") } return err } type RevisionV14 struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` UserID string `xorm:"not null default 0 BIGINT(20) user_id"` ObjectType int `xorm:"not null default 0 INT(11) object_type"` ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` Title string `xorm:"not null default '' VARCHAR(255) title"` Content string `xorm:"not null MEDIUMTEXT content"` Log string `xorm:"VARCHAR(255) log"` Status int `xorm:"not null default 1 INT(11) status"` ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"` } func (RevisionV14) TableName() string { return "revision" } ================================================ FILE: internal/migrations/v15.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) func addNoticeConfig(ctx context.Context, x *xorm.Engine) error { return x.Context(ctx).Sync(new(entity.UserNotificationConfig)) } ================================================ FILE: internal/migrations/v16.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) func setDefaultUserNotificationConfig(ctx context.Context, x *xorm.Engine) error { userIDs := make([]string, 0) err := x.Context(ctx).Table("user").Select("id").Find(&userIDs) if err != nil { return err } for _, id := range userIDs { bean := entity.UserNotificationConfig{UserID: id, Source: string(constant.InboxSource)} exist, err := x.Context(ctx).Get(&bean) if err != nil { log.Error(err) } if exist { continue } _, err = x.Context(ctx).Insert(&entity.UserNotificationConfig{ UserID: id, Source: string(constant.InboxSource), Channels: `[{"key":"email","enable":true}]`, Enabled: true, }) if err != nil { log.Error(err) } } return nil } ================================================ FILE: internal/migrations/v17.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/permission" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) func addRecoverPermission(ctx context.Context, x *xorm.Engine) error { powers := []*entity.Power{ {ID: 39, Name: "recover answer", PowerType: permission.AnswerUnDelete, Description: "recover deleted answer"}, {ID: 40, Name: "recover question", PowerType: permission.QuestionUnDelete, Description: "recover deleted question"}, {ID: 41, Name: "recover tag", PowerType: permission.TagUnDelete, Description: "recover deleted tag"}, } for _, power := range powers { exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID}) if err != nil { return err } if exist { _, err = x.Context(ctx).ID(power.ID).Update(power) } else { _, err = x.Context(ctx).Insert(power) } if err != nil { return err } } rolePowerRels := []*entity.RolePowerRel{ {RoleID: 2, PowerType: permission.AnswerUnDelete}, {RoleID: 2, PowerType: permission.QuestionUnDelete}, {RoleID: 2, PowerType: permission.TagUnDelete}, {RoleID: 3, PowerType: permission.AnswerUnDelete}, {RoleID: 3, PowerType: permission.QuestionUnDelete}, {RoleID: 3, PowerType: permission.TagUnDelete}, } for _, rel := range rolePowerRels { exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) if err != nil { return err } if exist { continue } _, err = x.Context(ctx).Insert(rel) if err != nil { return err } } defaultConfigTable := []*entity.Config{ {ID: 128, Key: "rank.answer.undeleted", Value: `-1`}, {ID: 129, Key: "rank.question.undeleted", Value: `-1`}, {ID: 130, Key: "rank.tag.undeleted", Value: `-1`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil { log.Errorf("update %+v config failed: %s", c, err) return fmt.Errorf("update config failed: %w", err) } continue } if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { log.Errorf("insert %+v config failed: %s", c, err) return fmt.Errorf("add config failed: %w", err) } } return nil } ================================================ FILE: internal/migrations/v18.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "xorm.io/xorm" ) func addPasswordLoginControl(ctx context.Context, x *xorm.Engine) error { loginSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeLogin, } exist, err := x.Context(ctx).Get(loginSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { content := &schema.SiteLoginReq{} _ = json.Unmarshal([]byte(loginSiteInfo.Content), content) content.AllowPasswordLogin = true data, _ := json.Marshal(content) loginSiteInfo.Content = string(data) _, err = x.Context(ctx).ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo) if err != nil { return fmt.Errorf("update site info failed: %w", err) } } writeSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeWrite, } exist, err = x.Context(ctx).Get(writeSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { type OldSiteWriteReq struct { RestrictAnswer bool `validate:"omitempty" form:"restrict_answer" json:"restrict_answer"` RequiredTag bool `validate:"omitempty" form:"required_tag" json:"required_tag"` RecommendTags []string `validate:"omitempty" form:"recommend_tags" json:"recommend_tags"` ReservedTags []string `validate:"omitempty" form:"reserved_tags" json:"reserved_tags"` UserID string `json:"-"` } content := &OldSiteWriteReq{} _ = json.Unmarshal([]byte(writeSiteInfo.Content), content) content.RestrictAnswer = true data, _ := json.Marshal(content) writeSiteInfo.Content = string(data) _, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols("content").Update(writeSiteInfo) if err != nil { return fmt.Errorf("update site info failed: %w", err) } } type User struct { Avatar string `xorm:"not null default '' VARCHAR(1024) avatar"` } return x.Context(ctx).Sync(new(User)) } ================================================ FILE: internal/migrations/v19.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) func addNotificationPluginAndThemeConfig(ctx context.Context, x *xorm.Engine) error { type User struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` ColorScheme string `xorm:"not null default '' VARCHAR(100) color_scheme"` } return x.Context(ctx).Sync(new(entity.PluginUserConfig), new(User)) } ================================================ FILE: internal/migrations/v2.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "xorm.io/xorm" ) func addTagRecommendedAndReserved(ctx context.Context, x *xorm.Engine) error { type Tag struct { ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"` SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"` Recommend bool `xorm:"not null default false BOOL recommend"` Reserved bool `xorm:"not null default false BOOL reserved"` } return x.Context(ctx).Sync(new(Tag)) } ================================================ FILE: internal/migrations/v20.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) func addReview(ctx context.Context, x *xorm.Engine) error { c := &entity.Config{Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only."}`} if _, err := x.Context(ctx).Update(c, &entity.Config{Key: "reason.not_clarity"}); err != nil { log.Errorf("update %+v config failed: %s", c, err) return fmt.Errorf("update config failed: %w", err) } return x.Context(ctx).Sync(new(entity.Review)) } ================================================ FILE: internal/migrations/v21.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "xorm.io/xorm" ) func addQuestionHotScore(ctx context.Context, x *xorm.Engine) error { type Question struct { HotScore int `xorm:"not null default 0 INT(11) hot_score"` } return x.Context(ctx).Sync(new(Question)) } ================================================ FILE: internal/migrations/v22.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/unique" "xorm.io/xorm" ) func addBadges(ctx context.Context, x *xorm.Engine) (err error) { uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: x}) err = x.Context(ctx).Sync(new(entity.Badge), new(entity.BadgeGroup), new(entity.BadgeAward)) if err != nil { return fmt.Errorf("sync table failed: %w", err) } for _, badgeGroup := range defaultBadgeGroupTable { exist, err := x.Context(ctx).Get(&entity.BadgeGroup{ID: badgeGroup.ID}) if err != nil { return err } if exist { _, err = x.Context(ctx).ID(badgeGroup.ID).Update(badgeGroup) } else { _, err = x.Context(ctx).Insert(badgeGroup) } if err != nil { return fmt.Errorf("insert badge group failed: %w", err) } } for _, badge := range defaultBadgeTable { beans := &entity.Badge{Name: badge.Name} exist, err := x.Context(ctx).Get(beans) if err != nil { return err } if exist { badge.ID = beans.ID _, err = x.Context(ctx).ID(beans.ID).Update(badge) if err != nil { return fmt.Errorf("update badge failed: %w", err) } continue } badge.ID, err = uniqueIDRepo.GenUniqueIDStr(ctx, new(entity.Badge).TableName()) if err != nil { return err } if _, err := x.Context(ctx).Insert(badge); err != nil { return err } } return } ================================================ FILE: internal/migrations/v23.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) func addQuestionLink(ctx context.Context, x *xorm.Engine) (err error) { return x.Context(ctx).Sync(new(entity.QuestionLink)) } ================================================ FILE: internal/migrations/v24.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "xorm.io/xorm" ) func addQuestionLinkedCount(ctx context.Context, x *xorm.Engine) error { writeSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeWrite, } exist, err := x.Context(ctx).Get(writeSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { type OldSiteWriteReq struct { RestrictAnswer bool `json:"restrict_answer"` RequiredTag bool `json:"required_tag"` RecommendTags []*schema.SiteWriteTag `json:"recommend_tags"` ReservedTags []*schema.SiteWriteTag `json:"reserved_tags"` MaxImageSize int `json:"max_image_size"` MaxAttachmentSize int `json:"max_attachment_size"` MaxImageMegapixel int `json:"max_image_megapixel"` AuthorizedImageExtensions []string `json:"authorized_image_extensions"` AuthorizedAttachmentExtensions []string `json:"authorized_attachment_extensions"` } content := &OldSiteWriteReq{} _ = json.Unmarshal([]byte(writeSiteInfo.Content), content) content.MaxImageSize = 4 content.MaxAttachmentSize = 8 content.MaxImageMegapixel = 40 content.AuthorizedImageExtensions = []string{"jpg", "jpeg", "png", "gif", "webp"} content.AuthorizedAttachmentExtensions = []string{} data, _ := json.Marshal(content) writeSiteInfo.Content = string(data) _, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols("content").Update(writeSiteInfo) if err != nil { return fmt.Errorf("update site info failed: %w", err) } } return x.Context(ctx).Sync(new(entity.Question)) } ================================================ FILE: internal/migrations/v25.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) func addFileRecord(ctx context.Context, x *xorm.Engine) error { if err := x.Context(ctx).Sync(new(entity.FileRecord)); err != nil { return err } // Set default external_content_display to always_display legalInfo := &entity.SiteInfo{Type: "legal"} exist, err := x.Context(ctx).Get(legalInfo) if err != nil { return fmt.Errorf("get legal config failed: %w", err) } legalConfig := make(map[string]any) if exist { if err := json.Unmarshal([]byte(legalInfo.Content), &legalConfig); err != nil { return fmt.Errorf("unmarshal legal config failed: %w", err) } } legalConfig["external_content_display"] = "always_display" legalConfigBytes, _ := json.Marshal(legalConfig) if exist { legalInfo.Content = string(legalConfigBytes) _, err = x.Context(ctx).ID(legalInfo.ID).Cols("content").Update(legalInfo) if err != nil { return fmt.Errorf("update legal config failed: %w", err) } } else { legalInfo.Content = string(legalConfigBytes) legalInfo.Status = 1 _, err = x.Context(ctx).Insert(legalInfo) if err != nil { return fmt.Errorf("insert legal config failed: %w", err) } } return nil } ================================================ FILE: internal/migrations/v26.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) func addPluginKVStorage(ctx context.Context, x *xorm.Engine) error { return x.Context(ctx).Sync(new(entity.PluginKVStorage)) } ================================================ FILE: internal/migrations/v27.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "xorm.io/xorm" ) func addSuspendedUntilToUser(ctx context.Context, x *xorm.Engine) error { type User struct { SuspendedUntil *time.Time `xorm:"DATETIME suspended_until"` } return x.Context(ctx).Sync(new(User)) } func moveUserConfigToInterface(ctx context.Context, x *xorm.Engine) error { if err := addSuspendedUntilToUser(ctx, x); err != nil { return fmt.Errorf("add suspended_until to user failed: %w", err) } // Get old interface config interfaceSiteInfo := &entity.SiteInfo{Type: constant.SiteTypeInterface} exist, err := x.Context(ctx).Get(interfaceSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } if !exist { return fmt.Errorf("interface site info not found") } interfaceConfig := &schema.SiteInterfaceReq{} _ = json.Unmarshal([]byte(interfaceSiteInfo.Content), interfaceConfig) // Get old user config usersConfig := &entity.SiteInfo{Type: constant.SiteTypeUsers} exist, err = x.Context(ctx).Get(usersConfig) if err != nil { return fmt.Errorf("get config failed: %w", err) } if !exist { return fmt.Errorf("users site info not found") } siteUsers := &schema.SiteUsersReq{} _ = json.Unmarshal([]byte(usersConfig.Content), siteUsers) interfaceConfig.DefaultAvatar = siteUsers.DefaultAvatar interfaceConfig.GravatarBaseURL = siteUsers.GravatarBaseURL interfaceConfigByte, _ := json.Marshal(interfaceConfig) interfaceSiteInfo.Content = string(interfaceConfigByte) _, err = x.Context(ctx).ID(interfaceSiteInfo.ID).Update(interfaceSiteInfo) if err != nil { return fmt.Errorf("insert site info failed: %w", err) } return nil } ================================================ FILE: internal/migrations/v28.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "xorm.io/xorm" ) func addOptionalTags(ctx context.Context, x *xorm.Engine) error { writeSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeWrite, } exist, err := x.Context(ctx).Get(writeSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { type OldSiteWriteReq struct { MinimumContent int `json:"min_content"` RestrictAnswer bool `json:"restrict_answer"` MinimumTags int `json:"min_tags"` RequiredTag bool `json:"required_tag"` RecommendTags []*schema.SiteWriteTag `json:"recommend_tags"` ReservedTags []*schema.SiteWriteTag `json:"reserved_tags"` MaxImageSize int `json:"max_image_size"` MaxAttachmentSize int `json:"max_attachment_size"` MaxImageMegapixel int `json:"max_image_megapixel"` AuthorizedImageExtensions []string `json:"authorized_image_extensions"` AuthorizedAttachmentExtensions []string `json:"authorized_attachment_extensions"` } content := &OldSiteWriteReq{} _ = json.Unmarshal([]byte(writeSiteInfo.Content), content) content.MinimumTags = 1 content.MinimumContent = 6 data, _ := json.Marshal(content) writeSiteInfo.Content = string(data) _, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols("content").Update(writeSiteInfo) if err != nil { return fmt.Errorf("update site info failed: %w", err) } } return nil } ================================================ FILE: internal/migrations/v29.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "xorm.io/xorm" ) func expandAvatarColumnLength(ctx context.Context, x *xorm.Engine) error { type User struct { Avatar string `xorm:"not null default '' VARCHAR(2048) avatar"` } if err := x.Context(ctx).Sync(new(User)); err != nil { return fmt.Errorf("expand avatar column length failed: %w", err) } return nil } ================================================ FILE: internal/migrations/v3.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "time" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/log" "xorm.io/xorm" "xorm.io/xorm/schemas" ) func addActivityTimeline(ctx context.Context, x *xorm.Engine) (err error) { switch x.Dialect().URI().DBType { case schemas.MYSQL: _, err = x.Context(ctx).Exec("ALTER TABLE `answer` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL") if err != nil { return err } _, err = x.Context(ctx).Exec("ALTER TABLE `question` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL") if err != nil { return err } case schemas.POSTGRES: _, err = x.Context(ctx).Exec(`ALTER TABLE "answer" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`) if err != nil { return err } _, err = x.Context(ctx).Exec(`ALTER TABLE "question" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`) if err != nil { return err } case schemas.SQLITE: _, err = x.Context(ctx).Exec(`DROP INDEX "IDX_answer_user_id"; ALTER TABLE "answer" RENAME TO "_answer_old_v3"; CREATE TABLE "answer" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" DATETIME DEFAULT NULL, "question_id" INTEGER NOT NULL DEFAULT 0, "user_id" INTEGER NOT NULL DEFAULT 0, "original_text" TEXT NOT NULL, "parsed_text" TEXT NOT NULL, "status" INTEGER NOT NULL DEFAULT 1, "adopted" INTEGER NOT NULL DEFAULT 1, "comment_count" INTEGER NOT NULL DEFAULT 0, "vote_count" INTEGER NOT NULL DEFAULT 0, "revision_id" INTEGER NOT NULL DEFAULT 0 ); INSERT INTO "answer" ("id", "created_at", "updated_at", "question_id", "user_id", "original_text", "parsed_text", "status", "adopted", "comment_count", "vote_count", "revision_id") SELECT "id", "created_at", "updated_at", "question_id", "user_id", "original_text", "parsed_text", "status", "adopted", "comment_count", "vote_count", "revision_id" FROM "_answer_old_v3"; CREATE INDEX "IDX_answer_user_id" ON "answer" ( "user_id" ASC ); DROP INDEX "IDX_question_user_id"; ALTER TABLE "question" RENAME TO "_question_old_v3"; CREATE TABLE "question" ( "id" INTEGER NOT NULL, "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" DATETIME DEFAULT NULL, "user_id" INTEGER NOT NULL DEFAULT 0, "title" TEXT NOT NULL DEFAULT '', "original_text" TEXT NOT NULL, "parsed_text" TEXT NOT NULL, "status" INTEGER NOT NULL DEFAULT 1, "view_count" INTEGER NOT NULL DEFAULT 0, "unique_view_count" INTEGER NOT NULL DEFAULT 0, "vote_count" INTEGER NOT NULL DEFAULT 0, "answer_count" INTEGER NOT NULL DEFAULT 0, "collection_count" INTEGER NOT NULL DEFAULT 0, "follow_count" INTEGER NOT NULL DEFAULT 0, "accepted_answer_id" INTEGER NOT NULL DEFAULT 0, "last_answer_id" INTEGER NOT NULL DEFAULT 0, "post_update_time" DATETIME DEFAULT CURRENT_TIMESTAMP, "revision_id" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("id") ); INSERT INTO "question" ("id", "created_at", "updated_at", "user_id", "title", "original_text", "parsed_text", "status", "view_count", "unique_view_count", "vote_count", "answer_count", "collection_count", "follow_count", "accepted_answer_id", "last_answer_id", "post_update_time", "revision_id") SELECT "id", "created_at", "updated_at", "user_id", "title", "original_text", "parsed_text", "status", "view_count", "unique_view_count", "vote_count", "answer_count", "collection_count", "follow_count", "accepted_answer_id", "last_answer_id", "post_update_time", "revision_id" FROM "_question_old_v3"; CREATE INDEX "IDX_question_user_id" ON "question" ( "user_id" ASC );`) if err != nil { return err } } // only increasing field length to 128 type Config struct { ID int `xorm:"not null pk autoincr INT(11) id"` Key string `xorm:"unique VARCHAR(128) key"` } if err := x.Context(ctx).Sync(new(Config)); err != nil { return fmt.Errorf("sync config table failed: %w", err) } defaultConfigTable := []*entity.Config{ {ID: 36, Key: "rank.question.add", Value: `1`}, {ID: 37, Key: "rank.question.edit", Value: `200`}, {ID: 38, Key: "rank.question.delete", Value: `-1`}, {ID: 39, Key: "rank.question.vote_up", Value: `15`}, {ID: 40, Key: "rank.question.vote_down", Value: `125`}, {ID: 41, Key: "rank.answer.add", Value: `1`}, {ID: 42, Key: "rank.answer.edit", Value: `200`}, {ID: 43, Key: "rank.answer.delete", Value: `-1`}, {ID: 44, Key: "rank.answer.accept", Value: `-1`}, {ID: 45, Key: "rank.answer.vote_up", Value: `15`}, {ID: 46, Key: "rank.answer.vote_down", Value: `125`}, {ID: 47, Key: "rank.comment.add", Value: `1`}, {ID: 48, Key: "rank.comment.edit", Value: `-1`}, {ID: 49, Key: "rank.comment.delete", Value: `-1`}, {ID: 50, Key: "rank.report.add", Value: `1`}, {ID: 51, Key: "rank.tag.add", Value: `1500`}, {ID: 52, Key: "rank.tag.edit", Value: `100`}, {ID: 53, Key: "rank.tag.delete", Value: `-1`}, {ID: 54, Key: "rank.tag.synonym", Value: `20000`}, {ID: 55, Key: "rank.link.url_limit", Value: `10`}, {ID: 56, Key: "rank.vote.detail", Value: `0`}, {ID: 87, Key: "question.asked", Value: `0`}, {ID: 88, Key: "question.closed", Value: `0`}, {ID: 89, Key: "question.reopened", Value: `0`}, {ID: 90, Key: "question.answered", Value: `0`}, {ID: 91, Key: "question.commented", Value: `0`}, {ID: 92, Key: "question.accept", Value: `0`}, {ID: 93, Key: "question.edited", Value: `0`}, {ID: 94, Key: "question.rollback", Value: `0`}, {ID: 95, Key: "question.deleted", Value: `0`}, {ID: 96, Key: "question.undeleted", Value: `0`}, {ID: 97, Key: "answer.answered", Value: `0`}, {ID: 98, Key: "answer.commented", Value: `0`}, {ID: 99, Key: "answer.edited", Value: `0`}, {ID: 100, Key: "answer.rollback", Value: `0`}, {ID: 101, Key: "answer.undeleted", Value: `0`}, {ID: 102, Key: "tag.created", Value: `0`}, {ID: 103, Key: "tag.edited", Value: `0`}, {ID: 104, Key: "tag.rollback", Value: `0`}, {ID: 105, Key: "tag.deleted", Value: `0`}, {ID: 106, Key: "tag.undeleted", Value: `0`}, {ID: 107, Key: "rank.comment.vote_up", Value: `1`}, {ID: 108, Key: "rank.comment.vote_down", Value: `1`}, {ID: 109, Key: "rank.question.edit_without_review", Value: `2000`}, {ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`}, {ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`}, {ID: 112, Key: "rank.answer.audit", Value: `2000`}, {ID: 113, Key: "rank.question.audit", Value: `2000`}, {ID: 114, Key: "rank.tag.audit", Value: `20000`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil { log.Errorf("update %+v config failed: %s", c, err) return fmt.Errorf("update config failed: %w", err) } continue } if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { log.Errorf("insert %+v config failed: %s", c, err) return fmt.Errorf("add config failed: %w", err) } } type Revision struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"` } type Activity struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"` UserID string `xorm:"not null index BIGINT(20) user_id"` TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"` ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"` RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"` OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"` } type Tag struct { ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"` SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"` UserID string `xorm:"not null default 0 BIGINT(20) user_id"` } type Question struct { ID string `xorm:"not null pk BIGINT(20) id"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` } type Answer struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` } err = x.Context(ctx).Sync(new(Activity), new(Revision), new(Tag), new(Question), new(Answer)) if err != nil { return fmt.Errorf("sync table failed %w", err) } return nil } ================================================ FILE: internal/migrations/v30.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/errors" "xorm.io/builder" "xorm.io/xorm" ) func updateAdminMenuSettings(ctx context.Context, x *xorm.Engine) (err error) { err = splitWriteMenu(ctx, x) if err != nil { return } err = splitInterfaceMenu(ctx, x) if err != nil { return } err = splitLegalMenu(ctx, x) if err != nil { return } return } // splitWriteMenu splits the site write settings into advanced, questions, and tags settings func splitWriteMenu(ctx context.Context, x *xorm.Engine) error { var ( siteInfo = &entity.SiteInfo{} siteInfoAdvanced = &entity.SiteInfo{} siteInfoQuestions = &entity.SiteInfo{} siteInfoTags = &entity.SiteInfo{} ) exist, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeWrite}).Get(siteInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return err } if !exist { return nil } siteWrite := &schema.SiteWriteResp{} if err := json.Unmarshal([]byte(siteInfo.Content), siteWrite); err != nil { return err } // site advanced settings siteAdvanced := &schema.SiteAdvancedResp{ MaxImageSize: siteWrite.MaxImageSize, MaxAttachmentSize: siteWrite.MaxAttachmentSize, MaxImageMegapixel: siteWrite.MaxImageMegapixel, AuthorizedImageExtensions: siteWrite.AuthorizedImageExtensions, AuthorizedAttachmentExtensions: siteWrite.AuthorizedAttachmentExtensions, } // site questions settings siteQuestions := &schema.SiteQuestionsResp{ MinimumTags: siteWrite.MinimumTags, MinimumContent: siteWrite.MinimumContent, RestrictAnswer: siteWrite.RestrictAnswer, } // site tags settings siteTags := &schema.SiteTagsResp{ ReservedTags: siteWrite.ReservedTags, RecommendTags: siteWrite.RecommendTags, RequiredTag: siteWrite.RequiredTag, } // save site settings // save advanced settings existsAdvanced, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeWrite}).Get(siteInfoAdvanced) if err != nil { return err } advancedContent, err := json.Marshal(siteAdvanced) if err != nil { return err } if !existsAdvanced { _, err = x.Context(ctx).Insert(&entity.SiteInfo{ Type: constant.SiteTypeAdvanced, Content: string(advancedContent), Status: 1, }) if err != nil { return err } } // save questions settings existsQuestions, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeQuestions}).Get(siteInfoQuestions) if err != nil { return err } questionsContent, err := json.Marshal(siteQuestions) if err != nil { return err } if !existsQuestions { _, err = x.Context(ctx).Insert(&entity.SiteInfo{ Type: constant.SiteTypeQuestions, Content: string(questionsContent), Status: 1, }) if err != nil { return err } } // save tags settings existsTags, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeTags}).Get(siteInfoTags) if err != nil { return err } tagsContent, err := json.Marshal(siteTags) if err != nil { return err } if !existsTags { _, err = x.Context(ctx).Insert(&entity.SiteInfo{ Type: constant.SiteTypeTags, Content: string(tagsContent), Status: 1, }) if err != nil { return err } } return nil } // splitInterfaceMenu splits the site interface settings into interface and user settings func splitInterfaceMenu(ctx context.Context, x *xorm.Engine) error { var ( siteInfo = &entity.SiteInfo{} siteInfoInterface = &entity.SiteInfo{} siteInfoUsers = &entity.SiteInfo{} ) type SiteInterface struct { Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"` TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"` DefaultAvatar string `validate:"required,oneof=system gravatar" json:"default_avatar"` GravatarBaseURL string `validate:"omitempty" json:"gravatar_base_url"` } exist, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeInterface}).Get(siteInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return err } if !exist { return nil } oldSiteInterface := &SiteInterface{} if err := json.Unmarshal([]byte(siteInfo.Content), oldSiteInterface); err != nil { return err } siteUser := &schema.SiteUsersSettingsResp{ DefaultAvatar: oldSiteInterface.DefaultAvatar, GravatarBaseURL: oldSiteInterface.GravatarBaseURL, } siteInterface := &schema.SiteInterfaceResp{ Language: oldSiteInterface.Language, TimeZone: oldSiteInterface.TimeZone, } // save settings // save user settings existsUsers, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeUsersSettings}).Get(siteInfoUsers) if err != nil { return err } userContent, err := json.Marshal(siteUser) if err != nil { return err } if !existsUsers { _, err = x.Context(ctx).Insert(&entity.SiteInfo{ Type: constant.SiteTypeUsersSettings, Content: string(userContent), Status: 1, }) if err != nil { return err } } // save interface settings existsInterface, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeInterfaceSettings}).Get(siteInfoInterface) if err != nil { return err } interfaceContent, err := json.Marshal(siteInterface) if err != nil { return err } if !existsInterface { _, err = x.Context(ctx).Insert(&entity.SiteInfo{ Type: constant.SiteTypeInterfaceSettings, Content: string(interfaceContent), Status: 1, }) if err != nil { return err } } return nil } // splitLegalMenu splits the site legal settings into policies and security settings func splitLegalMenu(ctx context.Context, x *xorm.Engine) error { var ( siteInfo = &entity.SiteInfo{} siteInfoPolices = &entity.SiteInfo{} siteInfoSecurity = &entity.SiteInfo{} siteInfoLogin = &entity.SiteInfo{} siteInfoGeneral = &entity.SiteInfo{} ) type SiteLogin struct { AllowNewRegistrations bool `json:"allow_new_registrations"` AllowEmailRegistrations bool `json:"allow_email_registrations"` AllowPasswordLogin bool `json:"allow_password_login"` LoginRequired bool `json:"login_required"` AllowEmailDomains []string `json:"allow_email_domains"` } type SiteGeneral struct { Name string `validate:"required,sanitizer,gt=1,lte=128" form:"name" json:"name"` ShortDescription string `validate:"omitempty,sanitizer,gt=3,lte=255" form:"short_description" json:"short_description"` Description string `validate:"omitempty,sanitizer,gt=3,lte=2000" form:"description" json:"description"` SiteUrl string `validate:"required,sanitizer,gt=1,lte=512,url" form:"site_url" json:"site_url"` ContactEmail string `validate:"required,sanitizer,gt=1,lte=512,email" form:"contact_email" json:"contact_email"` CheckUpdate bool `validate:"omitempty,sanitizer" form:"check_update" json:"check_update"` } // find old site legal settings exist, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeLegal}).Get(siteInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return err } if !exist { return nil } oldSiteLegal := &schema.SiteLegalResp{} if err := json.Unmarshal([]byte(siteInfo.Content), oldSiteLegal); err != nil { return err } // find old site login settings existsLogin, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeLogin}).Get(siteInfoLogin) if err != nil { return err } oldSiteLogin := &SiteLogin{} if err := json.Unmarshal([]byte(siteInfoLogin.Content), oldSiteLogin); err != nil { return err } // find old site general settings existGeneral, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeGeneral}).Get(siteInfoGeneral) if err != nil { return err } oldSiteGeneral := &SiteGeneral{} if err := json.Unmarshal([]byte(siteInfoGeneral.Content), oldSiteGeneral); err != nil { return err } sitePolicies := &schema.SitePoliciesResp{ TermsOfServiceOriginalText: oldSiteLegal.TermsOfServiceOriginalText, TermsOfServiceParsedText: oldSiteLegal.TermsOfServiceParsedText, PrivacyPolicyOriginalText: oldSiteLegal.PrivacyPolicyOriginalText, PrivacyPolicyParsedText: oldSiteLegal.PrivacyPolicyParsedText, } siteLogin := &schema.SiteLoginResp{ AllowNewRegistrations: oldSiteLogin.AllowNewRegistrations, AllowEmailRegistrations: oldSiteLogin.AllowEmailRegistrations, AllowPasswordLogin: oldSiteLogin.AllowPasswordLogin, AllowEmailDomains: oldSiteLogin.AllowEmailDomains, } siteGeneral := &schema.SiteGeneralReq{ Name: oldSiteGeneral.Name, ShortDescription: oldSiteGeneral.ShortDescription, Description: oldSiteGeneral.Description, SiteUrl: oldSiteGeneral.SiteUrl, ContactEmail: oldSiteGeneral.ContactEmail, } siteSecurity := &schema.SiteSecurityResp{ LoginRequired: oldSiteLogin.LoginRequired, ExternalContentDisplay: oldSiteLegal.ExternalContentDisplay, CheckUpdate: oldSiteGeneral.CheckUpdate, } if !existsLogin { siteSecurity.LoginRequired = false } if !existGeneral { siteSecurity.CheckUpdate = true } // save settings // save policies settings existsPolicies, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypePolicies}).Get(siteInfoPolices) if err != nil { return err } policiesContent, err := json.Marshal(sitePolicies) if err != nil { return err } if !existsPolicies { _, err = x.Context(ctx).Insert(&entity.SiteInfo{ Type: constant.SiteTypePolicies, Content: string(policiesContent), Status: 1, }) if err != nil { return err } } // save security settings existsSecurity, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeSecurity}).Get(siteInfoSecurity) if err != nil { return err } securityContent, err := json.Marshal(siteSecurity) if err != nil { return err } if !existsSecurity { _, err = x.Context(ctx).Insert(&entity.SiteInfo{ Type: constant.SiteTypeSecurity, Content: string(securityContent), Status: 1, }) if err != nil { return err } } // save login settings if existsLogin { loginContent, _ := json.Marshal(siteLogin) _, err = x.Context(ctx).ID(siteInfoLogin.ID).Update(&entity.SiteInfo{ Type: constant.SiteTypeLogin, Content: string(loginContent), Status: 1, }) if err != nil { return err } } // save general settings if existGeneral { generalContent, _ := json.Marshal(siteGeneral) _, err = x.Context(ctx).ID(siteInfoGeneral.ID).Update(&entity.SiteInfo{ Type: constant.SiteTypeGeneral, Content: string(generalContent), Status: 1, }) if err != nil { return err } } return nil } ================================================ FILE: internal/migrations/v31.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) func aiFeat(ctx context.Context, x *xorm.Engine) error { if err := addAIConversationTables(ctx, x); err != nil { return fmt.Errorf("add ai conversation tables failed: %w", err) } if err := addAPIKey(ctx, x); err != nil { return fmt.Errorf("add api key failed: %w", err) } log.Info("AI feature migration completed successfully") return nil } func addAIConversationTables(ctx context.Context, x *xorm.Engine) error { if err := x.Context(ctx).Sync(new(entity.AIConversation)); err != nil { return fmt.Errorf("sync ai_conversation table failed: %w", err) } if err := x.Context(ctx).Sync(new(entity.AIConversationRecord)); err != nil { return fmt.Errorf("sync ai_conversation_record table failed: %w", err) } return nil } func addAPIKey(ctx context.Context, x *xorm.Engine) error { err := x.Context(ctx).Sync(new(entity.APIKey)) if err != nil { return err } defaultConfigTable := []*entity.Config{ {ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"}]`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{Key: c.Key}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { continue } if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { log.Errorf("insert %+v config failed: %s", c, err) return fmt.Errorf("add config failed: %w", err) } } aiSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeAI, } exist, err := x.Context(ctx).Get(aiSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { content := &schema.SiteAIReq{} _ = json.Unmarshal([]byte(aiSiteInfo.Content), content) content.PromptConfig = &schema.AIPromptConfig{ ZhCN: constant.DefaultAIPromptConfigZhCN, EnUS: constant.DefaultAIPromptConfigEnUS, } data, _ := json.Marshal(content) aiSiteInfo.Content = string(data) _, err = x.Context(ctx).ID(aiSiteInfo.ID).Cols("content").Update(aiSiteInfo) if err != nil { return fmt.Errorf("update site info failed: %w", err) } } else { content := &schema.SiteAIReq{ PromptConfig: &schema.AIPromptConfig{ ZhCN: constant.DefaultAIPromptConfigZhCN, EnUS: constant.DefaultAIPromptConfigEnUS, }, } data, _ := json.Marshal(content) aiSiteInfo.Content = string(data) aiSiteInfo.Type = constant.SiteTypeAI if _, err = x.Context(ctx).Insert(aiSiteInfo); err != nil { return fmt.Errorf("insert site info failed: %w", err) } log.Infof("insert site info %+v", aiSiteInfo) } return nil } ================================================ FILE: internal/migrations/v4.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/permission" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) func addRoleFeatures(ctx context.Context, x *xorm.Engine) error { err := x.Context(ctx).Sync(new(entity.Role), new(entity.RolePowerRel), new(entity.Power), new(entity.UserRoleRel)) if err != nil { return err } roles := []*entity.Role{ {ID: 1, Name: "User", Description: "Default with no special access."}, {ID: 2, Name: "Admin", Description: "Have the full power to access the site."}, {ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."}, } // insert default roles for _, role := range roles { exist, err := x.Context(ctx).Get(&entity.Role{ID: role.ID, Name: role.Name}) if err != nil { return err } if exist { continue } _, err = x.Context(ctx).Insert(role) if err != nil { return err } } powers := []*entity.Power{ {ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"}, {ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"}, {ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"}, {ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"}, {ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"}, {ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"}, {ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"}, {ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"}, {ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"}, {ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"}, {ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"}, {ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"}, {ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"}, {ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"}, {ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"}, {ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"}, {ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"}, {ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"}, {ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"}, {ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"}, {ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"}, {ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"}, {ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"}, {ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"}, {ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"}, {ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"}, {ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"}, {ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"}, {ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"}, {ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"}, {ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"}, {ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"}, {ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"}, } // insert default powers for _, power := range powers { exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID}) if err != nil { return err } if exist { _, err = x.Context(ctx).ID(power.ID).Update(power) } else { _, err = x.Context(ctx).Insert(power) } if err != nil { return err } } rolePowerRels := []*entity.RolePowerRel{ {RoleID: 2, PowerType: permission.AdminAccess}, {RoleID: 2, PowerType: permission.QuestionAdd}, {RoleID: 2, PowerType: permission.QuestionEdit}, {RoleID: 2, PowerType: permission.QuestionEditWithoutReview}, {RoleID: 2, PowerType: permission.QuestionDelete}, {RoleID: 2, PowerType: permission.QuestionClose}, {RoleID: 2, PowerType: permission.QuestionReopen}, {RoleID: 2, PowerType: permission.QuestionVoteUp}, {RoleID: 2, PowerType: permission.QuestionVoteDown}, {RoleID: 2, PowerType: permission.AnswerAdd}, {RoleID: 2, PowerType: permission.AnswerEdit}, {RoleID: 2, PowerType: permission.AnswerEditWithoutReview}, {RoleID: 2, PowerType: permission.AnswerDelete}, {RoleID: 2, PowerType: permission.AnswerAccept}, {RoleID: 2, PowerType: permission.AnswerVoteUp}, {RoleID: 2, PowerType: permission.AnswerVoteDown}, {RoleID: 2, PowerType: permission.CommentAdd}, {RoleID: 2, PowerType: permission.CommentEdit}, {RoleID: 2, PowerType: permission.CommentDelete}, {RoleID: 2, PowerType: permission.CommentVoteUp}, {RoleID: 2, PowerType: permission.CommentVoteDown}, {RoleID: 2, PowerType: permission.ReportAdd}, {RoleID: 2, PowerType: permission.TagAdd}, {RoleID: 2, PowerType: permission.TagEdit}, {RoleID: 2, PowerType: permission.TagEditSlugName}, {RoleID: 2, PowerType: permission.TagEditWithoutReview}, {RoleID: 2, PowerType: permission.TagDelete}, {RoleID: 2, PowerType: permission.TagSynonym}, {RoleID: 2, PowerType: permission.LinkUrlLimit}, {RoleID: 2, PowerType: permission.VoteDetail}, {RoleID: 2, PowerType: permission.AnswerAudit}, {RoleID: 2, PowerType: permission.QuestionAudit}, {RoleID: 2, PowerType: permission.TagAudit}, {RoleID: 2, PowerType: permission.TagUseReservedTag}, {RoleID: 3, PowerType: permission.QuestionAdd}, {RoleID: 3, PowerType: permission.QuestionEdit}, {RoleID: 3, PowerType: permission.QuestionEditWithoutReview}, {RoleID: 3, PowerType: permission.QuestionDelete}, {RoleID: 3, PowerType: permission.QuestionClose}, {RoleID: 3, PowerType: permission.QuestionReopen}, {RoleID: 3, PowerType: permission.QuestionVoteUp}, {RoleID: 3, PowerType: permission.QuestionVoteDown}, {RoleID: 3, PowerType: permission.AnswerAdd}, {RoleID: 3, PowerType: permission.AnswerEdit}, {RoleID: 3, PowerType: permission.AnswerEditWithoutReview}, {RoleID: 3, PowerType: permission.AnswerDelete}, {RoleID: 3, PowerType: permission.AnswerAccept}, {RoleID: 3, PowerType: permission.AnswerVoteUp}, {RoleID: 3, PowerType: permission.AnswerVoteDown}, {RoleID: 3, PowerType: permission.CommentAdd}, {RoleID: 3, PowerType: permission.CommentEdit}, {RoleID: 3, PowerType: permission.CommentDelete}, {RoleID: 3, PowerType: permission.CommentVoteUp}, {RoleID: 3, PowerType: permission.CommentVoteDown}, {RoleID: 3, PowerType: permission.ReportAdd}, {RoleID: 3, PowerType: permission.TagAdd}, {RoleID: 3, PowerType: permission.TagEdit}, {RoleID: 3, PowerType: permission.TagEditSlugName}, {RoleID: 3, PowerType: permission.TagEditWithoutReview}, {RoleID: 3, PowerType: permission.TagDelete}, {RoleID: 3, PowerType: permission.TagSynonym}, {RoleID: 3, PowerType: permission.LinkUrlLimit}, {RoleID: 3, PowerType: permission.VoteDetail}, {RoleID: 3, PowerType: permission.AnswerAudit}, {RoleID: 3, PowerType: permission.QuestionAudit}, {RoleID: 3, PowerType: permission.TagAudit}, {RoleID: 3, PowerType: permission.TagUseReservedTag}, } // insert default powers for _, rel := range rolePowerRels { exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) if err != nil { return err } if exist { continue } _, err = x.Context(ctx).Insert(rel) if err != nil { return err } } adminUserRoleRel := &entity.UserRoleRel{ UserID: "1", RoleID: 2, } exist, err := x.Context(ctx).Get(adminUserRoleRel) if err != nil { return err } if !exist { _, err = x.Context(ctx).Insert(adminUserRoleRel) if err != nil { return err } } defaultConfigTable := []*entity.Config{ {ID: 115, Key: "rank.question.close", Value: `-1`}, {ID: 116, Key: "rank.question.reopen", Value: `-1`}, {ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil { log.Errorf("update %+v config failed: %s", c, err) return fmt.Errorf("update config failed: %w", err) } continue } if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { log.Errorf("insert %+v config failed: %s", c, err) return fmt.Errorf("add config failed: %w", err) } } return nil } ================================================ FILE: internal/migrations/v5.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) func addThemeAndPrivateMode(ctx context.Context, x *xorm.Engine) error { loginConfig := map[string]bool{ "allow_new_registrations": true, "login_required": false, } loginConfigDataBytes, _ := json.Marshal(loginConfig) siteInfo := &entity.SiteInfo{ Type: "login", Content: string(loginConfigDataBytes), Status: 1, } exist, err := x.Context(ctx).Get(&entity.SiteInfo{Type: siteInfo.Type}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if !exist { _, err = x.Context(ctx).Insert(siteInfo) if err != nil { return fmt.Errorf("insert site info failed: %w", err) } } themeConfig := fmt.Sprintf(`{"theme":"default","theme_config":{"default":{"navbar_style":"#0033ff","primary_color":"#0033ff"}},"layout":"%s"}`, constant.ThemeLayoutFullWidth) themeSiteInfo := &entity.SiteInfo{ Type: "theme", Content: themeConfig, Status: 1, } exist, err = x.Context(ctx).Get(&entity.SiteInfo{Type: themeSiteInfo.Type}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if !exist { _, err = x.Context(ctx).Insert(themeSiteInfo) } return err } ================================================ FILE: internal/migrations/v6.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) func addNewAnswerNotification(ctx context.Context, x *xorm.Engine) error { cond := &entity.Config{Key: "email.config"} exists, err := x.Context(ctx).Get(cond) if err != nil { return fmt.Errorf("get email config failed: %w", err) } if !exists { // This should be impossible except that the config was deleted manually by user. _, err = x.Context(ctx).Insert(&entity.Config{ Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe"}`, }) if err != nil { return fmt.Errorf("add email config failed: %v", err) } } m := make(map[string]any) _ = json.Unmarshal([]byte(cond.Value), &m) m["new_answer_title"] = "[{{.SiteName}}] {{.DisplayName}} answered your question" m["new_answer_body"] = "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe" m["new_comment_title"] = "[{{.SiteName}}] {{.DisplayName}} commented on your post" m["new_comment_body"] = "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe" val, _ := json.Marshal(m) _, err = x.Context(ctx).ID(cond.ID).Update(&entity.Config{Value: string(val)}) if err != nil { return fmt.Errorf("update email config failed: %v", err) } return nil } ================================================ FILE: internal/migrations/v7.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) func addPlugin(ctx context.Context, x *xorm.Engine) error { defaultConfigTable := []*entity.Config{ {ID: 118, Key: "plugin.status", Value: `{}`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { continue } if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { log.Errorf("insert %+v config failed: %s", c, err) return fmt.Errorf("add config failed: %w", err) } } return x.Context(ctx).Sync(new(entity.PluginConfig), new(entity.UserExternalLogin)) } ================================================ FILE: internal/migrations/v8.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "time" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/permission" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) func addRolePinAndHideFeatures(ctx context.Context, x *xorm.Engine) error { powers := []*entity.Power{ {ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"}, {ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"}, {ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"}, {ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"}, } // insert default powers for _, power := range powers { exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID}) if err != nil { return err } if exist { _, err = x.Context(ctx).ID(power.ID).Update(power) } else { _, err = x.Context(ctx).Insert(power) } if err != nil { return err } } rolePowerRels := []*entity.RolePowerRel{ {RoleID: 2, PowerType: permission.QuestionPin}, {RoleID: 2, PowerType: permission.QuestionHide}, {RoleID: 2, PowerType: permission.QuestionUnPin}, {RoleID: 2, PowerType: permission.QuestionShow}, {RoleID: 3, PowerType: permission.QuestionPin}, {RoleID: 3, PowerType: permission.QuestionHide}, {RoleID: 3, PowerType: permission.QuestionUnPin}, {RoleID: 3, PowerType: permission.QuestionShow}, } // insert default powers for _, rel := range rolePowerRels { exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) if err != nil { return err } if exist { continue } _, err = x.Context(ctx).Insert(rel) if err != nil { return err } } defaultConfigTable := []*entity.Config{ {ID: 119, Key: "question.pin", Value: `0`}, {ID: 120, Key: "question.unpin", Value: `0`}, {ID: 121, Key: "question.show", Value: `0`}, {ID: 122, Key: "question.hide", Value: `0`}, {ID: 123, Key: "rank.question.pin", Value: `-1`}, {ID: 124, Key: "rank.question.unpin", Value: `-1`}, {ID: 125, Key: "rank.question.show", Value: `-1`}, {ID: 126, Key: "rank.question.hide", Value: `-1`}, } for _, c := range defaultConfigTable { exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil { log.Errorf("update %+v config failed: %s", c, err) return fmt.Errorf("update config failed: %w", err) } continue } if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { log.Errorf("insert %+v config failed: %s", c, err) return fmt.Errorf("add config failed: %w", err) } } type Question struct { ID string `xorm:"not null pk BIGINT(20) id"` CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` Title string `xorm:"not null default '' VARCHAR(150) title"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"` ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` Status int `xorm:"not null default 1 INT(11) status"` Pin int `xorm:"not null default 1 INT(11) pin"` Show int `xorm:"not null default 1 INT(11) show"` ViewCount int `xorm:"not null default 0 INT(11) view_count"` UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"` VoteCount int `xorm:"not null default 0 INT(11) vote_count"` AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` CollectionCount int `xorm:"not null default 0 INT(11) collection_count"` FollowCount int `xorm:"not null default 0 INT(11) follow_count"` AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` } err := x.Context(ctx).Sync(new(Question)) if err != nil { return err } return nil } ================================================ FILE: internal/migrations/v9.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package migrations import ( "context" "fmt" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) func updateAcceptAnswerRank(ctx context.Context, x *xorm.Engine) error { c := &entity.Config{ID: 44, Key: "rank.answer.accept", Value: `-1`} if _, err := x.Context(ctx).Update(c, &entity.Config{ID: 44, Key: "rank.answer.accept"}); err != nil { log.Errorf("update %+v config failed: %s", c, err) return fmt.Errorf("update config failed: %w", err) } return nil } ================================================ FILE: internal/repo/activity/activity_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/activity_type" "github.com/apache/answer/internal/service/config" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // activityRepo activity repository type activityRepo struct { data *data.Data configService *config.ConfigService } // NewActivityRepo new repository func NewActivityRepo( data *data.Data, configService *config.ConfigService, ) activity.ActivityRepo { return &activityRepo{ data: data, configService: configService, } } func (ar *activityRepo) GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) ( activityList []*entity.Activity, err error) { activityList = make([]*entity.Activity, 0) session := ar.data.DB.Context(ctx).Desc("id") if !showVote { activityTypeNotShown := ar.getAllActivityType(ctx) session.NotIn("activity_type", activityTypeNotShown) } err = session.Find(&activityList, &entity.Activity{OriginalObjectID: objectID}) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return activityList, nil } func (ar *activityRepo) getAllActivityType(ctx context.Context) (activityTypes []int) { var activityTypeNotShown []int for _, key := range activity_type.VoteActivityTypeList { id, err := ar.configService.GetIDByKey(ctx, key) if err != nil { log.Errorf("get config id by key [%s] error: %v", key, err) } else { activityTypeNotShown = append(activityTypeNotShown, id) } } return activityTypeNotShown } ================================================ FILE: internal/repo/activity/answer_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity import ( "context" "fmt" "time" "github.com/segmentfault/pacman/log" "xorm.io/builder" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) // AnswerActivityRepo answer accepted type AnswerActivityRepo struct { data *data.Data activityRepo activity_common.ActivityRepo userRankRepo rank.UserRankRepo notificationQueueService noticequeue.Service } // NewAnswerActivityRepo new repository func NewAnswerActivityRepo( data *data.Data, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, notificationQueueService noticequeue.Service, ) activity.AnswerActivityRepo { return &AnswerActivityRepo{ data: data, activityRepo: activityRepo, userRankRepo: userRankRepo, notificationQueueService: notificationQueueService, } } func (ar *AnswerActivityRepo) SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ( err error) { // save activity _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) userInfoMapping, err := ar.acquireUserInfo(session, op.GetUserIDs()) if err != nil { return nil, err } err = ar.saveActivitiesAvailable(session, op) if err != nil { return nil, err } err = ar.changeUserRank(ctx, session, op, userInfoMapping) if err != nil { return nil, err } return nil, nil }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // notification ar.sendAcceptAnswerNotification(ctx, op) return nil } func (ar *AnswerActivityRepo) SaveCancelAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ( err error) { // pre check activities, err := ar.getExistActivity(ctx, op) if err != nil { return err } var userIDs []string for _, act := range activities { if act.Cancelled == entity.ActivityCancelled { continue } userIDs = append(userIDs, act.UserID) } if len(userIDs) == 0 { return nil } // save activity _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) userInfoMapping, err := ar.acquireUserInfo(session, userIDs) if err != nil { return nil, err } err = ar.cancelActivities(session, activities) if err != nil { return nil, err } err = ar.rollbackUserRank(ctx, session, activities, userInfoMapping) if err != nil { return nil, err } return nil, nil }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // notification ar.sendCancelAcceptAnswerNotification(ctx, op) return nil } func (ar *AnswerActivityRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) { us := make([]*entity.User, 0) err := session.In("id", userIDs).ForUpdate().Find(&us) if err != nil { log.Error(err) return nil, err } users := make(map[string]*entity.User, 0) for _, u := range us { users[u.ID] = u } return users, nil } // saveActivitiesAvailable save activities // If activity not exist it will be created or else will be updated // If this activity is already exist, set activity rank to 0 // So after this function, the activity rank will be correct for update user rank func (ar *AnswerActivityRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.AcceptAnswerOperationInfo) ( err error) { for _, act := range op.Activities { existsActivity := &entity.Activity{} exist, err := session. Where(builder.Eq{"object_id": op.AnswerObjectID}). And(builder.Eq{"user_id": act.ActivityUserID}). And(builder.Eq{"trigger_user_id": act.TriggerUserID}). And(builder.Eq{"activity_type": act.ActivityType}). Get(existsActivity) if err != nil { return err } if exist && existsActivity.Cancelled == entity.ActivityAvailable { act.Rank = 0 continue } if exist { bean := &entity.Activity{ Cancelled: entity.ActivityAvailable, Rank: act.Rank, HasRank: act.HasRank(), } session.Where("id = ?", existsActivity.ID) if _, err = session.Cols("`cancelled`", "`rank`", "`has_rank`").Update(bean); err != nil { return err } } else { insertActivity := entity.Activity{ ObjectID: op.AnswerObjectID, OriginalObjectID: act.OriginalObjectID, UserID: act.ActivityUserID, TriggerUserID: converter.StringToInt64(act.TriggerUserID), ActivityType: act.ActivityType, Rank: act.Rank, HasRank: act.HasRank(), Cancelled: entity.ActivityAvailable, } _, err = session.Insert(&insertActivity) if err != nil { return err } } } return nil } // cancelActivities cancel activities // If this activity is already cancelled, set activity rank to 0 // So after this function, the activity rank will be correct for update user rank func (ar *AnswerActivityRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) { for _, act := range activities { t := &entity.Activity{} exist, err := session.ID(act.ID).Get(t) if err != nil { log.Error(err) return err } if !exist { log.Error(fmt.Errorf("%s activity not exist", act.ID)) return fmt.Errorf("%s activity not exist", act.ID) } // If this activity is already cancelled, set activity rank to 0 if t.Cancelled == entity.ActivityCancelled { act.Rank = 0 } if _, err = session.ID(act.ID).Cols("cancelled", "cancelled_at"). Update(&entity.Activity{ Cancelled: entity.ActivityCancelled, CancelledAt: time.Now(), }); err != nil { log.Error(err) return err } } return nil } func (ar *AnswerActivityRepo) changeUserRank(ctx context.Context, session *xorm.Session, op *schema.AcceptAnswerOperationInfo, userInfoMapping map[string]*entity.User) (err error) { for _, act := range op.Activities { if act.Rank == 0 { continue } user := userInfoMapping[act.ActivityUserID] if user == nil { continue } if err = ar.userRankRepo.ChangeUserRank(ctx, session, act.ActivityUserID, user.Rank, act.Rank); err != nil { log.Error(err) return err } } return nil } func (ar *AnswerActivityRepo) rollbackUserRank(ctx context.Context, session *xorm.Session, activities []*entity.Activity, userInfoMapping map[string]*entity.User) (err error) { for _, act := range activities { if act.Rank == 0 { continue } user := userInfoMapping[act.UserID] if user == nil { continue } if err = ar.userRankRepo.ChangeUserRank(ctx, session, act.UserID, user.Rank, -act.Rank); err != nil { log.Error(err) return err } } return nil } func (ar *AnswerActivityRepo) getExistActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ([]*entity.Activity, error) { var activities []*entity.Activity for _, action := range op.Activities { var t []*entity.Activity err := ar.data.DB.Context(ctx). Where(builder.Eq{"user_id": action.ActivityUserID}). And(builder.Eq{"activity_type": action.ActivityType}). And(builder.Eq{"object_id": op.AnswerObjectID}). Find(&t) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if len(t) > 0 { activities = append(activities, t...) } } return activities, nil } func (ar *AnswerActivityRepo) sendAcceptAnswerNotification( ctx context.Context, op *schema.AcceptAnswerOperationInfo) { for _, act := range op.Activities { msg := &schema.NotificationMsg{ Type: schema.NotificationTypeAchievement, ObjectID: op.AnswerObjectID, ReceiverUserID: act.ActivityUserID, TriggerUserID: act.TriggerUserID, } msg.ObjectType = constant.AnswerObjectType if msg.TriggerUserID != msg.ReceiverUserID { ar.notificationQueueService.Send(ctx, msg) } } for _, act := range op.Activities { msg := &schema.NotificationMsg{ ReceiverUserID: act.ActivityUserID, Type: schema.NotificationTypeInbox, ObjectID: op.AnswerObjectID, TriggerUserID: op.TriggerUserID, } if act.ActivityUserID != op.QuestionUserID { msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationAcceptAnswer ar.notificationQueueService.Send(ctx, msg) } } } func (ar *AnswerActivityRepo) sendCancelAcceptAnswerNotification( ctx context.Context, op *schema.AcceptAnswerOperationInfo) { for _, act := range op.Activities { msg := &schema.NotificationMsg{ TriggerUserID: act.TriggerUserID, ReceiverUserID: act.ActivityUserID, Type: schema.NotificationTypeAchievement, ObjectID: op.AnswerObjectID, } if act.ActivityUserID == op.QuestionObjectID { msg.ObjectType = constant.QuestionObjectType } else { msg.ObjectType = constant.AnswerObjectType } if msg.TriggerUserID != msg.ReceiverUserID { ar.notificationQueueService.Send(ctx, msg) } } } ================================================ FILE: internal/repo/activity/follow_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity import ( "context" "time" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/follow" "github.com/apache/answer/pkg/obj" "github.com/segmentfault/pacman/log" "xorm.io/builder" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) // FollowRepo activity repository type FollowRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo activityRepo activity_common.ActivityRepo } // NewFollowRepo new repository func NewFollowRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, activityRepo activity_common.ActivityRepo, ) follow.FollowRepo { return &FollowRepo{ data: data, uniqueIDRepo: uniqueIDRepo, activityRepo: activityRepo, } } func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error { objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow") if err != nil { return err } _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) var ( existsActivity entity.Activity has bool ) result = nil has, err = session.Where(builder.Eq{"activity_type": activityType}). And(builder.Eq{"user_id": userID}). And(builder.Eq{"object_id": objectID}). Get(&existsActivity) if err != nil { return } if has && existsActivity.Cancelled == entity.ActivityAvailable { return } if has { _, err = session.Where(builder.Eq{"id": existsActivity.ID}). Cols(`cancelled`). Update(&entity.Activity{ Cancelled: entity.ActivityAvailable, }) } else { // update existing activity with new user id and u object id _, err = session.Insert(&entity.Activity{ UserID: userID, ObjectID: objectID, OriginalObjectID: objectID, ActivityType: activityType, Cancelled: entity.ActivityAvailable, Rank: 0, HasRank: 0, }) } if err != nil { log.Error(err) return } // start update followers when everything is fine err = ar.updateFollows(ctx, session, objectID, 1) if err != nil { log.Error(err) } return }) return err } func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string) error { objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow") if err != nil { return err } _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) var ( existsActivity entity.Activity has bool ) result = nil has, err = session.Where(builder.Eq{"activity_type": activityType}). And(builder.Eq{"user_id": userID}). And(builder.Eq{"object_id": objectID}). Get(&existsActivity) if err != nil || !has { return } if has && existsActivity.Cancelled == entity.ActivityCancelled { return } if _, err = session.Where("id = ?", existsActivity.ID). Cols("cancelled"). Update(&entity.Activity{ Cancelled: entity.ActivityCancelled, CancelledAt: time.Now(), }); err != nil { return } err = ar.updateFollows(ctx, session, objectID, -1) return }) return err } func (ar *FollowRepo) updateFollows(_ context.Context, session *xorm.Session, objectID string, follows int) error { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return err } switch objectType { case "question": _, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.Question{}) case "user": _, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.User{}) case "tag": _, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.Tag{}) default: err = errors.InternalServer(reason.DisallowFollow).WithMsg("this object can't be followed") } return err } ================================================ FILE: internal/repo/activity/review_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity import ( "context" "fmt" "github.com/apache/answer/internal/schema" "github.com/apache/answer/pkg/converter" "xorm.io/builder" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/rank" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) // ReviewActivityRepo answer accepted type ReviewActivityRepo struct { data *data.Data activityRepo activity_common.ActivityRepo userRankRepo rank.UserRankRepo configService *config.ConfigService } const ( EditAccepted = "edit.accepted" ) // NewReviewActivityRepo new repository func NewReviewActivityRepo( data *data.Data, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, configService *config.ConfigService, ) activity.ReviewActivityRepo { return &ReviewActivityRepo{ data: data, activityRepo: activityRepo, userRankRepo: userRankRepo, configService: configService, } } // Review user active func (ar *ReviewActivityRepo) Review(ctx context.Context, act *schema.PassReviewActivity) (err error) { cfg, err := ar.configService.GetConfigByKey(ctx, EditAccepted) if err != nil { return err } addActivity := &entity.Activity{ UserID: act.UserID, TriggerUserID: converter.StringToInt64(act.TriggerUserID), ObjectID: act.ObjectID, OriginalObjectID: act.OriginalObjectID, ActivityType: cfg.ID, Rank: cfg.GetIntValue(), HasRank: 1, RevisionID: converter.StringToInt64(act.RevisionID), } _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) user := &entity.User{} exist, err := session.ID(addActivity.UserID).ForUpdate().Get(user) if err != nil { return nil, err } if !exist { return nil, fmt.Errorf("user not exist") } existsActivity := &entity.Activity{} exist, err = session. And(builder.Eq{"user_id": addActivity.UserID}). And(builder.Eq{"activity_type": addActivity.ActivityType}). And(builder.Eq{"revision_id": addActivity.RevisionID}). Get(existsActivity) if err != nil { return nil, err } if exist { return nil, nil } err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank) if err != nil { return nil, err } _, err = session.Insert(addActivity) if err != nil { return nil, err } return nil, nil }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } ================================================ FILE: internal/repo/activity/user_active_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity import ( "context" "fmt" "xorm.io/builder" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/rank" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) // UserActiveActivityRepo answer accepted type UserActiveActivityRepo struct { data *data.Data activityRepo activity_common.ActivityRepo userRankRepo rank.UserRankRepo configService *config.ConfigService } const ( UserActivated = "user.activated" ) // NewUserActiveActivityRepo new repository func NewUserActiveActivityRepo( data *data.Data, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, configService *config.ConfigService, ) activity.UserActiveActivityRepo { return &UserActiveActivityRepo{ data: data, activityRepo: activityRepo, userRankRepo: userRankRepo, configService: configService, } } // UserActive user active func (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string) (err error) { cfg, err := ar.configService.GetConfigByKey(ctx, UserActivated) if err != nil { return err } addActivity := &entity.Activity{ UserID: userID, ObjectID: "0", OriginalObjectID: "0", ActivityType: cfg.ID, Rank: cfg.GetIntValue(), HasRank: 1, } _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) user := &entity.User{} exist, err := session.ID(userID).ForUpdate().Get(user) if err != nil { return nil, err } if !exist { return nil, fmt.Errorf("user not exist") } existsActivity := &entity.Activity{} exist, err = session. And(builder.Eq{"user_id": addActivity.UserID}). And(builder.Eq{"activity_type": addActivity.ActivityType}). Get(existsActivity) if err != nil { return nil, err } if exist { return nil, nil } err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank) if err != nil { return nil, err } _, err = session.Insert(addActivity) if err != nil { return nil, err } return nil, nil }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } ================================================ FILE: internal/repo/activity/vote_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity import ( "context" "fmt" "time" "github.com/apache/answer/internal/service/content" "github.com/segmentfault/pacman/log" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/pkg/obj" "xorm.io/builder" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_common" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) // VoteRepo activity repository type VoteRepo struct { data *data.Data activityRepo activity_common.ActivityRepo userRankRepo rank.UserRankRepo notificationQueueService noticequeue.Service } // NewVoteRepo new repository func NewVoteRepo( data *data.Data, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, notificationQueueService noticequeue.Service, ) content.VoteRepo { return &VoteRepo{ data: data, activityRepo: activityRepo, userRankRepo: userRankRepo, notificationQueueService: notificationQueueService, } } func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) { noNeedToVote, err := vr.votePreCheck(ctx, op) if err != nil { return err } if noNeedToVote { return nil } sendInboxNotification := false maxDailyRank, err := vr.userRankRepo.GetMaxDailyRank(ctx) if err != nil { return err } var userIDs []string for _, activity := range op.Activities { userIDs = append(userIDs, activity.ActivityUserID) } _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) userInfoMapping, err := vr.acquireUserInfo(session, userIDs) if err != nil { return nil, err } err = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, userInfoMapping, maxDailyRank) if err != nil { return nil, err } sendInboxNotification, err = vr.saveActivitiesAvailable(session, op) if err != nil { return nil, err } err = vr.changeUserRank(ctx, session, op, userInfoMapping) if err != nil { return nil, err } return nil, nil }) if err != nil { return err } for _, activity := range op.Activities { if activity.Rank == 0 { continue } vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID) } if sendInboxNotification { vr.sendVoteInboxNotification(ctx, op.OperatingUserID, op.ObjectCreatorUserID, op.ObjectID, op.VoteUp) } return nil } func (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) { // Pre-Check // 1. check if the activity exist // 2. check if the activity is not cancelled // 3. if all activities are cancelled, return directly activities, err := vr.getExistActivity(ctx, op) if err != nil { return err } var userIDs []string for _, activity := range activities { if activity.Cancelled == entity.ActivityCancelled { continue } userIDs = append(userIDs, activity.UserID) } if len(userIDs) == 0 { return nil } _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) userInfoMapping, err := vr.acquireUserInfo(session, userIDs) if err != nil { return nil, err } err = vr.cancelActivities(session, activities) if err != nil { return nil, err } err = vr.rollbackUserRank(ctx, session, activities, userInfoMapping) if err != nil { return nil, err } return nil, nil }) if err != nil { return err } for _, activity := range activities { if activity.Rank == 0 { continue } vr.sendAchievementNotification(ctx, activity.UserID, op.ObjectCreatorUserID, op.ObjectID) } return nil } func (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) ( up, down int64, err error) { up = vr.countVoteUp(ctx, objectID, objectType) down = vr.countVoteDown(ctx, objectID, objectType) err = vr.updateVotes(ctx, objectID, objectType, int(up-down)) return } func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) { session := vr.data.DB.Context(ctx) cond := builder. And( builder.Eq{"user_id": userID}, builder.Eq{"cancelled": 0}, builder.In("activity_type", activityTypes), ) session.Where(cond).Desc("updated_at") total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) { activities, err := vr.getExistActivity(ctx, op) if err != nil { return false, err } done := 0 for _, activity := range activities { if activity.Cancelled == entity.ActivityAvailable { done++ } } return done == len(op.Activities), nil } func (vr *VoteRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) { us := make([]*entity.User, 0) err := session.In("id", userIDs).ForUpdate().Find(&us) if err != nil { log.Error(err) return nil, err } users := make(map[string]*entity.User, 0) for _, u := range us { users[u.ID] = u } return users, nil } func (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, session *xorm.Session, op *schema.VoteOperationInfo, userInfoMapping map[string]*entity.User, maxDailyRank int) (err error) { // check if user reach daily rank limit for _, activity := range op.Activities { if userInfoMapping[activity.ActivityUserID] == nil { continue } if activity.Rank > 0 { // check if reach max daily rank reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank) if err != nil { log.Error(err) return err } if reach { activity.Rank = 0 continue } } else { // If user rank is lower than 1 after this action, then user rank will be set to 1 only. userCurrentScore := userInfoMapping[activity.ActivityUserID].Rank if userCurrentScore+activity.Rank < 1 { activity.Rank = 1 - userCurrentScore } } } return nil } func (vr *VoteRepo) changeUserRank(ctx context.Context, session *xorm.Session, op *schema.VoteOperationInfo, userInfoMapping map[string]*entity.User) (err error) { for _, activity := range op.Activities { if activity.Rank == 0 { continue } user := userInfoMapping[activity.ActivityUserID] if user == nil { continue } if err = vr.userRankRepo.ChangeUserRank(ctx, session, activity.ActivityUserID, user.Rank, activity.Rank); err != nil { log.Error(err) return err } } return nil } func (vr *VoteRepo) rollbackUserRank(ctx context.Context, session *xorm.Session, activities []*entity.Activity, userInfoMapping map[string]*entity.User) (err error) { for _, activity := range activities { if activity.Rank == 0 { continue } user := userInfoMapping[activity.UserID] if user == nil { continue } if err = vr.userRankRepo.ChangeUserRank(ctx, session, activity.UserID, user.Rank, -activity.Rank); err != nil { log.Error(err) return err } } return nil } // saveActivitiesAvailable save activities // If activity not exist it will be created or else will be updated // If this activity is already exist, set activity rank to 0 // So after this function, the activity rank will be correct for update user rank func (vr *VoteRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.VoteOperationInfo) (newAct bool, err error) { for _, activity := range op.Activities { existsActivity := &entity.Activity{} exist, err := session. Where(builder.Eq{"object_id": op.ObjectID}). And(builder.Eq{"user_id": activity.ActivityUserID}). And(builder.Eq{"trigger_user_id": activity.TriggerUserID}). And(builder.Eq{"activity_type": activity.ActivityType}). Get(existsActivity) if err != nil { return false, err } if exist && existsActivity.Cancelled == entity.ActivityAvailable { activity.Rank = 0 continue } if exist { bean := &entity.Activity{ Cancelled: entity.ActivityAvailable, Rank: activity.Rank, HasRank: activity.HasRank(), } session.Where("id = ?", existsActivity.ID) if _, err = session.Cols("`cancelled`", "`rank`", "`has_rank`"). Update(bean); err != nil { return false, err } } else { insertActivity := entity.Activity{ ObjectID: op.ObjectID, OriginalObjectID: op.ObjectID, UserID: activity.ActivityUserID, TriggerUserID: converter.StringToInt64(activity.TriggerUserID), ActivityType: activity.ActivityType, Rank: activity.Rank, HasRank: activity.HasRank(), Cancelled: entity.ActivityAvailable, } _, err = session.Insert(&insertActivity) if err != nil { return false, err } newAct = true } } return newAct, nil } // cancelActivities cancel activities // If this activity is already cancelled, set activity rank to 0 // So after this function, the activity rank will be correct for update user rank func (vr *VoteRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) { for _, activity := range activities { t := &entity.Activity{} exist, err := session.ID(activity.ID).Get(t) if err != nil { log.Error(err) return err } if !exist { log.Error(fmt.Errorf("%s activity not exist", activity.ID)) return fmt.Errorf("%s activity not exist", activity.ID) } // If this activity is already cancelled, set activity rank to 0 if t.Cancelled == entity.ActivityCancelled { activity.Rank = 0 } if _, err = session.ID(activity.ID).Cols("cancelled", "cancelled_at"). Update(&entity.Activity{ Cancelled: entity.ActivityCancelled, CancelledAt: time.Now(), }); err != nil { log.Error(err) return err } } return nil } func (vr *VoteRepo) getExistActivity(ctx context.Context, op *schema.VoteOperationInfo) ([]*entity.Activity, error) { var activities []*entity.Activity for _, action := range op.Activities { t := &entity.Activity{} exist, err := vr.data.DB.Context(ctx). Where(builder.Eq{"user_id": action.ActivityUserID}). And(builder.Eq{"trigger_user_id": action.TriggerUserID}). And(builder.Eq{"activity_type": action.ActivityType}). And(builder.Eq{"object_id": op.ObjectID}). Get(t) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if exist { activities = append(activities, t) } } return activities, nil } func (vr *VoteRepo) countVoteUp(ctx context.Context, objectID, objectType string) (count int64) { count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteUp) if err != nil { log.Errorf("get vote up count error: %v", err) } return count } func (vr *VoteRepo) countVoteDown(ctx context.Context, objectID, objectType string) (count int64) { count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteDown) if err != nil { log.Errorf("get vote down count error: %v", err) } return count } func (vr *VoteRepo) countVote(ctx context.Context, objectID, objectType, action string) (count int64, err error) { activity := &entity.Activity{} activityType, _ := vr.activityRepo.GetActivityTypeByObjectType(ctx, objectType, action) count, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}). And(builder.Eq{"activity_type": activityType}). And(builder.Eq{"cancelled": 0}). Count(activity) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, err } func (vr *VoteRepo) updateVotes(ctx context.Context, objectID, objectType string, voteCount int) (err error) { session := vr.data.DB.Context(ctx) switch objectType { case constant.QuestionObjectType: _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Question{VoteCount: voteCount}) case constant.AnswerObjectType: _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Answer{VoteCount: voteCount}) case constant.CommentObjectType: _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Comment{VoteCount: voteCount}) } if err != nil { log.Error(err) } return } func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return } msg := &schema.NotificationMsg{ ReceiverUserID: activityUserID, TriggerUserID: objectUserID, Type: schema.NotificationTypeAchievement, ObjectID: objectID, ObjectType: objectType, } vr.notificationQueueService.Send(ctx, msg) } func (vr *VoteRepo) sendVoteInboxNotification(ctx context.Context, triggerUserID, receiverUserID, objectID string, upvote bool) { if triggerUserID == receiverUserID { return } objectType, _ := obj.GetObjectTypeStrByObjectID(objectID) msg := &schema.NotificationMsg{ TriggerUserID: triggerUserID, ReceiverUserID: receiverUserID, Type: schema.NotificationTypeInbox, ObjectID: objectID, ObjectType: objectType, } if objectType == constant.QuestionObjectType { if upvote { msg.NotificationAction = constant.NotificationUpVotedTheQuestion } else { msg.NotificationAction = constant.NotificationDownVotedTheQuestion } } if objectType == constant.AnswerObjectType { if upvote { msg.NotificationAction = constant.NotificationUpVotedTheAnswer } else { msg.NotificationAction = constant.NotificationDownVotedTheAnswer } } if objectType == constant.CommentObjectType { if upvote { msg.NotificationAction = constant.NotificationUpVotedTheComment } } if len(msg.NotificationAction) > 0 { vr.notificationQueueService.Send(ctx, msg) } } ================================================ FILE: internal/repo/activity_common/activity_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity_common import ( "context" "fmt" "time" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/activity_type" "github.com/apache/answer/pkg/obj" "xorm.io/builder" "xorm.io/xorm" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" ) // ActivityRepo activity repository type ActivityRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo configService *config.ConfigService } // NewActivityRepo new repository func NewActivityRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, configService *config.ConfigService, ) activity_common.ActivityRepo { return &ActivityRepo{ data: data, uniqueIDRepo: uniqueIDRepo, configService: configService, } } func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) ( activityType, rank, hasRank int, err error) { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return } confKey := fmt.Sprintf("%s.%s", objectType, action) cfg, err := ar.configService.GetConfigByKey(ctx, confKey) if err != nil { return } activityType, rank = cfg.ID, cfg.GetIntValue() hasRank = 0 if rank != 0 { hasRank = 1 } return } func (ar *ActivityRepo) GetActivityTypeByObjectType(ctx context.Context, objectType, action string) (activityType int, err error) { configKey := fmt.Sprintf("%s.%s", objectType, action) cfg, err := ar.configService.GetConfigByKey(ctx, configKey) if err != nil { return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return cfg.ID, nil } func (ar *ActivityRepo) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) { cfg, err := ar.configService.GetConfigByKey(ctx, configKey) if err != nil { return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return cfg.ID, nil } func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int, ) (existsActivity *entity.Activity, exist bool, err error) { existsActivity = &entity.Activity{} exist, err = session. Where(builder.Eq{"object_id": objectID}). And(builder.Eq{"user_id": userID}). And(builder.Eq{"activity_type": activityType}). Get(existsActivity) return } func (ar *ActivityRepo) GetUserActivitiesByActivityType(ctx context.Context, userID string, activityType int) ( activityList []*entity.Activity, err error) { activityList = make([]*entity.Activity, 0) err = ar.data.DB.Context(ctx).Where("user_id = ?", userID). And("activity_type = ?", activityType). And("cancelled = 0"). Find(&activityList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) { sum := &entity.ActivityRankSum{} _, err := ar.data.DB.Context(ctx).Table(entity.Activity{}.TableName()). Select("sum(`rank`) as `rank`"). Where("user_id =?", userID). And("object_id = ?", objectID). And("cancelled =0"). Get(sum) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return 0, err } return sum.Rank, nil } // AddActivity add activity func (ar *ActivityRepo) AddActivity(ctx context.Context, activity *entity.Activity) (err error) { _, err = ar.data.DB.Context(ctx).Insert(activity) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetUsersWhoHasGainedTheMostReputation get users who has gained the most reputation over a period of time func (ar *ActivityRepo) GetUsersWhoHasGainedTheMostReputation( ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) { rankStat = make([]*entity.ActivityUserRankStat, 0) session := ar.data.DB.Context(ctx).Select("user_id, SUM(`rank`) AS rank_amount").Table("activity") session.Where("has_rank = 1 AND cancelled = 0") session.Where("created_at >= ?", startTime) session.Where("created_at <= ?", endTime) session.GroupBy("user_id") session.Desc("rank_amount") session.Limit(limit) err = session.Find(&rankStat) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetUsersWhoHasVoteMost get users who has vote most func (ar *ActivityRepo) GetUsersWhoHasVoteMost( ctx context.Context, startTime, endTime time.Time, limit int) (voteStat []*entity.ActivityUserVoteStat, err error) { voteStat = make([]*entity.ActivityUserVoteStat, 0) actIDs := make([]int, 0) for _, act := range activity_type.ActivityTypeList { cfg, err := ar.configService.GetConfigByKey(ctx, act) if err == nil { actIDs = append(actIDs, cfg.ID) } } session := ar.data.DB.Context(ctx).Select("user_id, COUNT(*) AS vote_count").Table("activity") session.Where("cancelled = 0") session.In("activity_type", actIDs) session.Where("created_at >= ?", startTime) session.Where("created_at <= ?", endTime) session.GroupBy("user_id") session.Desc("vote_count") session.Limit(limit) err = session.Find(&voteStat) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/activity_common/follow.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity_common import ( "context" "time" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/unique" "github.com/apache/answer/pkg/obj" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "xorm.io/builder" "xorm.io/xorm" ) // FollowRepo follow repository type FollowRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo activityRepo activity_common.ActivityRepo } // NewFollowRepo new repository func NewFollowRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, activityRepo activity_common.ActivityRepo, ) activity_common.FollowRepo { return &FollowRepo{ data: data, uniqueIDRepo: uniqueIDRepo, activityRepo: activityRepo, } } // GetFollowAmount get object id's follows func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectID string) (follows int, err error) { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return 0, err } switch objectType { case "question": model := &entity.Question{} _, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = model.FollowCount } case "user": model := &entity.User{} _, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = model.FollowCount } case "tag": model := &entity.Tag{} _, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = model.FollowCount } default: err = errors.InternalServer(reason.DisallowFollow).WithMsg("this object can't be followed") } if err != nil { return 0, err } return follows, nil } // GetFollowUserIDs get follow userID by objectID func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) { objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return nil, err } activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow") if err != nil { log.Errorf("can't get activity type by object key: %s", objectTypeStr) return nil, err } userIDs = make([]string, 0) session := ar.data.DB.Context(ctx).Select("user_id") session.Table(entity.Activity{}.TableName()) session.Where("object_id = ?", objectID) session.Where("activity_type = ?", activityType) session.Where("cancelled = 0") err = session.Find(&userIDs) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return userIDs, nil } // GetFollowIDs get all follow id list func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) { followIDs = make([]string, 0) activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow") if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } session := ar.data.DB.Context(ctx).Select("object_id") session.Table(entity.Activity{}.TableName()) session.Where("user_id = ? AND activity_type = ?", userID, activityType) session.Where("cancelled = 0") err = session.Find(&followIDs) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return followIDs, nil } // IsFollowed check user if follow object or not func (ar *FollowRepo) IsFollowed(ctx context.Context, userID, objectID string) (followed bool, err error) { objectKey, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return false, err } activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow") if err != nil { return false, err } at := &entity.Activity{} has, err := ar.data.DB.Context(ctx).Where("user_id = ? AND object_id = ? AND activity_type = ?", userID, objectID, activityType).Get(at) if err != nil { return false, err } if !has { return false, nil } if at.Cancelled == entity.ActivityCancelled { return false, nil } else { return true, nil } } // MigrateFollowers migrate followers from source object to target object func (ar *FollowRepo) MigrateFollowers(ctx context.Context, sourceObjectID, targetObjectID, action string) error { // if source object id and target object id are same type sourceObjectTypeStr, err := obj.GetObjectTypeStrByObjectID(sourceObjectID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } targetObjectTypeStr, err := obj.GetObjectTypeStrByObjectID(targetObjectID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if sourceObjectTypeStr != targetObjectTypeStr { return errors.InternalServer(reason.DisallowFollow).WithMsg("not same object type") } activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, sourceObjectTypeStr, action) if err != nil { return err } // 1. get all user ids who follow the source object userIDs, err := ar.GetFollowUserIDs(ctx, sourceObjectID) if err != nil { log.Errorf("MigrateFollowers: failed to get user ids who follow %s: %v", sourceObjectID, err) return err } _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) // 1. delete all follows of the source object _, err = session.Table(entity.Activity{}.TableName()). Where(builder.Eq{ "object_id": sourceObjectID, "activity_type": activityType, }). Delete(&entity.Activity{}) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // 2. update cancel status to active for target tag if source tag followers is active _, err = session.Table(entity.Activity{}.TableName()). Where(builder.Eq{ "object_id": targetObjectID, "activity_type": activityType, }). And(builder.In("user_id", userIDs)). Cols("cancelled"). Update(&entity.Activity{ Cancelled: entity.ActivityAvailable, }) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // 3. get existing follows of the target object targetFollowers := make([]string, 0) err = session.Table(entity.Activity{}.TableName()). Where(builder.Eq{ "object_id": targetObjectID, "activity_type": activityType, "cancelled": entity.ActivityAvailable, }). Cols("user_id"). Find(&targetFollowers) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // 4. filter out user ids that already follow the target object and create new activity // Create a map for faster lookup of existing followers existingFollowers := make(map[string]bool) for _, uid := range targetFollowers { existingFollowers[uid] = true } // Filter out users who already follow the target newFollowers := make([]string, 0) for _, uid := range userIDs { if !existingFollowers[uid] { newFollowers = append(newFollowers, uid) } } // Create new activities for the filtered users for _, uid := range newFollowers { activity := &entity.Activity{ UserID: uid, ObjectID: targetObjectID, OriginalObjectID: targetObjectID, ActivityType: activityType, CreatedAt: time.Now(), UpdatedAt: time.Now(), Cancelled: entity.ActivityAvailable, } if _, err = session.Insert(activity); err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } } return nil, nil }) return err } ================================================ FILE: internal/repo/activity_common/vote.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity_common import ( "context" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/activity_common" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // VoteRepo activity repository type VoteRepo struct { data *data.Data activityRepo activity_common.ActivityRepo } // NewVoteRepo new repository func NewVoteRepo(data *data.Data, activityRepo activity_common.ActivityRepo) activity_common.VoteRepo { return &VoteRepo{ data: data, activityRepo: activityRepo, } } func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string) (status string) { if len(userID) == 0 { return "" } objectID = uid.DeShortID(objectID) if len(objectID) == 0 || objectID == "0" { return "" } for _, action := range []string{"vote_up", "vote_down"} { activityType, _, _, err := vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) if err != nil { return "" } at := &entity.Activity{} has, err := vr.data.DB.Context(ctx).Where("object_id = ? AND cancelled = 0 AND activity_type = ? AND user_id = ?", objectID, activityType, userID).Get(at) if err != nil { log.Error(err) return "" } if has { return action } } return "" } func (vr *VoteRepo) GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) { list := make([]*entity.Activity, 0) count, err = vr.data.DB.Context(ctx).Where("cancelled =0").In("activity_type", activityTypes).FindAndCount(&list) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/ai_conversation/ai_conversation_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package ai_conversation import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "xorm.io/builder" "xorm.io/xorm" ) // AIConversationRepo type AIConversationRepo interface { CreateConversation(ctx context.Context, conversation *entity.AIConversation) error GetConversation(ctx context.Context, conversationID string) (*entity.AIConversation, bool, error) UpdateConversation(ctx context.Context, conversation *entity.AIConversation) error GetConversationsPage(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error) CreateRecord(ctx context.Context, record *entity.AIConversationRecord) error GetRecordsByConversationID(ctx context.Context, conversationID string) ([]*entity.AIConversationRecord, error) UpdateRecordVote(ctx context.Context, cond *entity.AIConversationRecord) error GetRecord(ctx context.Context, recordID int) (*entity.AIConversationRecord, bool, error) GetRecordByChatCompletionID(ctx context.Context, role, chatCompletionID string) (*entity.AIConversationRecord, bool, error) GetConversationsForAdmin(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error) GetConversationWithVoteStats(ctx context.Context, conversationID string) (helpful, unhelpful int64, err error) DeleteConversation(ctx context.Context, conversationID string) error } type aiConversationRepo struct { data *data.Data } // NewAIConversationRepo new AIConversationRepo func NewAIConversationRepo(data *data.Data) AIConversationRepo { return &aiConversationRepo{ data: data, } } // CreateConversation creates a conversation func (r *aiConversationRepo) CreateConversation(ctx context.Context, conversation *entity.AIConversation) error { _, err := r.data.DB.Context(ctx).Insert(conversation) if err != nil { log.Errorf("create ai conversation failed: %v", err) return err } return nil } // GetConversation gets a conversation func (r *aiConversationRepo) GetConversation(ctx context.Context, conversationID string) (*entity.AIConversation, bool, error) { conversation := &entity.AIConversation{} exist, err := r.data.DB.Context(ctx).Where(builder.Eq{"conversation_id": conversationID}).Get(conversation) if err != nil { log.Errorf("get ai conversation failed: %v", err) return nil, false, err } return conversation, exist, nil } // UpdateConversation updates a conversation func (r *aiConversationRepo) UpdateConversation(ctx context.Context, conversation *entity.AIConversation) error { _, err := r.data.DB.Context(ctx).ID(conversation.ID).Update(conversation) if err != nil { log.Errorf("update ai conversation failed: %v", err) return err } return nil } // GetConversationsPage get conversations by user ID func (r *aiConversationRepo) GetConversationsPage(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error) { list = make([]*entity.AIConversation, 0) total, err = pager.Help(page, pageSize, &list, cond, r.data.DB.Context(ctx).Desc("id")) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return list, total, err } // CreateRecord creates a conversation record func (r *aiConversationRepo) CreateRecord(ctx context.Context, record *entity.AIConversationRecord) error { _, err := r.data.DB.Context(ctx).Insert(record) if err != nil { log.Errorf("create ai conversation record failed: %v", err) return err } return nil } // GetRecordsByConversationID get records by conversation ID func (r *aiConversationRepo) GetRecordsByConversationID(ctx context.Context, conversationID string) ([]*entity.AIConversationRecord, error) { records := make([]*entity.AIConversationRecord, 0) err := r.data.DB.Context(ctx). Where(builder.Eq{"conversation_id": conversationID}). OrderBy("created_at ASC"). Find(&records) if err != nil { log.Errorf("get ai conversation records failed: %v", err) return nil, err } return records, nil } // UpdateRecordVote update record vote func (r *aiConversationRepo) UpdateRecordVote(ctx context.Context, cond *entity.AIConversationRecord) (err error) { _, err = r.data.DB.Context(ctx).ID(cond.ID).MustCols("helpful", "unhelpful").Update(cond) if err != nil { log.Errorf("update ai conversation record vote failed: %v", err) return err } return nil } // GetRecord get record func (r *aiConversationRepo) GetRecord(ctx context.Context, recordID int) (*entity.AIConversationRecord, bool, error) { record := &entity.AIConversationRecord{} exist, err := r.data.DB.Context(ctx).ID(recordID).Get(record) if err != nil { log.Errorf("get ai conversation record failed: %v", err) return nil, false, err } return record, exist, nil } // GetRecordByChatCompletionID gets record by chat completion ID func (r *aiConversationRepo) GetRecordByChatCompletionID(ctx context.Context, role, chatCompletionID string) (*entity.AIConversationRecord, bool, error) { record := &entity.AIConversationRecord{} exist, err := r.data.DB.Context(ctx).Where(builder.Eq{"role": role}). Where(builder.Eq{"chat_completion_id": chatCompletionID}).Get(record) if err != nil { log.Errorf("get ai conversation record by chat completion id failed: %v", err) return nil, false, err } return record, exist, nil } // GetConversationsForAdmin gets conversation list for admin func (r *aiConversationRepo) GetConversationsForAdmin(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error) { list = make([]*entity.AIConversation, 0) total, err = pager.Help(page, pageSize, &list, cond, r.data.DB.Context(ctx).Desc("id")) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return list, total, err } // GetConversationWithVoteStats gets conversation vote statistics func (r *aiConversationRepo) GetConversationWithVoteStats(ctx context.Context, conversationID string) (helpful, unhelpful int64, err error) { res, err := r.data.DB.Context(ctx).SumsInt(&entity.AIConversationRecord{ConversationID: conversationID}, "helpful", "unhelpful") if err != nil { log.Errorf("get ai conversation vote stats failed: %v", err) return 0, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if len(res) < 2 { log.Errorf("get ai conversation vote stats failed: invalid result length %d", len(res)) return 0, 0, nil } return res[0], res[1], nil } // DeleteConversation deletes a conversation and its related records func (r *aiConversationRepo) DeleteConversation(ctx context.Context, conversationID string) error { _, err := r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { if _, err := session.Context(ctx).Where("conversation_id = ?", conversationID).Delete(&entity.AIConversationRecord{}); err != nil { log.Errorf("delete ai conversation records failed: %v", err) return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if _, err := session.Context(ctx).Where("conversation_id = ?", conversationID).Delete(&entity.AIConversation{}); err != nil { log.Errorf("delete ai conversation failed: %v", err) return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil, nil }) if err != nil { return err } return nil } ================================================ FILE: internal/repo/answer/answer_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package answer import ( "context" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_common" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/internal/service/unique" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // answerRepo answer repository type answerRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo userRankRepo rank.UserRankRepo activityRepo activity_common.ActivityRepo } // NewAnswerRepo new repository func NewAnswerRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userRankRepo rank.UserRankRepo, activityRepo activity_common.ActivityRepo, ) answercommon.AnswerRepo { return &answerRepo{ data: data, uniqueIDRepo: uniqueIDRepo, userRankRepo: userRankRepo, activityRepo: activityRepo, } } // AddAnswer add answer func (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err error) { answer.QuestionID = uid.DeShortID(answer.QuestionID) ID, err := ar.uniqueIDRepo.GenUniqueIDStr(ctx, answer.TableName()) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } answer.ID = ID _, err = ar.data.DB.Context(ctx).Insert(answer) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { answer.ID = uid.EnShortID(answer.ID) answer.QuestionID = uid.EnShortID(answer.QuestionID) } _ = ar.updateSearch(ctx, answer.ID) return nil } // RemoveAnswer delete answer func (ar *answerRepo) RemoveAnswer(ctx context.Context, answerID string) (err error) { answerID = uid.DeShortID(answerID) _, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{ Status: entity.AnswerStatusDeleted, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = ar.updateSearch(ctx, answerID) return nil } // RecoverAnswer recover answer func (ar *answerRepo) RecoverAnswer(ctx context.Context, answerID string) (err error) { answerID = uid.DeShortID(answerID) _, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{ Status: entity.AnswerStatusAvailable, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = ar.updateSearch(ctx, answerID) return nil } // RemoveAllUserAnswer remove all user answer func (ar *answerRepo) RemoveAllUserAnswer(ctx context.Context, userID string) (err error) { // find all answer id that need to be deleted answerIDs := make([]string, 0) session := ar.data.DB.Context(ctx).Where("user_id = ?", userID) session.Where("status != ?", entity.AnswerStatusDeleted) err = session.Select("id").Table("answer").Find(&answerIDs) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if len(answerIDs) == 0 { return nil } log.Infof("find %d answers need to be deleted for user %s", len(answerIDs), userID) // delete all question session = ar.data.DB.Context(ctx).Where("user_id = ?", userID) session.Where("status != ?", entity.AnswerStatusDeleted) _, err = session.Cols("status", "updated_at").Update(&entity.Answer{ UpdatedAt: time.Now(), Status: entity.AnswerStatusDeleted, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // update search content for _, id := range answerIDs { _ = ar.updateSearch(ctx, id) } return nil } // UpdateAnswer update answer func (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, cols []string) (err error) { answer.ID = uid.DeShortID(answer.ID) answer.QuestionID = uid.DeShortID(answer.QuestionID) _, err = ar.data.DB.Context(ctx).ID(answer.ID).Cols(cols...).Update(answer) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = ar.updateSearch(ctx, answer.ID) return err } func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answerID string, status int) (err error) { answerID = uid.DeShortID(answerID) _, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{Status: status}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = ar.updateSearch(ctx, answerID) return } // GetAnswer get answer one func (ar *answerRepo) GetAnswer(ctx context.Context, id string) ( answer *entity.Answer, exist bool, err error, ) { id = uid.DeShortID(id) answer = &entity.Answer{} exist, err = ar.data.DB.Context(ctx).ID(id).Get(answer) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { answer.ID = uid.EnShortID(answer.ID) answer.QuestionID = uid.EnShortID(answer.QuestionID) } return } // GetAnswerCount count answer func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) { var resp = new(entity.Answer) count, err = ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusAvailable).Count(resp) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetAnswerList get answer list all func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) { answerList = make([]*entity.Answer, 0) answer.ID = uid.DeShortID(answer.ID) answer.QuestionID = uid.DeShortID(answer.QuestionID) err = ar.data.DB.Context(ctx).Find(&answerList, answer) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range answerList { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return } // GetAnswerPage get answer page func (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error) { answer.ID = uid.DeShortID(answer.ID) answer.QuestionID = uid.DeShortID(answer.QuestionID) answerList = make([]*entity.Answer, 0) total, err = pager.Help(page, pageSize, &answerList, answer, ar.data.DB.Context(ctx)) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range answerList { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return } // UpdateAcceptedStatus update all accepted status of this question's answers func (ar *answerRepo) UpdateAcceptedStatus(ctx context.Context, acceptedAnswerID string, questionID string) error { acceptedAnswerID = uid.DeShortID(acceptedAnswerID) questionID = uid.DeShortID(questionID) // update all this question's answer accepted status to false _, err := ar.data.DB.Context(ctx).Where("question_id = ?", questionID).Cols("adopted").Update(&entity.Answer{ Accepted: schema.AnswerAcceptedFailed, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // if acceptedAnswerID is not empty, update accepted status to true if len(acceptedAnswerID) > 0 && acceptedAnswerID != "0" { _, err = ar.data.DB.Context(ctx).Where("id = ?", acceptedAnswerID).Cols("adopted").Update(&entity.Answer{ Accepted: schema.AnswerAcceptedEnable, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } } _ = ar.updateSearch(ctx, acceptedAnswerID) return nil } // GetByID func (ar *answerRepo) GetByID(ctx context.Context, answerID string) (*entity.Answer, bool, error) { var resp entity.Answer answerID = uid.DeShortID(answerID) has, err := ar.data.DB.Context(ctx).ID(answerID).Get(&resp) if err != nil { return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { resp.ID = uid.EnShortID(resp.ID) resp.QuestionID = uid.EnShortID(resp.QuestionID) } return &resp, has, nil } func (ar *answerRepo) GetByIDs(ctx context.Context, answerIDs ...string) ([]*entity.Answer, error) { for idx, answerID := range answerIDs { answerIDs[idx] = uid.DeShortID(answerID) } var resp = make([]*entity.Answer, 0) err := ar.data.DB.Context(ctx).In("id", answerIDs).Find(&resp) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range resp { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return resp, nil } func (ar *answerRepo) GetCountByQuestionID(ctx context.Context, questionID string) (int64, error) { questionID = uid.DeShortID(questionID) var resp = new(entity.Answer) count, err := ar.data.DB.Context(ctx).Where("question_id =? and status = ?", questionID, entity.AnswerStatusAvailable).Count(resp) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } func (ar *answerRepo) GetCountByUserID(ctx context.Context, userID string) (int64, error) { var resp = new(entity.Answer) count, err := ar.data.DB.Context(ctx).Where(" user_id = ? and status = ?", userID, entity.AnswerStatusAvailable).Count(resp) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } func (ar *answerRepo) GetIDsByUserIDAndQuestionID(ctx context.Context, userID string, questionID string) ([]string, error) { questionID = uid.DeShortID(questionID) var ids []string resp := make([]string, 0) err := ar.data.DB.Context(ctx).Table(entity.Answer{}.TableName()).Where("question_id =? and user_id = ? and status = ?", questionID, userID, entity.AnswerStatusAvailable).OrderBy("created_at ASC").Cols("id").Find(&ids) if err != nil { return resp, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, id := range ids { resp = append(resp, uid.EnShortID(id)) } } else { resp = ids } return resp, nil } // SearchList func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) { if search.QuestionID != "" { search.QuestionID = uid.DeShortID(search.QuestionID) } search.ID = uid.DeShortID(search.ID) var count int64 var err error rows := make([]*entity.Answer, 0) if search.Page > 0 { search.Page-- } else { search.Page = 0 } if search.PageSize == 0 { search.PageSize = constant.DefaultPageSize } offset := search.Page * search.PageSize session := ar.data.DB.Context(ctx) if search.QuestionID != "" { session = session.And("question_id = ?", search.QuestionID) } if len(search.UserID) > 0 { session = session.And("user_id = ?", search.UserID) } switch search.Order { case entity.AnswerSearchOrderByTime: session = session.OrderBy("created_at desc") case entity.AnswerSearchOrderByTimeAsc: session = session.OrderBy("created_at asc") case entity.AnswerSearchOrderByVote: session = session.OrderBy("vote_count desc") default: session = session.OrderBy("adopted desc,vote_count desc,created_at asc") } if !search.IncludeDeleted { if search.LoginUserID == "" { session = session.And("status = ? ", entity.AnswerStatusAvailable) } else { session = session.And("status = ? OR user_id = ?", entity.AnswerStatusAvailable, search.LoginUserID) } } session = session.Limit(search.PageSize, offset) count, err = session.FindAndCount(&rows) if err != nil { return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range rows { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return rows, count, nil } // GetPersonalAnswerPage personal answer page func (ar *answerRepo) GetPersonalAnswerPage(ctx context.Context, req *entity.PersonalAnswerPageQueryCond) ( resp []*entity.Answer, total int64, err error) { cond := &entity.Answer{ UserID: req.UserID, } session := ar.data.DB.Context(ctx) switch req.Order { case entity.AnswerSearchOrderByTime: session = session.OrderBy("created_at desc") case entity.AnswerSearchOrderByTimeAsc: session = session.OrderBy("created_at asc") case entity.AnswerSearchOrderByVote: session = session.OrderBy("vote_count desc") default: session = session.OrderBy("adopted desc,vote_count desc,created_at asc") } if req.ShowPending { session = session.And("status != ?", entity.AnswerStatusDeleted) } else { session = session.And("status = ?", entity.AnswerStatusAvailable) } resp = make([]*entity.Answer, 0) total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session) if err != nil { return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range resp { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return resp, total, nil } func (ar *answerRepo) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) ( resp []*entity.Answer, total int64, err error) { cond := &entity.Answer{} session := ar.data.DB.Context(ctx) if len(req.QuestionID) == 0 && len(req.AnswerID) == 0 { session.Join("INNER", "question", "answer.question_id = question.id") if len(req.QuestionTitle) > 0 { session.Where("question.title like ?", "%"+req.QuestionTitle+"%") } } if len(req.AnswerID) > 0 { cond.ID = req.AnswerID } if len(req.QuestionID) > 0 { session.Where("answer.question_id = ?", req.QuestionID) } if req.Status > 0 { cond.Status = req.Status } session.Desc("answer.created_at") resp = make([]*entity.Answer, 0) total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session) if err != nil { return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return resp, total, nil } // SumVotesByQuestionID sum votes by question id func (ar *answerRepo) SumVotesByQuestionID(ctx context.Context, questionID string) (float64, error) { questionID = uid.DeShortID(questionID) var resp entity.Answer count, err := ar.data.DB.Context(ctx).Where("question_id = ? and status = ?", questionID, entity.AnswerStatusAvailable).Sum(&resp, "vote_count") if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } // updateSearch update search, if search plugin not enable, do nothing func (ar *answerRepo) updateSearch(ctx context.Context, answerID string) (err error) { answerID = uid.DeShortID(answerID) // check search plugin var ( s plugin.Search ) _ = plugin.CallSearch(func(search plugin.Search) error { s = search return nil }) if s == nil { return } answer, exist, err := ar.GetAnswer(ctx, answerID) if !exist { return } if err != nil { return err } // get question var ( question = new(entity.Question) ) exist, err = ar.data.DB.Context(ctx).Where("id = ?", answer.QuestionID).Get(&question) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return } // get tags var ( tagListList = make([]*entity.TagRel, 0) tags = make([]string, 0) ) st := ar.data.DB.Context(ctx).Where("object_id = ?", uid.DeShortID(question.ID)) st.Where("status = ?", entity.TagRelStatusAvailable) err = st.Find(&tagListList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } for _, tag := range tagListList { tags = append(tags, tag.TagID) } content := &plugin.SearchContent{ ObjectID: answerID, Title: question.Title, Type: constant.AnswerObjectType, Content: answer.OriginalText, Answers: 0, Status: plugin.SearchContentStatus(answer.Status), Tags: tags, QuestionID: answer.QuestionID, UserID: answer.UserID, Views: int64(question.ViewCount), Created: answer.CreatedAt.Unix(), Active: answer.UpdatedAt.Unix(), Score: int64(answer.VoteCount), HasAccepted: answer.Accepted == schema.AnswerAcceptedEnable, } err = s.UpdateContent(ctx, content) return } func (ar *answerRepo) DeletePermanentlyAnswers(ctx context.Context) error { // get all deleted answers ids ids := make([]string, 0) err := ar.data.DB.Context(ctx).Select("id").Table(new(entity.Answer).TableName()). Where("status = ?", entity.AnswerStatusDeleted).Find(&ids) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if len(ids) == 0 { return nil } // delete all revisions permanently _, err = ar.data.DB.Context(ctx).In("object_id", ids).Delete(&entity.Revision{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _, err = ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusDeleted).Delete(&entity.Answer{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } ================================================ FILE: internal/repo/api_key/api_key_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package api_key import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/apikey" "github.com/segmentfault/pacman/errors" ) type apiKeyRepo struct { data *data.Data } // NewAPIKeyRepo creates a new apiKey repository func NewAPIKeyRepo(data *data.Data) apikey.APIKeyRepo { return &apiKeyRepo{ data: data, } } func (ar *apiKeyRepo) GetAPIKeyList(ctx context.Context) (keys []*entity.APIKey, err error) { keys = make([]*entity.APIKey, 0) err = ar.data.DB.Context(ctx).Where("hidden = ?", 0).Find(&keys) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (ar *apiKeyRepo) GetAPIKey(ctx context.Context, apiKey string) (key *entity.APIKey, exist bool, err error) { key = &entity.APIKey{} exist, err = ar.data.DB.Context(ctx).Where("access_key = ?", apiKey).Get(key) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (ar *apiKeyRepo) UpdateAPIKey(ctx context.Context, apiKey entity.APIKey) (err error) { _, err = ar.data.DB.Context(ctx).ID(apiKey.ID).Update(&apiKey) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (ar *apiKeyRepo) AddAPIKey(ctx context.Context, apiKey entity.APIKey) (err error) { _, err = ar.data.DB.Context(ctx).Insert(&apiKey) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (ar *apiKeyRepo) DeleteAPIKey(ctx context.Context, id int) (err error) { _, err = ar.data.DB.Context(ctx).ID(id).Delete(&entity.APIKey{}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/auth/auth.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package auth import ( "context" "encoding/json" "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // authRepo auth repository type authRepo struct { data *data.Data } // NewAuthRepo new repository func NewAuthRepo(data *data.Data) auth.AuthRepo { return &authRepo{ data: data, } } // GetUserCacheInfo get user cache info func (ar *authRepo) GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.UserTokenCacheKey+accessToken) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return nil, nil } userInfo = &entity.UserCacheInfo{} _ = json.Unmarshal([]byte(userInfoCache), userInfo) return userInfo, nil } // SetUserCacheInfo set user cache info func (ar *authRepo) SetUserCacheInfo(ctx context.Context, accessToken, visitToken string, userInfo *entity.UserCacheInfo) (err error) { userInfo.VisitToken = visitToken userInfoCache, err := json.Marshal(userInfo) if err != nil { return err } err = ar.data.Cache.SetString(ctx, constant.UserTokenCacheKey+accessToken, string(userInfoCache), constant.UserTokenCacheTime) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if err := ar.AddUserTokenMapping(ctx, userInfo.UserID, accessToken); err != nil { log.Error(err) } if len(visitToken) == 0 { return nil } if err := ar.data.Cache.SetString(ctx, constant.UserVisitTokenCacheKey+visitToken, accessToken, constant.UserTokenCacheTime); err != nil { log.Error(err) } return nil } // GetUserVisitCacheInfo get user visit cache info func (ar *authRepo) GetUserVisitCacheInfo(ctx context.Context, visitToken string) (accessToken string, err error) { accessToken, exist, err := ar.data.Cache.GetString(ctx, constant.UserVisitTokenCacheKey+visitToken) if err != nil { return "", errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return "", nil } return accessToken, nil } // RemoveUserCacheInfo remove user cache info func (ar *authRepo) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) { err = ar.data.Cache.Del(ctx, constant.UserTokenCacheKey+accessToken) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // RemoveUserVisitCacheInfo remove visit token cache func (ar *authRepo) RemoveUserVisitCacheInfo(ctx context.Context, visitToken string) (err error) { err = ar.data.Cache.Del(ctx, constant.UserVisitTokenCacheKey+visitToken) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // SetUserStatus set user status func (ar *authRepo) SetUserStatus(ctx context.Context, userID string, userInfo *entity.UserCacheInfo) (err error) { userInfoCache, err := json.Marshal(userInfo) if err != nil { return err } err = ar.data.Cache.SetString(ctx, constant.UserStatusChangedCacheKey+userID, string(userInfoCache), constant.UserStatusChangedCacheTime) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // GetUserStatus get user status func (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) { userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.UserStatusChangedCacheKey+userID) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return nil, nil } userInfo = &entity.UserCacheInfo{} _ = json.Unmarshal([]byte(userInfoCache), userInfo) return userInfo, nil } // RemoveUserStatus remove user status func (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err error) { err = ar.data.Cache.Del(ctx, constant.UserStatusChangedCacheKey+userID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // GetAdminUserCacheInfo get admin user cache info func (ar *authRepo) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.AdminTokenCacheKey+accessToken) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } if !exist { return nil, nil } userInfo = &entity.UserCacheInfo{} _ = json.Unmarshal([]byte(userInfoCache), userInfo) return userInfo, nil } // SetAdminUserCacheInfo set admin user cache info func (ar *authRepo) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { userInfoCache, err := json.Marshal(userInfo) if err != nil { return err } err = ar.data.Cache.SetString(ctx, constant.AdminTokenCacheKey+accessToken, string(userInfoCache), constant.AdminTokenCacheTime) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // RemoveAdminUserCacheInfo remove admin user cache info func (ar *authRepo) RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) { err = ar.data.Cache.Del(ctx, constant.AdminTokenCacheKey+accessToken) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // AddUserTokenMapping add user token mapping func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) { key := constant.UserTokenMappingCacheKey + userID resp, _, err := ar.data.Cache.GetString(ctx, key) if err != nil { return err } mapping := make(map[string]bool, 0) if len(resp) > 0 { _ = json.Unmarshal([]byte(resp), &mapping) } mapping[accessToken] = true content, _ := json.Marshal(mapping) return ar.data.Cache.SetString(ctx, key, string(content), constant.UserTokenCacheTime) } // RemoveUserTokens Log out all users under this user id func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) { key := constant.UserTokenMappingCacheKey + userID resp, _, err := ar.data.Cache.GetString(ctx, key) if err != nil { return } mapping := make(map[string]bool, 0) if len(resp) > 0 { _ = json.Unmarshal([]byte(resp), &mapping) log.Debugf("find %d user tokens by user id %s", len(mapping), userID) } for token := range mapping { if token == remainToken { continue } if err := ar.RemoveUserCacheInfo(ctx, token); err != nil { log.Error(err) } else { log.Debugf("del user %s token success", userID) } } if err := ar.RemoveUserStatus(ctx, userID); err != nil { log.Error(err) } if err := ar.data.Cache.Del(ctx, key); err != nil { log.Error(err) } } ================================================ FILE: internal/repo/badge/badge_event_rule.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package badge import ( "context" "strconv" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/badge" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // eventRuleRepo event rule repo type eventRuleRepo struct { data *data.Data EventRuleMapping map[constant.EventType][]badge.EventRuleHandler } // NewEventRuleRepo creates a new badge repository func NewEventRuleRepo(data *data.Data) badge.EventRuleRepo { b := &eventRuleRepo{ data: data, } b.EventRuleMapping = map[constant.EventType][]badge.EventRuleHandler{ constant.EventUserUpdate: {b.FirstUpdateUserProfile}, constant.EventUserShare: {b.FirstSharedPost}, constant.EventQuestionCreate: nil, constant.EventQuestionUpdate: {b.FirstPostEdit}, constant.EventQuestionDelete: nil, constant.EventQuestionVote: {b.FirstVotedPost, b.ReachQuestionVote}, constant.EventQuestionAccept: {b.FirstAcceptAnswer, b.ReachAnswerAcceptedAmount}, constant.EventQuestionFlag: {b.FirstFlaggedPost}, constant.EventQuestionReact: {b.FirstReactedPost}, constant.EventAnswerCreate: nil, constant.EventAnswerUpdate: {b.FirstPostEdit}, constant.EventAnswerDelete: nil, constant.EventAnswerVote: {b.FirstVotedPost, b.ReachAnswerVote}, constant.EventAnswerFlag: {b.FirstFlaggedPost}, constant.EventAnswerReact: {b.FirstReactedPost}, constant.EventCommentCreate: nil, constant.EventCommentUpdate: nil, constant.EventCommentDelete: nil, constant.EventCommentVote: {b.FirstVotedPost}, constant.EventCommentFlag: {b.FirstFlaggedPost}, } return b } // HandleEventWithRule handle event with rule func (br *eventRuleRepo) HandleEventWithRule(ctx context.Context, msg *schema.EventMsg) ( awards []*entity.BadgeAward) { handlers := br.EventRuleMapping[msg.EventType] for _, handler := range handlers { t, err := handler(ctx, msg) if err != nil { log.Errorf("error handling badge event %+v: %v", msg, err) } else { awards = append(awards, t...) } } return awards } // FirstUpdateUserProfile first update user profile func (br *eventRuleRepo) FirstUpdateUserProfile(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { badges := br.getBadgesByHandler(ctx, "FirstUpdateUserProfile") for _, b := range badges { bean := &entity.User{ID: event.UserID} exist, err := br.data.DB.Context(ctx).Get(bean) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { continue } if len(bean.Bio) > 0 { awards = append(awards, br.createBadgeAward(event.UserID, entity.BadgeEmptyAwardKey, b)) } } return awards, nil } // FirstPostEdit first post edit func (br *eventRuleRepo) FirstPostEdit(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { badges := br.getBadgesByHandler(ctx, "FirstPostEdit") for _, b := range badges { awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } return awards, nil } // FirstFlaggedPost first flagged post. func (br *eventRuleRepo) FirstFlaggedPost(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { badges := br.getBadgesByHandler(ctx, "FirstFlaggedPost") for _, b := range badges { awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } return awards, nil } // FirstVotedPost first voted post func (br *eventRuleRepo) FirstVotedPost(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { badges := br.getBadgesByHandler(ctx, "FirstVotedPost") for _, b := range badges { awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } return awards, nil } // FirstReactedPost first reacted post func (br *eventRuleRepo) FirstReactedPost(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { badges := br.getBadgesByHandler(ctx, "FirstReactedPost") for _, b := range badges { awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } return awards, nil } // FirstSharedPost first shared post func (br *eventRuleRepo) FirstSharedPost(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { badges := br.getBadgesByHandler(ctx, "FirstSharedPost") for _, b := range badges { awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } return awards, nil } // FirstAcceptAnswer user first accept answer func (br *eventRuleRepo) FirstAcceptAnswer(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { badges := br.getBadgesByHandler(ctx, "FirstAcceptAnswer") for _, b := range badges { awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } return awards, nil } // ReachAnswerAcceptedAmount reach answer accepted amount func (br *eventRuleRepo) ReachAnswerAcceptedAmount(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { badges := br.getBadgesByHandler(ctx, "ReachAnswerAcceptedAmount") if len(event.AnswerUserID) == 0 { return nil, nil } // count user's accepted answer amount amount, err := br.data.DB.Context(ctx).Count(&entity.Answer{ UserID: event.AnswerUserID, Accepted: schema.AnswerAcceptedEnable, Status: entity.AnswerStatusAvailable, }) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } for _, b := range badges { // get badge requirement requirement := b.GetIntParam("amount") if requirement == 0 || amount < requirement { continue } awards = append(awards, br.createBadgeAward(event.AnswerUserID, event.AnswerID, b)) } return awards, nil } // ReachAnswerVote reach answer vote func (br *eventRuleRepo) ReachAnswerVote(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { badges := br.getBadgesByHandler(ctx, "ReachAnswerVote") // get vote amount amount, _ := strconv.Atoi(event.GetExtra("vote_up_amount")) if amount == 0 { return nil, nil } for _, b := range badges { // get badge requirement requirement := b.GetIntParam("amount") if requirement == 0 || int64(amount) < requirement { continue } awards = append(awards, br.createBadgeAward(event.AnswerUserID, event.AnswerID, b)) } return awards, nil } // ReachQuestionVote reach question vote func (br *eventRuleRepo) ReachQuestionVote(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { badges := br.getBadgesByHandler(ctx, "ReachQuestionVote") // get vote amount amount, _ := strconv.Atoi(event.GetExtra("vote_up_amount")) if amount == 0 { return nil, nil } for _, b := range badges { // get badge requirement requirement := b.GetIntParam("amount") if requirement == 0 || int64(amount) < requirement { continue } awards = append(awards, br.createBadgeAward(event.QuestionUserID, event.QuestionID, b)) } return awards, nil } func (br *eventRuleRepo) getBadgesByHandler(ctx context.Context, handler string) (badges []*entity.Badge) { badges = make([]*entity.Badge, 0) err := br.data.DB.Context(ctx).Where("handler = ?", handler).Find(&badges) if err != nil { log.Errorf("error getting badge by handler %s: %v", handler, err) return nil } return badges } func (br *eventRuleRepo) createBadgeAward(userID, awardKey string, badge *entity.Badge) (awards *entity.BadgeAward) { return &entity.BadgeAward{ UserID: userID, BadgeID: badge.ID, AwardKey: awardKey, } } ================================================ FILE: internal/repo/badge/badge_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package badge import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/badge" "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) type badgeRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } // NewBadgeRepo creates a new badge repository func NewBadgeRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeRepo { return &badgeRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } func (r *badgeRepo) GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) { badge = &entity.Badge{} exists, err = r.data.DB.Context(ctx).Where("id = ?", id).Get(badge) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (r *badgeRepo) GetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error) { badges = make([]*entity.Badge, 0) err = r.data.DB.Context(ctx).In("id", ids).Find(&badges) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // ListPaged returns a list of activated badges func (r *badgeRepo) ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { badges = make([]*entity.Badge, 0) total = 0 session := r.data.DB.Context(ctx).Where("status <> ?", entity.BadgeStatusDeleted) if page == 0 || pageSize == 0 { err = session.Find(&badges) } else { total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) } if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // ListActivated returns a list of activated badges func (r *badgeRepo) ListActivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { badges = make([]*entity.Badge, 0) total = 0 session := r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusActive) if page == 0 || pageSize == 0 { err = session.Find(&badges) } else { total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) } if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // ListInactivated returns a list of inactivated badges func (r *badgeRepo) ListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { badges = make([]*entity.Badge, 0) total = 0 session := r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusInactive) if page == 0 || pageSize == 0 { err = session.Find(&badges) } else { total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) } if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateStatus updates the award count of a badge func (r *badgeRepo) UpdateStatus(ctx context.Context, id string, status int8) (err error) { _, err = r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { _, err = session.ID(id).Update(&entity.Badge{ Status: status, }) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(session.Rollback()).WithStack() return } if status >= entity.BadgeStatusDeleted { _, err = session.Where("badge_id = ?", id).Cols("is_badge_deleted").Update(&entity.BadgeAward{ IsBadgeDeleted: entity.IsBadgeDeleted, }) } else { _, err = session.Where("badge_id = ?", id).Cols("is_badge_deleted").Update(&entity.BadgeAward{ IsBadgeDeleted: entity.IsBadgeNotDeleted, }) } return }) return } // UpdateAwardCount updates the award count of a badge func (r *badgeRepo) UpdateAwardCount(ctx context.Context, badgeID string, awardCount int) (err error) { _, err = r.data.DB.Context(ctx).ID(badgeID).Cols("award_count").Update(&entity.Badge{AwardCount: awardCount}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/badge_award/badge_award_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package badge_award import ( "context" "fmt" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/badge" "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) type badgeAwardRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } func NewBadgeAwardRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeAwardRepo { return &badgeAwardRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } // AwardBadgeForUser award badge for user func (r *badgeAwardRepo) AwardBadgeForUser(ctx context.Context, badgeAward *entity.BadgeAward) (err error) { badgeAward.ID, err = r.uniqueIDRepo.GenUniqueIDStr(ctx, entity.BadgeAward{}.TableName()) if err != nil { return err } _, err = r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) badgeInfo := &entity.Badge{} exist, err := session.ID(badgeAward.BadgeID).ForUpdate().Get(badgeInfo) if err != nil { return nil, err } if !exist { return nil, fmt.Errorf("badge not exist") } old := &entity.BadgeAward{ UserID: badgeAward.UserID, BadgeID: badgeAward.BadgeID, IsBadgeDeleted: entity.IsBadgeNotDeleted, } if badgeInfo.Single != entity.BadgeSingleAward { old.AwardKey = badgeAward.AwardKey } exist, err = session.Get(old) if err != nil { return nil, err } if exist { return nil, fmt.Errorf("badge already awarded") } _, err = session.Insert(badgeAward) if err != nil { return nil, err } return session.ID(badgeInfo.ID).Incr("award_count", 1).Update(&entity.Badge{}) }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // CheckIsAward check this badge is awarded for this user or not func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID, userID, awardKey string, singleOrMulti int8) ( isAward bool, err error) { if singleOrMulti == entity.BadgeSingleAward { _, isAward, err = r.GetByUserIdAndBadgeId(ctx, userID, badgeID) } else { _, isAward, err = r.GetByUserIdAndBadgeIdAndAwardKey(ctx, userID, badgeID, awardKey) } if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return isAward, err } func (r *badgeAwardRepo) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) { awardCount, err := r.data.DB.Context(ctx).Where("user_id = ? AND badge_id = ?", userID, badgeID).Count(&entity.BadgeAward{}) if err != nil { return 0 } return } func (r *badgeAwardRepo) CountByBadgeID(ctx context.Context, badgeID string) (awardCount int64, err error) { awardCount, err = r.data.DB.Context(ctx).Count(&entity.BadgeAward{BadgeID: badgeID}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (r *badgeAwardRepo) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) { err = r.data.DB.Context(ctx).Select("badge_id, count(`id`) AS earned_count").Where("user_id = ?", userID).GroupBy("badge_id").Find(&earnedCounts) return } // ListPagedByBadgeId list badge awards by badge id func (r *badgeAwardRepo) ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) { session := r.data.DB.Context(ctx) session.Where("badge_id = ?", badgeID) total, err = pager.Help(page, pageSize, &badgeAwardList, &entity.BadgeAward{}, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // ListPagedByBadgeIdAndUserId list badge awards by badge id and user id func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) { session := r.data.DB.Context(ctx) session.Where("badge_id = ? AND user_id = ?", badgeID, userID) total, err = pager.Help(page, pageSize, &badgeAwardList, &entity.Question{}, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // ListNewestEarned list newest earned badge awards func (r *badgeAwardRepo) ListNewestEarned(ctx context.Context, userID string, limit int) (badgeAwards []*entity.BadgeAwardRecent, err error) { badgeAwards = make([]*entity.BadgeAwardRecent, 0) err = r.data.DB.Context(ctx). Select("badge_id, max(created_at) created,count(*) earned_count"). Where("user_id = ? AND is_badge_deleted = ? ", userID, entity.IsBadgeNotDeleted). GroupBy("badge_id"). OrderBy("created desc"). Limit(limit).Find(&badgeAwards) return } // GetByUserIdAndBadgeId get badge award by user id and badge id func (r *badgeAwardRepo) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) ( badgeAward *entity.BadgeAward, exists bool, err error) { badgeAward = &entity.BadgeAward{} exists, err = r.data.DB.Context(ctx). Where("user_id = ? AND badge_id = ? AND is_badge_deleted = 0", userID, badgeID).Get(badgeAward) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetByUserIdAndBadgeIdAndAwardKey get badge award by user id and badge id and award key func (r *badgeAwardRepo) GetByUserIdAndBadgeIdAndAwardKey(ctx context.Context, userID string, badgeID string, awardKey string) ( badgeAward *entity.BadgeAward, exists bool, err error) { badgeAward = &entity.BadgeAward{} exists, err = r.data.DB.Context(ctx). Where("user_id = ? AND badge_id = ? AND award_key = ? AND is_badge_deleted = 0", userID, badgeID, awardKey).Get(badgeAward) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // DeleteUserBadgeAward delete user badge award func (r *badgeAwardRepo) DeleteUserBadgeAward(ctx context.Context, userID string) (err error) { _, err = r.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.BadgeAward{}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/badge_group/badge_group_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package badge_group import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/badge" "github.com/apache/answer/internal/service/unique" ) type badgeGroupRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } func NewBadgeGroupRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeGroupRepo { return &badgeGroupRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } func (r *badgeGroupRepo) ListGroups(ctx context.Context) (groups []*entity.BadgeGroup, err error) { groups = make([]*entity.BadgeGroup, 0) err = r.data.DB.Context(ctx).Find(&groups) return } func (r *badgeGroupRepo) AddGroup(ctx context.Context, group *entity.BadgeGroup) (err error) { return } ================================================ FILE: internal/repo/captcha/captcha.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package captcha import ( "context" "encoding/json" "fmt" "time" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/action" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // captchaRepo captcha repository type captchaRepo struct { data *data.Data } // NewCaptchaRepo new repository func NewCaptchaRepo(data *data.Data) action.CaptchaRepo { return &captchaRepo{ data: data, } } func (cr *captchaRepo) SetActionType(ctx context.Context, unit, actionType, config string, amount int) (err error) { now := time.Now() cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02")) value := &entity.ActionRecordInfo{} value.LastTime = now.Unix() value.Num = amount valueStr, err := json.Marshal(value) if err != nil { return nil } err = cr.data.Cache.SetString(ctx, cacheKey, string(valueStr), 6*time.Minute) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (cr *captchaRepo) GetActionType(ctx context.Context, unit, actionType string) (actionInfo *entity.ActionRecordInfo, err error) { now := time.Now() cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02")) res, exist, err := cr.data.Cache.GetString(ctx, cacheKey) if err != nil { return nil, err } if !exist { return nil, nil } actionInfo = &entity.ActionRecordInfo{} _ = json.Unmarshal([]byte(res), actionInfo) return actionInfo, nil } func (cr *captchaRepo) DelActionType(ctx context.Context, unit, actionType string) (err error) { now := time.Now() cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02")) err = cr.data.Cache.Del(ctx, cacheKey) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // SetCaptcha set captcha to cache func (cr *captchaRepo) SetCaptcha(ctx context.Context, key, captcha string) (err error) { err = cr.data.Cache.SetString(ctx, key, captcha, 6*time.Minute) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetCaptcha get captcha from cache func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha string, err error) { captcha, exist, err := cr.data.Cache.GetString(ctx, key) if err != nil { return "", err } if !exist { return "", fmt.Errorf("captcha not exist") } return captcha, nil } func (cr *captchaRepo) DelCaptcha(ctx context.Context, key string) (err error) { err = cr.data.Cache.Del(ctx, key) if err != nil { log.Debug(err) } return nil } ================================================ FILE: internal/repo/collection/collection_group_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package collection import ( "context" "github.com/apache/answer/internal/service/collection" "xorm.io/xorm" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/errors" ) // collectionGroupRepo collectionGroup repository type collectionGroupRepo struct { data *data.Data } // NewCollectionGroupRepo new repository func NewCollectionGroupRepo(data *data.Data) collection.CollectionGroupRepo { return &collectionGroupRepo{ data: data, } } // AddCollectionGroup add collection group func (cr *collectionGroupRepo) AddCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup) (err error) { _, err = cr.data.DB.Context(ctx).Insert(collectionGroup) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // AddCollectionDefaultGroup add collection group func (cr *collectionGroupRepo) AddCollectionDefaultGroup(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error) { defaultGroup := &entity.CollectionGroup{ Name: "default", DefaultGroup: schema.CGDefault, UserID: userID, } _, err = cr.data.DB.Context(ctx).Insert(defaultGroup) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } collectionGroup = defaultGroup return } // CreateDefaultGroupIfNotExist create default group if not exist func (cr *collectionGroupRepo) CreateDefaultGroupIfNotExist(ctx context.Context, userID string) ( collectionGroup *entity.CollectionGroup, err error) { _, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) old := &entity.CollectionGroup{ UserID: userID, DefaultGroup: schema.CGDefault, } exist, err := session.ForUpdate().Get(old) if err != nil { return nil, err } if exist { collectionGroup = old return old, nil } defaultGroup := &entity.CollectionGroup{ Name: "default", DefaultGroup: schema.CGDefault, UserID: userID, } _, err = session.Insert(defaultGroup) if err != nil { return nil, err } collectionGroup = defaultGroup return nil, nil }) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return collectionGroup, nil } // UpdateCollectionGroup update collection group func (cr *collectionGroupRepo) UpdateCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup, cols []string) (err error) { _, err = cr.data.DB.Context(ctx).ID(collectionGroup.ID).Cols(cols...).Update(collectionGroup) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetCollectionGroup get collection group one func (cr *collectionGroupRepo) GetCollectionGroup(ctx context.Context, id string) ( collectionGroup *entity.CollectionGroup, exist bool, err error, ) { collectionGroup = &entity.CollectionGroup{} exist, err = cr.data.DB.Context(ctx).ID(id).Get(collectionGroup) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetCollectionGroupPage get collection group page func (cr *collectionGroupRepo) GetCollectionGroupPage(ctx context.Context, page, pageSize int, collectionGroup *entity.CollectionGroup) (collectionGroupList []*entity.CollectionGroup, total int64, err error) { collectionGroupList = make([]*entity.CollectionGroup, 0) session := cr.data.DB.Context(ctx) if collectionGroup.UserID != "" && collectionGroup.UserID != "0" { session = session.Where("user_id = ?", collectionGroup.UserID) } session = session.OrderBy("update_time desc") total, err = pager.Help(page, pageSize, collectionGroupList, collectionGroup, session) err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } func (cr *collectionGroupRepo) GetDefaultID(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, has bool, err error) { collectionGroup = &entity.CollectionGroup{} has, err = cr.data.DB.Context(ctx).Where("user_id =? and default_group = ?", userID, schema.CGDefault).Get(collectionGroup) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } return } ================================================ FILE: internal/repo/collection/collection_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package collection import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" collectioncommon "github.com/apache/answer/internal/service/collection_common" "github.com/apache/answer/internal/service/unique" "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) // collectionRepo collection repository type collectionRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } // NewCollectionRepo new repository func NewCollectionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) collectioncommon.CollectionRepo { return &collectionRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } // AddCollection add collection func (cr *collectionRepo) AddCollection(ctx context.Context, collection *entity.Collection) (err error) { collection.ID, err = cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName()) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) old := &entity.Collection{ UserID: collection.UserID, ObjectID: collection.ObjectID, } exist, err := session.ForUpdate().Get(old) if err != nil { return nil, err } if exist { return nil, nil } _, err = session.Insert(collection) if err != nil { return nil, err } return }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // RemoveCollection delete collection func (cr *collectionRepo) RemoveCollection(ctx context.Context, id string) (err error) { _, err = cr.data.DB.Context(ctx).Where("id = ?", id).Delete(&entity.Collection{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // UpdateCollection update collection func (cr *collectionRepo) UpdateCollection(ctx context.Context, collection *entity.Collection, cols []string) (err error) { _, err = cr.data.DB.Context(ctx).ID(collection.ID).Cols(cols...).Update(collection) return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // GetCollection get collection one func (cr *collectionRepo) GetCollection(ctx context.Context, id int) (collection *entity.Collection, exist bool, err error) { collection = &entity.Collection{} exist, err = cr.data.DB.Context(ctx).ID(id).Get(collection) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetCollectionList get collection list all func (cr *collectionRepo) GetCollectionList(ctx context.Context, collection *entity.Collection) (collectionList []*entity.Collection, err error) { collectionList = make([]*entity.Collection, 0) err = cr.data.DB.Context(ctx).Find(&collectionList, collection) err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } // GetOneByObjectIDAndUser get one by object TagID and user func (cr *collectionRepo) GetOneByObjectIDAndUser(ctx context.Context, userID string, objectID string) (collection *entity.Collection, exist bool, err error) { collection = &entity.Collection{} exist, err = cr.data.DB.Context(ctx).Where("user_id = ? and object_id = ?", userID, objectID).Get(collection) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // SearchByObjectIDsAndUser search by object IDs and user func (cr *collectionRepo) SearchByObjectIDsAndUser(ctx context.Context, userID string, objectIDs []string) ([]*entity.Collection, error) { collectionList := make([]*entity.Collection, 0) err := cr.data.DB.Context(ctx).Where("user_id = ?", userID).In("object_id", objectIDs).Find(&collectionList) if err != nil { return collectionList, err } return collectionList, nil } // CountByObjectID count by object TagID func (cr *collectionRepo) CountByObjectID(ctx context.Context, objectID string) (total int64, err error) { collection := &entity.Collection{} total, err = cr.data.DB.Context(ctx).Where("object_id = ?", objectID).Count(collection) if err != nil { return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetCollectionPage get collection page func (cr *collectionRepo) GetCollectionPage(ctx context.Context, page, pageSize int, collection *entity.Collection) (collectionList []*entity.Collection, total int64, err error) { collectionList = make([]*entity.Collection, 0) session := cr.data.DB.Context(ctx) if collection.UserID != "" && collection.UserID != "0" { session = session.Where("user_id = ?", collection.UserID) } if collection.UserCollectionGroupID != "" && collection.UserCollectionGroupID != "0" { session = session.Where("user_collection_group_id = ?", collection.UserCollectionGroupID) } session = session.OrderBy("update_time desc") total, err = pager.Help(page, pageSize, collectionList, collection, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // SearchObjectCollected check object is collected or not func (cr *collectionRepo) SearchObjectCollected(ctx context.Context, userID string, objectIds []string) (map[string]bool, error) { for i := range objectIds { objectIds[i] = uid.DeShortID(objectIds[i]) } list, err := cr.SearchByObjectIDsAndUser(ctx, userID, objectIds) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } collectedMap := make(map[string]bool) short := handler.GetEnableShortID(ctx) for _, item := range list { if short { item.ObjectID = uid.EnShortID(item.ObjectID) } collectedMap[item.ObjectID] = true } return collectedMap, nil } // SearchList func (cr *collectionRepo) SearchList(ctx context.Context, search *entity.CollectionSearch) ([]*entity.Collection, int64, error) { var count int64 var err error rows := make([]*entity.Collection, 0) if search.Page > 0 { search.Page-- } else { search.Page = 0 } if search.PageSize == 0 { search.PageSize = constant.DefaultPageSize } offset := search.Page * search.PageSize session := cr.data.DB.Context(ctx).Where("") if len(search.UserID) > 0 { session = session.And("user_id = ?", search.UserID) } else { return rows, count, nil } session = session.Limit(search.PageSize, offset) count, err = session.OrderBy("updated_at desc").FindAndCount(&rows) if err != nil { return rows, count, err } return rows, count, nil } ================================================ FILE: internal/repo/comment/comment_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package comment import ( "context" "github.com/segmentfault/pacman/log" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/comment" "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" ) // commentRepo comment repository type commentRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } // NewCommentRepo new repository func NewCommentRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) comment.CommentRepo { return &commentRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } // NewCommentCommonRepo new repository func NewCommentCommonRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) comment_common.CommentCommonRepo { return &commentRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } // AddComment add comment func (cr *commentRepo) AddComment(ctx context.Context, comment *entity.Comment) (err error) { comment.ID, err = cr.uniqueIDRepo.GenUniqueIDStr(ctx, comment.TableName()) if err != nil { return err } _, err = cr.data.DB.Context(ctx).Insert(comment) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // RemoveComment delete comment func (cr *commentRepo) RemoveComment(ctx context.Context, commentID string) (err error) { session := cr.data.DB.Context(ctx).ID(commentID) _, err = session.Update(&entity.Comment{Status: entity.CommentStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateCommentContent update comment func (cr *commentRepo) UpdateCommentContent( ctx context.Context, commentID string, originalText string, parsedText string) (err error) { _, err = cr.data.DB.Context(ctx).ID(commentID).Update(&entity.Comment{ OriginalText: originalText, ParsedText: parsedText, }) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateCommentStatus update comment status func (cr *commentRepo) UpdateCommentStatus(ctx context.Context, commentID string, status int) (err error) { _, err = cr.data.DB.Context(ctx).ID(commentID).Update(&entity.Comment{ Status: status, }) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetComment get comment one func (cr *commentRepo) GetComment(ctx context.Context, commentID string) ( comment *entity.Comment, exist bool, err error) { comment = &entity.Comment{} exist, err = cr.data.DB.Context(ctx).Where("status = ?", entity.CommentStatusAvailable).ID(commentID).Get(comment) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetCommentWithoutStatus get comment one without status func (cr *commentRepo) GetCommentWithoutStatus(ctx context.Context, commentID string) ( comment *entity.Comment, exist bool, err error) { comment = &entity.Comment{} exist, err = cr.data.DB.Context(ctx).ID(commentID).Get(comment) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (cr *commentRepo) GetCommentCount(ctx context.Context) (count int64, err error) { list := make([]*entity.Comment, 0) count, err = cr.data.DB.Context(ctx).Where("status = ?", entity.CommentStatusAvailable).FindAndCount(&list) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetCommentPage get comment page func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) ( commentList []*entity.Comment, total int64, err error, ) { commentList = make([]*entity.Comment, 0) session := cr.data.DB.Context(ctx) session.OrderBy(commentQuery.GetOrderBy()) session.Where("status = ?", entity.CommentStatusAvailable) cond := &entity.Comment{ObjectID: commentQuery.ObjectID, UserID: commentQuery.UserID} total, err = pager.Help(commentQuery.Page, commentQuery.PageSize, &commentList, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // RemoveAllUserComment remove all user comment func (cr *commentRepo) RemoveAllUserComment(ctx context.Context, userID string) (err error) { session := cr.data.DB.Context(ctx).Where("user_id = ?", userID) session.Where("status != ?", entity.CommentStatusDeleted) affected, err := session.Update(&entity.Comment{Status: entity.CommentStatusDeleted}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } log.Infof("delete user comment, userID: %s, affected: %d", userID, affected) return } ================================================ FILE: internal/repo/config/config_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package config import ( "context" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/config" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // configRepo config repository type configRepo struct { data *data.Data } // NewConfigRepo new repository func NewConfigRepo(data *data.Data) config.ConfigRepo { repo := &configRepo{ data: data, } return repo } func (cr configRepo) GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) { cacheKey := fmt.Sprintf("%s%d", constant.ConfigID2KEYCacheKeyPrefix, id) cacheData, exist, err := cr.data.Cache.GetString(ctx, cacheKey) if err == nil && exist && len(cacheData) > 0 { c = &entity.Config{} c.BuildByJSON([]byte(cacheData)) if c.ID > 0 { return c, nil } } c = &entity.Config{} exist, err = cr.data.DB.Context(ctx).ID(id).Get(c) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return nil, fmt.Errorf("config not found by id: %d", id) } // update cache if err := cr.data.Cache.SetString(ctx, cacheKey, c.JsonString(), constant.ConfigCacheTime); err != nil { log.Error(err) } return c, nil } func (cr configRepo) GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) { cacheKey := constant.ConfigKEY2ContentCacheKeyPrefix + key cacheData, exist, err := cr.data.Cache.GetString(ctx, cacheKey) if err == nil && exist && len(cacheData) > 0 { c = &entity.Config{} c.BuildByJSON([]byte(cacheData)) if c.ID > 0 { return c, nil } } c = &entity.Config{Key: key} exist, err = cr.data.DB.Context(ctx).Get(c) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return nil, fmt.Errorf("config not found by key: %s", key) } // update cache if err := cr.data.Cache.SetString(ctx, cacheKey, c.JsonString(), constant.ConfigCacheTime); err != nil { log.Error(err) } return c, nil } func (cr configRepo) GetConfigByKeyFromDB(ctx context.Context, key string) (c *entity.Config, err error) { c = &entity.Config{Key: key} exist, err := cr.data.DB.Context(ctx).Get(c) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return nil, fmt.Errorf("config not found by key: %s", key) } return c, nil } func (cr configRepo) UpdateConfig(ctx context.Context, key string, value string) (err error) { // check if key exists oldConfig := &entity.Config{Key: key} exist, err := cr.data.DB.Context(ctx).Get(oldConfig) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return errors.BadRequest(reason.ObjectNotFound) } // update database _, err = cr.data.DB.Context(ctx).ID(oldConfig.ID).Update(&entity.Config{Value: value}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } oldConfig.Value = value cacheVal := oldConfig.JsonString() // update cache if err := cr.data.Cache.SetString(ctx, constant.ConfigKEY2ContentCacheKeyPrefix+key, cacheVal, constant.ConfigCacheTime); err != nil { log.Error(err) } if err := cr.data.Cache.SetString(ctx, fmt.Sprintf("%s%d", constant.ConfigID2KEYCacheKeyPrefix, oldConfig.ID), cacheVal, constant.ConfigCacheTime); err != nil { log.Error(err) } return } ================================================ FILE: internal/repo/export/email_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package export import ( "context" "time" "github.com/apache/answer/internal/base/constant" "github.com/tidwall/gjson" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/service/export" "github.com/segmentfault/pacman/errors" ) // emailRepo email repository type emailRepo struct { data *data.Data } // NewEmailRepo new repository func NewEmailRepo(data *data.Data) export.EmailRepo { return &emailRepo{ data: data, } } // SetCode The email code is used to verify that the link in the message is out of date func (e *emailRepo) SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error { // Setting the latest code is to help ensure that only one link is active at a time. // Set userID -> latest code if err := e.data.Cache.SetString(ctx, constant.UserLatestEmailCodeCacheKey+userID, code, duration); err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // Set latest code -> content if err := e.data.Cache.SetString(ctx, constant.UserEmailCodeCacheKey+code, content, duration); err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // VerifyCode verify the code if out of date func (e *emailRepo) VerifyCode(ctx context.Context, code string) (content string, err error) { // Get latest code -> content codeCacheKey := constant.UserEmailCodeCacheKey + code content, exist, err := e.data.Cache.GetString(ctx, codeCacheKey) if err != nil { return "", err } if !exist { return "", nil } // Delete the code after verification _ = e.data.Cache.Del(ctx, codeCacheKey) // If some email content does not need to verify the latest code is the same as the code, skip it. // For example, some unsubscribe email content does not need to verify the latest code. // This link always works before the code is out of date. if skipValidationLatestCode := gjson.Get(content, "skip_validation_latest_code").Bool(); skipValidationLatestCode { return content, nil } userID := gjson.Get(content, "user_id").String() // Get userID -> latest code latestCode, exist, err := e.data.Cache.GetString(ctx, constant.UserLatestEmailCodeCacheKey+userID) if err != nil { return "", err } if !exist { return "", nil } // Check if the latest code is the same as the code, if not, means the code is out of date if latestCode != code { return "", nil } return content, nil } ================================================ FILE: internal/repo/file_record/file_record_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package file_record import ( "context" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/service/file_record" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/errors" ) // fileRecordRepo fileRecord repository type fileRecordRepo struct { data *data.Data } // NewFileRecordRepo new repository func NewFileRecordRepo(data *data.Data) file_record.FileRecordRepo { return &fileRecordRepo{ data: data, } } // AddFileRecord add file record func (fr *fileRecordRepo) AddFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) { _, err = fr.data.DB.Context(ctx).Insert(fileRecord) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetFileRecordPage get fileRecord page func (fr *fileRecordRepo) GetFileRecordPage(ctx context.Context, page, pageSize int, cond *entity.FileRecord) ( fileRecordList []*entity.FileRecord, total int64, err error) { fileRecordList = make([]*entity.FileRecord, 0) session := fr.data.DB.Context(ctx) total, err = pager.Help(page, pageSize, &fileRecordList, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // DeleteFileRecord delete file record func (fr *fileRecordRepo) DeleteFileRecord(ctx context.Context, id int) (err error) { _, err = fr.data.DB.Context(ctx).ID(id).Cols("status").Update(&entity.FileRecord{Status: entity.FileRecordStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateFileRecord update file record func (fr *fileRecordRepo) UpdateFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) { _, err = fr.data.DB.Context(ctx).ID(fileRecord.ID).Update(fileRecord) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetFileRecordByURL gets a file record by its url func (fr *fileRecordRepo) GetFileRecordByURL(ctx context.Context, fileURL string) (record *entity.FileRecord, err error) { record = &entity.FileRecord{} session := fr.data.DB.Context(ctx) exists, err := session.Where("file_url = ? AND status = ?", fileURL, entity.FileRecordStatusAvailable).Get(record) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } if !exists { return } return record, nil } ================================================ FILE: internal/repo/limit/limit.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package limit import ( "context" "fmt" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/segmentfault/pacman/errors" ) // LimitRepo auth repository type LimitRepo struct { data *data.Data } // NewRateLimitRepo new repository func NewRateLimitRepo(data *data.Data) *LimitRepo { return &LimitRepo{ data: data, } } // CheckAndRecord check func (lr *LimitRepo) CheckAndRecord(ctx context.Context, key string) (limit bool, err error) { _, exist, err := lr.data.Cache.GetString(ctx, constant.RateLimitCacheKeyPrefix+key) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if exist { return true, nil } err = lr.data.Cache.SetString(ctx, constant.RateLimitCacheKeyPrefix+key, fmt.Sprintf("%d", time.Now().Unix()), constant.RateLimitCacheTime) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return false, nil } // ClearRecord clear func (lr *LimitRepo) ClearRecord(ctx context.Context, key string) error { return lr.data.Cache.Del(ctx, constant.RateLimitCacheKeyPrefix+key) } ================================================ FILE: internal/repo/meta/meta_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package meta import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" metacommon "github.com/apache/answer/internal/service/meta_common" "github.com/segmentfault/pacman/errors" "xorm.io/builder" "xorm.io/xorm" ) // metaRepo meta repository type metaRepo struct { data *data.Data } // NewMetaRepo new repository func NewMetaRepo(data *data.Data) metacommon.MetaRepo { return &metaRepo{ data: data, } } // AddMeta add meta func (mr *metaRepo) AddMeta(ctx context.Context, meta *entity.Meta) (err error) { _, err = mr.data.DB.Context(ctx).Insert(meta) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // RemoveMeta delete meta func (mr *metaRepo) RemoveMeta(ctx context.Context, id int) (err error) { _, err = mr.data.DB.Context(ctx).ID(id).Delete(&entity.Meta{}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateMeta update meta func (mr *metaRepo) UpdateMeta(ctx context.Context, meta *entity.Meta) (err error) { _, err = mr.data.DB.Context(ctx).ID(meta.ID).Update(meta) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // AddOrUpdateMetaByObjectIdAndKey if exist record with same objectID and key, update it. Or create a new one func (mr *metaRepo) AddOrUpdateMetaByObjectIdAndKey(ctx context.Context, objectId, key string, f func(*entity.Meta, bool) (*entity.Meta, error)) error { _, err := mr.data.DB.Transaction(func(session *xorm.Session) (any, error) { session = session.Context(ctx) // 1. acquire meta entity with target object id and key metaEntity := &entity.Meta{} exist, err := session.Where(builder.Eq{"object_id": objectId}.And(builder.Eq{"`key`": key})).ForUpdate().Get(metaEntity) if err != nil { return nil, err } meta, err := f(metaEntity, exist) if err != nil { return nil, err } // return entity.Meta if exist { _, err = session.ID(metaEntity.ID).Update(meta) } else { _, err = session.Insert(meta) } return nil, err }) return err } // GetMetaByObjectIdAndKey get meta one func (mr *metaRepo) GetMetaByObjectIdAndKey(ctx context.Context, objectID, key string) ( meta *entity.Meta, exist bool, err error) { meta = &entity.Meta{} exist, err = mr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}.And(builder.Eq{"`key`": key})).Desc("created_at").Get(meta) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetMetaList get meta list all func (mr *metaRepo) GetMetaList(ctx context.Context, meta *entity.Meta) (metaList []*entity.Meta, err error) { metaList = make([]*entity.Meta, 0) err = mr.data.DB.Context(ctx).Find(&metaList, meta) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/notification/notification_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package notification import ( "context" "time" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" notficationcommon "github.com/apache/answer/internal/service/notification_common" "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" ) // notificationRepo notification repository type notificationRepo struct { data *data.Data } // NewNotificationRepo new repository func NewNotificationRepo(data *data.Data) notficationcommon.NotificationRepo { return ¬ificationRepo{ data: data, } } // AddNotification add notification func (nr *notificationRepo) AddNotification(ctx context.Context, notification *entity.Notification) (err error) { notification.ObjectID = uid.DeShortID(notification.ObjectID) _, err = nr.data.DB.Context(ctx).Insert(notification) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (nr *notificationRepo) UpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error) { now := time.Now() notification.UpdatedAt = now notification.ObjectID = uid.DeShortID(notification.ObjectID) _, err = nr.data.DB.Context(ctx).Where("id =?", notification.ID).Cols("content", "updated_at").Update(notification) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (nr *notificationRepo) ClearUnRead(ctx context.Context, userID string, notificationType int) (err error) { info := &entity.Notification{} info.IsRead = schema.NotificationRead _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("type = ?", notificationType).Cols("is_read").Update(info) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (nr *notificationRepo) ClearIDUnRead(ctx context.Context, userID string, id string) (err error) { info := &entity.Notification{} info.IsRead = schema.NotificationRead _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("id = ?", id).Cols("is_read").Update(info) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (nr *notificationRepo) GetById(ctx context.Context, id string) (*entity.Notification, bool, error) { info := &entity.Notification{} exist, err := nr.data.DB.Context(ctx).Where("id = ? ", id).Get(info) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return info, false, err } return info, exist, nil } func (nr *notificationRepo) GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error) { info := &entity.Notification{} exist, err := nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("object_id = ?", objectID).And("type = ?", notificationType).Get(info) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return info, false, err } return info, exist, nil } func (nr *notificationRepo) GetNotificationPage(ctx context.Context, searchCond *schema.NotificationSearch) ( notificationList []*entity.Notification, total int64, err error) { notificationList = make([]*entity.Notification, 0) if searchCond.UserID == "" { return notificationList, 0, nil } session := nr.data.DB.Context(ctx) session = session.Desc("updated_at") cond := &entity.Notification{ UserID: searchCond.UserID, Type: searchCond.Type, } if searchCond.InboxType > 0 { cond.MsgType = searchCond.InboxType } total, err = pager.Help(searchCond.Page, searchCond.PageSize, ¬ificationList, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (nr *notificationRepo) CountNotificationByUser(ctx context.Context, cond *entity.Notification) (int64, error) { count, err := nr.data.DB.Context(ctx).Count(cond) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, err } func (nr *notificationRepo) DeleteNotification(ctx context.Context, userID string) (err error) { _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.Notification{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (nr *notificationRepo) DeleteUserNotificationConfig(ctx context.Context, userID string) (err error) { _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.UserNotificationConfig{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/plugin_config/plugin_config_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin_config import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/plugin_common" "github.com/segmentfault/pacman/errors" ) type pluginConfigRepo struct { data *data.Data } // NewPluginConfigRepo new repository func NewPluginConfigRepo(data *data.Data) plugin_common.PluginConfigRepo { return &pluginConfigRepo{ data: data, } } func (ur *pluginConfigRepo) SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error) { old := &entity.PluginConfig{PluginSlugName: pluginSlugName} exist, err := ur.data.DB.Context(ctx).Get(old) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if exist { old.Value = configValue _, err = ur.data.DB.Context(ctx).ID(old.ID).Update(old) } else { _, err = ur.data.DB.Context(ctx).Insert(&entity.PluginConfig{PluginSlugName: pluginSlugName, Value: configValue}) } if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } func (ur *pluginConfigRepo) GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error) { pluginConfigs = make([]*entity.PluginConfig, 0) err = ur.data.DB.Context(ctx).Find(&pluginConfigs) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return pluginConfigs, err } ================================================ FILE: internal/repo/plugin_config/plugin_user_config_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin_config import ( "context" "github.com/apache/answer/internal/base/pager" "xorm.io/xorm" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/plugin_common" "github.com/segmentfault/pacman/errors" ) type pluginUserConfigRepo struct { data *data.Data } // NewPluginUserConfigRepo new repository func NewPluginUserConfigRepo(data *data.Data) plugin_common.PluginUserConfigRepo { return &pluginUserConfigRepo{ data: data, } } func (ur *pluginUserConfigRepo) SaveUserPluginConfig(ctx context.Context, userID string, pluginSlugName, configValue string) (err error) { _, err = ur.data.DB.Transaction(func(session *xorm.Session) (any, error) { session = session.Context(ctx) old := &entity.PluginUserConfig{ UserID: userID, PluginSlugName: pluginSlugName, } exist, err := session.Get(old) if err != nil { return nil, err } if exist { old.Value = configValue _, err = session.ID(old.ID).Update(old) } else { _, err = session.Insert(&entity.PluginUserConfig{ UserID: userID, PluginSlugName: pluginSlugName, Value: configValue, }) } if err != nil { return nil, err } return nil, nil }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } func (ur *pluginUserConfigRepo) GetPluginUserConfig(ctx context.Context, userID, pluginSlugName string) ( pluginUserConfig *entity.PluginUserConfig, exist bool, err error) { pluginUserConfig = &entity.PluginUserConfig{ UserID: userID, PluginSlugName: pluginSlugName, } exist, err = ur.data.DB.Context(ctx).Get(pluginUserConfig) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return pluginUserConfig, exist, err } func (ur *pluginUserConfigRepo) GetPluginUserConfigPage(ctx context.Context, page, pageSize int) ( pluginUserConfigs []*entity.PluginUserConfig, total int64, err error) { pluginUserConfigs = make([]*entity.PluginUserConfig, 0) total, err = pager.Help(page, pageSize, &pluginUserConfigs, &entity.PluginUserConfig{}, ur.data.DB.Context(ctx)) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (ur *pluginUserConfigRepo) DeleteUserPluginConfig(ctx context.Context, userID string) (err error) { _, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.PluginUserConfig{}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/provider.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo import ( "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/repo/activity" "github.com/apache/answer/internal/repo/activity_common" "github.com/apache/answer/internal/repo/ai_conversation" "github.com/apache/answer/internal/repo/answer" "github.com/apache/answer/internal/repo/api_key" "github.com/apache/answer/internal/repo/auth" "github.com/apache/answer/internal/repo/badge" "github.com/apache/answer/internal/repo/badge_award" "github.com/apache/answer/internal/repo/badge_group" "github.com/apache/answer/internal/repo/captcha" "github.com/apache/answer/internal/repo/collection" "github.com/apache/answer/internal/repo/comment" "github.com/apache/answer/internal/repo/config" "github.com/apache/answer/internal/repo/export" "github.com/apache/answer/internal/repo/file_record" "github.com/apache/answer/internal/repo/limit" "github.com/apache/answer/internal/repo/meta" "github.com/apache/answer/internal/repo/notification" "github.com/apache/answer/internal/repo/plugin_config" "github.com/apache/answer/internal/repo/question" "github.com/apache/answer/internal/repo/rank" "github.com/apache/answer/internal/repo/reason" "github.com/apache/answer/internal/repo/report" "github.com/apache/answer/internal/repo/review" "github.com/apache/answer/internal/repo/revision" "github.com/apache/answer/internal/repo/role" "github.com/apache/answer/internal/repo/search_common" "github.com/apache/answer/internal/repo/site_info" "github.com/apache/answer/internal/repo/tag" "github.com/apache/answer/internal/repo/tag_common" "github.com/apache/answer/internal/repo/unique" "github.com/apache/answer/internal/repo/user" "github.com/apache/answer/internal/repo/user_external_login" "github.com/apache/answer/internal/repo/user_notification_config" "github.com/google/wire" ) // ProviderSetRepo is data providers. var ProviderSetRepo = wire.NewSet( data.NewData, data.NewDB, data.NewCache, comment.NewCommentRepo, comment.NewCommentCommonRepo, captcha.NewCaptchaRepo, unique.NewUniqueIDRepo, report.NewReportRepo, activity_common.NewFollowRepo, activity_common.NewVoteRepo, config.NewConfigRepo, user.NewUserRepo, user.NewUserAdminRepo, rank.NewUserRankRepo, question.NewQuestionRepo, answer.NewAnswerRepo, activity_common.NewActivityRepo, activity.NewVoteRepo, activity.NewFollowRepo, activity.NewAnswerActivityRepo, activity.NewUserActiveActivityRepo, activity.NewActivityRepo, activity.NewReviewActivityRepo, tag.NewTagRepo, tag_common.NewTagCommonRepo, tag.NewTagRelRepo, collection.NewCollectionRepo, collection.NewCollectionGroupRepo, auth.NewAuthRepo, revision.NewRevisionRepo, search_common.NewSearchRepo, meta.NewMetaRepo, export.NewEmailRepo, reason.NewReasonRepo, site_info.NewSiteInfo, notification.NewNotificationRepo, role.NewRoleRepo, role.NewUserRoleRelRepo, role.NewRolePowerRelRepo, role.NewPowerRepo, user_external_login.NewUserExternalLoginRepo, plugin_config.NewPluginConfigRepo, user_notification_config.NewUserNotificationConfigRepo, limit.NewRateLimitRepo, plugin_config.NewPluginUserConfigRepo, review.NewReviewRepo, badge.NewBadgeRepo, badge.NewEventRuleRepo, badge_group.NewBadgeGroupRepo, badge_award.NewBadgeAwardRepo, file_record.NewFileRecordRepo, api_key.NewAPIKeyRepo, ai_conversation.NewAIConversationRepo, ) ================================================ FILE: internal/repo/question/question_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package question import ( "context" "encoding/json" "fmt" "strings" "time" "unicode" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/unique" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "xorm.io/builder" "xorm.io/xorm" ) // questionRepo question repository type questionRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } // NewQuestionRepo new repository func NewQuestionRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, ) questioncommon.QuestionRepo { return &questionRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } // AddQuestion add question func (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Question) (err error) { question.ID, err = qr.uniqueIDRepo.GenUniqueIDStr(ctx, question.TableName()) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _, err = qr.data.DB.Context(ctx).Insert(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { question.ID = uid.EnShortID(question.ID) } return } // RemoveQuestion delete question func (qr *questionRepo) RemoveQuestion(ctx context.Context, id string) (err error) { id = uid.DeShortID(id) _, err = qr.data.DB.Context(ctx).Where("id =?", id).Delete(&entity.Question{}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateQuestion update question func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Question, cols []string) (err error) { question.ID = uid.DeShortID(question.ID) _, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols(cols...).Update(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { question.ID = uid.EnShortID(question.ID) } _ = qr.UpdateSearch(ctx, question.ID) return } func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionID string) (err error) { questionID = uid.DeShortID(questionID) question := &entity.Question{} _, err = qr.data.DB.Context(ctx).Where("id =?", questionID).Incr("view_count", 1).Update(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = qr.UpdateSearch(ctx, question.ID) return nil } func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error) { questionID = uid.DeShortID(questionID) question := &entity.Question{} question.AnswerCount = num _, err = qr.data.DB.Context(ctx).Where("id =?", questionID).Cols("answer_count").Update(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = qr.UpdateSearch(ctx, question.ID) return nil } func (qr *questionRepo) UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) { questionID = uid.DeShortID(questionID) _, err = qr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) count, err = session.Count(&entity.Collection{ObjectID: questionID}) if err != nil { return nil, err } question := &entity.Question{CollectionCount: int(count)} _, err = session.ID(questionID).MustCols("collection_count").Update(question) if err != nil { return nil, err } return }) if err != nil { return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } func (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) { questionID = uid.DeShortID(questionID) _, err = qr.data.DB.Context(ctx).ID(questionID).Cols("status").Update(&entity.Question{Status: status}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = qr.UpdateSearch(ctx, questionID) return nil } func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) { question.ID = uid.DeShortID(question.ID) _, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("status").Update(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = qr.UpdateSearch(ctx, question.ID) return nil } func (qr *questionRepo) DeletePermanentlyQuestions(ctx context.Context) (err error) { // get all deleted question ids ids := make([]string, 0) err = qr.data.DB.Context(ctx).Select("id").Table(new(entity.Question).TableName()). Where("status = ?", entity.QuestionStatusDeleted).Find(&ids) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if len(ids) == 0 { return nil } // delete all revisions permanently _, err = qr.data.DB.Context(ctx).In("object_id", ids).Delete(&entity.Revision{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _, err = qr.data.DB.Context(ctx).Where("status = ?", entity.QuestionStatusDeleted).Delete(&entity.Question{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } func (qr *questionRepo) RecoverQuestion(ctx context.Context, questionID string) (err error) { questionID = uid.DeShortID(questionID) _, err = qr.data.DB.Context(ctx).ID(questionID).Cols("status").Update(&entity.Question{Status: entity.QuestionStatusAvailable}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = qr.UpdateSearch(ctx, questionID) return nil } func (qr *questionRepo) UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error) { question.ID = uid.DeShortID(question.ID) _, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("pin", "show").Update(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) { question.ID = uid.DeShortID(question.ID) _, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("accepted_answer_id").Update(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = qr.UpdateSearch(ctx, question.ID) return nil } func (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) { question.ID = uid.DeShortID(question.ID) _, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("last_answer_id").Update(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = qr.UpdateSearch(ctx, question.ID) return nil } // GetQuestion get question one func (qr *questionRepo) GetQuestion(ctx context.Context, id string) ( question *entity.Question, exist bool, err error, ) { id = uid.DeShortID(id) question = &entity.Question{} question.ID = id exist, err = qr.data.DB.Context(ctx).Where("id = ?", id).Get(question) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { question.ID = uid.EnShortID(question.ID) } return } // GetQuestionsByTitle get question list by title func (qr *questionRepo) GetQuestionsByTitle(ctx context.Context, title string, pageSize int) ( questionList []*entity.Question, err error) { questionList = make([]*entity.Question, 0) session := qr.data.DB.Context(ctx) session.Where("status != ?", entity.QuestionStatusDeleted) session.Where("title like ?", "%"+title+"%") session.Limit(pageSize) err = session.Find(&questionList) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range questionList { item.ID = uid.EnShortID(item.ID) } } return } func (qr *questionRepo) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) { for key, itemID := range id { id[key] = uid.DeShortID(itemID) } questionList = make([]*entity.Question, 0) err = qr.data.DB.Context(ctx).Table("question").In("id", id).Find(&questionList) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range questionList { item.ID = uid.EnShortID(item.ID) } } return } // GetQuestionList get question list all func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Question) (questionList []*entity.Question, err error) { question.ID = uid.DeShortID(question.ID) questionList = make([]*entity.Question, 0) err = qr.data.DB.Context(ctx).Find(&questionList, question) if err != nil { return questionList, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } for _, item := range questionList { item.ID = uid.DeShortID(item.ID) } return } func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) { session := qr.data.DB.Context(ctx) session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}) count, err = session.Count(&entity.Question{Show: entity.QuestionShow}) if err != nil { return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } func (qr *questionRepo) GetUnansweredQuestionCount(ctx context.Context) (count int64, err error) { session := qr.data.DB.Context(ctx) session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}). And(builder.Eq{"answer_count": 0}) count, err = session.Count(&entity.Question{Show: entity.QuestionShow}) if err != nil { return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } func (qr *questionRepo) GetResolvedQuestionCount(ctx context.Context) (count int64, err error) { session := qr.data.DB.Context(ctx) session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}). And(builder.Neq{"answer_count": 0}). And(builder.Neq{"accepted_answer_id": 0}) count, err = session.Count(&entity.Question{Show: entity.QuestionShow}) if err != nil { return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } func (qr *questionRepo) GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error) { session := qr.data.DB.Context(ctx) session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}) count, err = session.Count(&entity.Question{UserID: userID, Show: show}) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (qr *questionRepo) SitemapQuestions(ctx context.Context, page, pageSize int) ( questionIDList []*schema.SiteMapQuestionInfo, err error) { page-- questionIDList = make([]*schema.SiteMapQuestionInfo, 0) // try to get sitemap data from cache cacheKey := fmt.Sprintf(constant.SiteMapQuestionCacheKeyPrefix, page) cacheData, exist, err := qr.data.Cache.GetString(ctx, cacheKey) if err == nil && exist { _ = json.Unmarshal([]byte(cacheData), &questionIDList) return questionIDList, nil } // get sitemap data from db rows := make([]*entity.Question, 0) session := qr.data.DB.Context(ctx) session.Select("id,title,created_at,post_update_time") session.Where("`show` = ?", entity.QuestionShow) session.Where("status = ? OR status = ?", entity.QuestionStatusAvailable, entity.QuestionStatusClosed) session.Limit(pageSize, page*pageSize) session.Asc("created_at") err = session.Find(&rows) if err != nil { return questionIDList, err } // warp data for _, question := range rows { item := &schema.SiteMapQuestionInfo{ID: question.ID} if handler.GetEnableShortID(ctx) { item.ID = uid.EnShortID(question.ID) } item.Title = htmltext.UrlTitle(question.Title) if question.PostUpdateTime.IsZero() { item.UpdateTime = question.CreatedAt.Format(time.RFC3339) } else { item.UpdateTime = question.PostUpdateTime.Format(time.RFC3339) } questionIDList = append(questionIDList, item) } // set sitemap data to cache cacheDataByte, _ := json.Marshal(questionIDList) if err := qr.data.Cache.SetString(ctx, cacheKey, string(cacheDataByte), constant.SiteMapQuestionCacheTime); err != nil { log.Error(err) } return questionIDList, nil } // GetQuestionPage query question page func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) ( questionList []*entity.Question, total int64, err error) { questionList = make([]*entity.Question, 0) session := qr.data.DB.Context(ctx) status := []int{entity.QuestionStatusAvailable} if orderCond != "unanswered" { status = append(status, entity.QuestionStatusClosed) } if showPending { status = append(status, entity.QuestionStatusPending) } session.Select("question.*") session.In("question.status", status) if len(tagIDs) > 0 { session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id") session.In("tag_rel.tag_id", tagIDs) session.And("tag_rel.status = ?", entity.TagRelStatusAvailable) } if len(userID) > 0 { session.And("question.user_id = ?", userID) if !showHidden { session.And("question.show = ?", entity.QuestionShow) } } else { session.And("question.show = ?", entity.QuestionShow) } if inDays > 0 { session.And("question.created_at > ?", time.Now().AddDate(0, 0, -inDays)) } switch orderCond { case "newest": session.OrderBy("question.pin desc,question.created_at DESC") case "active": if inDays == 0 { session.And("question.created_at > ?", time.Now().AddDate(0, 0, -180)) } session.And("question.post_update_time > ?", time.Now().AddDate(0, 0, -90)) session.OrderBy("question.pin desc,question.post_update_time DESC, question.updated_at DESC") case "hot": session.OrderBy("question.pin desc,question.hot_score DESC") case "score": session.OrderBy("question.pin desc,question.vote_count DESC, question.view_count DESC") case "unanswered": session.Where("question.answer_count = 0") session.OrderBy("question.pin desc,question.created_at DESC") case "frequent": session.OrderBy("question.pin DESC, question.linked_count DESC, question.updated_at DESC") } session.GroupBy("question.id") total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range questionList { item.ID = uid.EnShortID(item.ID) } } return questionList, total, err } // GetRecommendQuestionPageByTags get recommend question page by tags func (qr *questionRepo) GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) ( questionList []*entity.Question, total int64, err error) { questionList = make([]*entity.Question, 0) orderBySQL := "question.pin DESC, question.created_at DESC" // Please Make sure every question has at least one tag selectSQL := entity.Question{}.TableName() + ".*" if len(followedQuestionIDs) > 0 { idStr := "'" + strings.Join(followedQuestionIDs, "','") + "'" selectSQL += fmt.Sprintf(", CASE WHEN question.id IN (%s) THEN 0 ELSE 1 END AS order_priority", idStr) orderBySQL = "order_priority, " + orderBySQL } session := qr.data.DB.Context(ctx).Select(selectSQL) if len(tagIDs) > 0 { session.Where("question.user_id != ?", userID). And("question.id NOT IN (SELECT question_id FROM answer WHERE user_id = ?)", userID). Join("INNER", "tag_rel", "question.id = tag_rel.object_id"). And("tag_rel.status = ?", entity.TagRelStatusAvailable). Join("INNER", "tag", "tag.id = tag_rel.tag_id"). In("tag.id", tagIDs) } else if len(followedQuestionIDs) == 0 { return questionList, 0, nil } if len(followedQuestionIDs) > 0 { if len(tagIDs) > 0 { // if tags provided, show followed questions and tag questions session.Or(builder.In("question.id", followedQuestionIDs)) } else { // if no tags, only show followed questions session.Where(builder.In("question.id", followedQuestionIDs)) } } session. And("question.show = ? and question.status = ?", entity.QuestionShow, entity.QuestionStatusAvailable). Distinct("question.id"). OrderBy(orderBySQL) total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range questionList { item.ID = uid.EnShortID(item.ID) } } return questionList, total, err } func (qr *questionRepo) AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) { var ( count int64 err error session = qr.data.DB.Context(ctx).Table("question") ) session.Where(builder.Eq{ "status": search.Status, }) rows := make([]*entity.Question, 0) if search.Page > 0 { search.Page-- } else { search.Page = 0 } if search.PageSize == 0 { search.PageSize = constant.DefaultPageSize } // search by question title like or question id if len(search.Query) > 0 { // check id search var ( idSearch = false id = "" ) if strings.Contains(search.Query, "question:") { idSearch = true id = strings.TrimSpace(strings.TrimPrefix(search.Query, "question:")) id = uid.DeShortID(id) for _, r := range id { if !unicode.IsDigit(r) { idSearch = false break } } } if idSearch { session.And(builder.Eq{ "id": id, }) } else { session.And(builder.Like{ "title", search.Query, }) } } offset := search.Page * search.PageSize session.OrderBy("created_at desc"). Limit(search.PageSize, offset) count, err = session.FindAndCount(&rows) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return rows, count, err } if handler.GetEnableShortID(ctx) { for _, item := range rows { item.ID = uid.EnShortID(item.ID) } } return rows, count, nil } // UpdateSearch update search, if search plugin not enable, do nothing func (qr *questionRepo) UpdateSearch(ctx context.Context, questionID string) (err error) { // check search plugin var s plugin.Search _ = plugin.CallSearch(func(search plugin.Search) error { s = search return nil }) if s == nil { return } questionID = uid.DeShortID(questionID) question, exist, err := qr.GetQuestion(ctx, questionID) if !exist { return } if err != nil { return err } // get tags var ( tagListList = make([]*entity.TagRel, 0) tags = make([]string, 0) ) session := qr.data.DB.Context(ctx).Where("object_id = ?", questionID) session.Where("status = ?", entity.TagRelStatusAvailable) err = session.Find(&tagListList) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } for _, tag := range tagListList { tags = append(tags, tag.TagID) } content := &plugin.SearchContent{ ObjectID: questionID, Title: question.Title, Type: constant.QuestionObjectType, Content: question.OriginalText, Answers: int64(question.AnswerCount), Status: plugin.SearchContentStatus(question.Status), Tags: tags, QuestionID: questionID, UserID: question.UserID, Views: int64(question.ViewCount), Created: question.CreatedAt.Unix(), Active: question.UpdatedAt.Unix(), Score: int64(question.VoteCount), HasAccepted: question.AcceptedAnswerID != "" && question.AcceptedAnswerID != "0", } err = s.UpdateContent(ctx, content) return } func (qr *questionRepo) RemoveAllUserQuestion(ctx context.Context, userID string) (err error) { // get all question id that need to be deleted questionIDs := make([]string, 0) session := qr.data.DB.Context(ctx).Where("user_id = ?", userID) session.Where("status != ?", entity.QuestionStatusDeleted) err = session.Select("id").Table("question").Find(&questionIDs) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if len(questionIDs) == 0 { return nil } log.Infof("find %d questions need to be deleted for user %s", len(questionIDs), userID) // delete all question session = qr.data.DB.Context(ctx).Where("user_id = ?", userID) session.Where("status != ?", entity.QuestionStatusDeleted) _, err = session.Cols("status", "updated_at").Update(&entity.Question{ UpdatedAt: time.Now(), Status: entity.QuestionStatusDeleted, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // update search content for _, id := range questionIDs { _ = qr.UpdateSearch(ctx, id) } return nil } // LinkQuestion batch insert question link func (qr *questionRepo) LinkQuestion(ctx context.Context, link ...*entity.QuestionLink) (err error) { // Batch retrieve all links var links []*entity.QuestionLink for _, l := range link { l.FromQuestionID = uid.DeShortID(l.FromQuestionID) l.ToQuestionID = uid.DeShortID(l.ToQuestionID) l.FromAnswerID = uid.DeShortID(l.FromAnswerID) l.ToAnswerID = uid.DeShortID(l.ToAnswerID) links = append(links, l) } // Retrieve existing records from the database var existLinks []*entity.QuestionLink session := qr.data.DB.Context(ctx) for _, link := range links { session = session.Or(builder.Eq{ "from_question_id": link.FromQuestionID, "to_question_id": link.ToQuestionID, "from_answer_id": link.FromAnswerID, "to_answer_id": link.ToAnswerID, }) } err = session.Find(&existLinks) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // Optimize separation of records that need to be updated or inserted using a map existMap := make(map[string]*entity.QuestionLink) for _, el := range existLinks { key := fmt.Sprintf("%s:%s:%s:%s", el.FromQuestionID, el.ToQuestionID, el.FromAnswerID, el.ToAnswerID) existMap[key] = el } var updateLinks []*entity.QuestionLink var insertLinks []*entity.QuestionLink for _, link := range links { key := fmt.Sprintf("%s:%s:%s:%s", link.FromQuestionID, link.ToQuestionID, link.FromAnswerID, link.ToAnswerID) if el, exist := existMap[key]; exist { if el.Status == entity.QuestionLinkStatusDeleted { el.Status = entity.QuestionLinkStatusAvailable el.UpdatedAt = time.Now() updateLinks = append(updateLinks, el) } } else { link.Status = entity.QuestionLinkStatusAvailable link.CreatedAt = time.Now() link.UpdatedAt = time.Now() insertLinks = append(insertLinks, link) } } // Batch update if len(updateLinks) > 0 { for _, link := range updateLinks { _, err = qr.data.DB.Context(ctx).ID(link.ID).Cols("status").Update(&entity.QuestionLink{Status: entity.QuestionLinkStatusAvailable}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } } } // Batch insert if len(insertLinks) > 0 { _, err = qr.data.DB.Context(ctx).Insert(insertLinks) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } } return } // UpdateQuestionLinkCount update question link count func (qr *questionRepo) UpdateQuestionLinkCount(ctx context.Context, questionID string) (err error) { // count the number of links count, err := qr.data.DB.Context(ctx). Where("to_question_id = ?", questionID). Where("status = ?", entity.QuestionLinkStatusAvailable). Count(&entity.QuestionLink{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // update the number of links _, err = qr.data.DB.Context(ctx).ID(questionID). Cols("linked_count").Update(&entity.Question{LinkedCount: int(count)}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetLinkedQuestionIDs get linked question ids func (qr *questionRepo) GetLinkedQuestionIDs(ctx context.Context, questionID string, status int) ( questionIDs []string, err error) { questionIDs = make([]string, 0) err = qr.data.DB.Context(ctx). Select("to_question_id"). Table(new(entity.QuestionLink).TableName()). Where("from_question_id = ?", questionID). Where("status = ?", status). Find(&questionIDs) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return questionIDs, nil } // RecoverQuestionLink batch recover question link func (qr *questionRepo) RecoverQuestionLink(ctx context.Context, links ...*entity.QuestionLink) (err error) { return qr.UpdateQuestionLinkStatus(ctx, entity.QuestionLinkStatusAvailable, links...) } // RemoveQuestionLink batch remove question link func (qr *questionRepo) RemoveQuestionLink(ctx context.Context, links ...*entity.QuestionLink) (err error) { return qr.UpdateQuestionLinkStatus(ctx, entity.QuestionLinkStatusDeleted, links...) } // UpdateQuestionLinkStatus update question link status func (qr *questionRepo) UpdateQuestionLinkStatus(ctx context.Context, status int, links ...*entity.QuestionLink) (err error) { if len(links) == 0 { return nil } session := qr.data.DB.Context(ctx).Cols("status") for _, link := range links { eq := builder.Eq{} if link.FromQuestionID != "" { eq["from_question_id"] = uid.DeShortID(link.FromQuestionID) } if link.FromAnswerID != "" { eq["from_answer_id"] = uid.DeShortID(link.FromAnswerID) } if link.ToQuestionID != "" { eq["to_question_id"] = uid.DeShortID(link.ToQuestionID) } if link.ToAnswerID != "" { eq["to_answer_id"] = uid.DeShortID(link.ToAnswerID) } session = session.Or(eq) } _, err = session.Update(&entity.QuestionLink{Status: status}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetQuestionLink get linked question to questionID func (qr *questionRepo) GetQuestionLink(ctx context.Context, page, pageSize int, questionID string, orderCond string, inDays int) (questionList []*entity.Question, total int64, err error) { questionList = make([]*entity.Question, 0) questionID = uid.DeShortID(questionID) questionStatus := []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed, entity.QuestionStatusPending} if questionID == "0" { return nil, 0, errors.InternalServer(reason.DatabaseError).WithError( fmt.Errorf("questionID is empty"), ).WithStack() } session := qr.data.DB.Context(ctx). Table("question_link"). Join("INNER", "question", "question_link.from_question_id = question.id"). Where("question_link.to_question_id = ? AND question.show = ?", questionID, entity.QuestionShow). Distinct("question.id"). Where("question_link.status = ?", entity.QuestionLinkStatusAvailable). Select("question.*"). In("question.status", questionStatus) switch orderCond { case "newest": session.OrderBy("question.pin desc,question.created_at DESC") case "active": if inDays == 0 { session.And("question.created_at > ?", time.Now().AddDate(0, 0, -180)) } session.And("question.post_update_time > ?", time.Now().AddDate(0, 0, -90)) session.OrderBy("question.pin desc,question.post_update_time DESC, question.updated_at DESC") case "hot": session.OrderBy("question.pin desc,question.hot_score DESC") case "score": session.OrderBy("question.pin desc,question.vote_count DESC, question.view_count DESC") case "unanswered": session.Where("question.answer_count = 0") session.OrderBy("question.pin desc,question.created_at DESC") case "frequent": session.OrderBy("question.pin DESC, question.linked_count DESC, question.updated_at DESC") } if page > 0 && pageSize > 0 { session.Limit(pageSize, (page-1)*pageSize) } total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range questionList { item.ID = uid.EnShortID(item.ID) } } return } ================================================ FILE: internal/repo/rank/user_rank_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package rank import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/plugin" "github.com/jinzhu/now" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "xorm.io/builder" "xorm.io/xorm" ) // UserRankRepo user rank repository type UserRankRepo struct { data *data.Data configService *config.ConfigService } // NewUserRankRepo new repository func NewUserRankRepo(data *data.Data, configService *config.ConfigService) rank.UserRankRepo { return &UserRankRepo{ data: data, configService: configService, } } func (ur *UserRankRepo) GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) { maxDailyRank, err = ur.configService.GetIntValue(ctx, "daily_rank_limit") if err != nil { return 0, err } return maxDailyRank, nil } func (ur *UserRankRepo) CheckReachLimit(ctx context.Context, session *xorm.Session, userID string, maxDailyRank int) ( reach bool, err error) { session.Where(builder.Eq{"user_id": userID}) session.Where(builder.Eq{"cancelled": 0}) session.Where(builder.Between{ Col: "updated_at", LessVal: now.BeginningOfDay(), MoreVal: now.EndOfDay(), }) earned, err := session.SumInt(&entity.Activity{}, "`rank`") if err != nil { return false, err } if int(earned) < maxDailyRank { return false, nil } log.Infof("user %s today has rank %d is reach stand %d", userID, earned, maxDailyRank) return true, nil } // ChangeUserRank change user rank func (ur *UserRankRepo) ChangeUserRank( ctx context.Context, session *xorm.Session, userID string, userCurrentScore, deltaRank int) (err error) { // IMPORTANT: If user center enabled the rank agent, then we should not change user rank. if plugin.RankAgentEnabled() || deltaRank == 0 { return nil } // If user rank is lower than 1 after this action, then user rank will be set to 1 only. if deltaRank < 0 && userCurrentScore+deltaRank < 1 { deltaRank = 1 - userCurrentScore } _, err = session.ID(userID).Incr("`rank`", deltaRank).Update(&entity.User{}) if err != nil { return err } return nil } // TriggerUserRank trigger user rank change // session is need provider, it means this action must be success or failure // if outer action is failed then this action is need rollback func (ur *UserRankRepo) TriggerUserRank(ctx context.Context, session *xorm.Session, userID string, deltaRank int, activityType int, ) (isReachStandard bool, err error) { // IMPORTANT: If user center enabled the rank agent, then we should not change user rank. if plugin.RankAgentEnabled() || deltaRank == 0 { return false, nil } if deltaRank < 0 { // if user rank is lower than 1 after this action, then user rank will be set to 1 only. var isReachMin bool isReachMin, err = ur.checkUserMinRank(ctx, session, userID, deltaRank) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if isReachMin { _, err = session.Where(builder.Eq{"id": userID}).Update(&entity.User{Rank: 1}) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return true, nil } } else { isReachStandard, err = ur.checkUserTodayRank(ctx, session, userID, activityType) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if isReachStandard { return isReachStandard, nil } } _, err = session.Where(builder.Eq{"id": userID}).Incr("`rank`", deltaRank).Update(&entity.User{}) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return false, nil } func (ur *UserRankRepo) checkUserMinRank(_ context.Context, session *xorm.Session, userID string, deltaRank int) ( isReachStandard bool, err error, ) { bean := &entity.User{ID: userID} _, err = session.Select("`rank`").Get(bean) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if bean.Rank+deltaRank < 1 { log.Infof("user %s is rank %d out of range before rank operation", userID, deltaRank) return true, nil } return } func (ur *UserRankRepo) checkUserTodayRank(ctx context.Context, session *xorm.Session, userID string, activityType int, ) (isReachStandard bool, err error) { // exclude daily rank exclude, _ := ur.configService.GetArrayStringValue(ctx, "daily_rank_limit.exclude") for _, item := range exclude { cfg, err := ur.configService.GetConfigByKey(ctx, item) if err != nil { return false, err } if activityType == cfg.ID { return false, nil } } // get user start, end := now.BeginningOfDay(), now.EndOfDay() session.Where(builder.Eq{"user_id": userID}) session.Where(builder.Eq{"cancelled": 0}) session.Where(builder.Between{ Col: "updated_at", LessVal: start, MoreVal: end, }) earned, err := session.SumInt(&entity.Activity{}, "`rank`") if err != nil { return false, err } // max rank maxDailyRank, err := ur.configService.GetIntValue(ctx, "daily_rank_limit") if err != nil { return false, err } if int(earned) < maxDailyRank { return false, nil } log.Infof("user %s today has rank %d is reach stand %d", userID, earned, maxDailyRank) return true, nil } func (ur *UserRankRepo) UserRankPage(ctx context.Context, userID string, page, pageSize int) ( rankPage []*entity.Activity, total int64, err error, ) { rankPage = make([]*entity.Activity, 0) session := ur.data.DB.Context(ctx).Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0})).And(builder.Gt{"`rank`": 0}) session.Desc("created_at") cond := &entity.Activity{UserID: userID} total, err = pager.Help(page, pageSize, &rankPage, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/reason/reason_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package reason import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/reason_common" "github.com/segmentfault/pacman/log" ) type reasonRepo struct { configService *config.ConfigService } func NewReasonRepo(configService *config.ConfigService) reason_common.ReasonRepo { return &reasonRepo{ configService: configService, } } func (rr *reasonRepo) ListReasons(ctx context.Context, objectType, action string) (resp []*schema.ReasonItem, err error) { lang := handler.GetLangByCtx(ctx) reasonAction := fmt.Sprintf("%s.%s.reasons", objectType, action) resp = make([]*schema.ReasonItem, 0) reasonKeys, err := rr.configService.GetArrayStringValue(ctx, reasonAction) if err != nil { return nil, err } for _, reasonKey := range reasonKeys { cfg, err := rr.configService.GetConfigByKey(ctx, reasonKey) if err != nil { log.Error(err) continue } reason := &schema.ReasonItem{} err = json.Unmarshal(cfg.GetByteValue(), reason) if err != nil { log.Error(err) continue } reason.Translate(reasonKey, lang) reason.ReasonType = cfg.ID resp = append(resp, reason) } return resp, nil } ================================================ FILE: internal/repo/repo_test/auth_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/auth" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( accessToken = "token" visitToken = "visitToken" userID = "1" ) func Test_authRepo_SetUserCacheInfo(t *testing.T) { authRepo := auth.NewAuthRepo(testDataSource) err := authRepo.SetUserCacheInfo(context.TODO(), accessToken, visitToken, &entity.UserCacheInfo{UserID: userID}) require.NoError(t, err) cacheInfo, err := authRepo.GetUserCacheInfo(context.TODO(), accessToken) require.NoError(t, err) assert.Equal(t, userID, cacheInfo.UserID) } func Test_authRepo_RemoveUserCacheInfo(t *testing.T) { authRepo := auth.NewAuthRepo(testDataSource) err := authRepo.SetUserCacheInfo(context.TODO(), accessToken, visitToken, &entity.UserCacheInfo{UserID: userID}) require.NoError(t, err) err = authRepo.RemoveUserCacheInfo(context.TODO(), accessToken) require.NoError(t, err) userInfo, err := authRepo.GetUserCacheInfo(context.TODO(), accessToken) require.NoError(t, err) assert.Nil(t, userInfo) } func Test_authRepo_SetUserStatus(t *testing.T) { authRepo := auth.NewAuthRepo(testDataSource) err := authRepo.SetUserStatus(context.TODO(), userID, &entity.UserCacheInfo{UserID: userID}) require.NoError(t, err) cacheInfo, err := authRepo.GetUserStatus(context.TODO(), userID) require.NoError(t, err) assert.Equal(t, userID, cacheInfo.UserID) } func Test_authRepo_RemoveUserStatus(t *testing.T) { authRepo := auth.NewAuthRepo(testDataSource) err := authRepo.SetUserStatus(context.TODO(), userID, &entity.UserCacheInfo{UserID: userID}) require.NoError(t, err) err = authRepo.RemoveUserStatus(context.TODO(), userID) require.NoError(t, err) userInfo, err := authRepo.GetUserStatus(context.TODO(), userID) require.NoError(t, err) assert.Nil(t, userInfo) } func Test_authRepo_SetAdminUserCacheInfo(t *testing.T) { authRepo := auth.NewAuthRepo(testDataSource) err := authRepo.SetAdminUserCacheInfo(context.TODO(), accessToken, &entity.UserCacheInfo{UserID: userID}) require.NoError(t, err) cacheInfo, err := authRepo.GetAdminUserCacheInfo(context.TODO(), accessToken) require.NoError(t, err) assert.Equal(t, userID, cacheInfo.UserID) } func Test_authRepo_RemoveAdminUserCacheInfo(t *testing.T) { authRepo := auth.NewAuthRepo(testDataSource) err := authRepo.SetAdminUserCacheInfo(context.TODO(), accessToken, &entity.UserCacheInfo{UserID: userID}) require.NoError(t, err) err = authRepo.RemoveAdminUserCacheInfo(context.TODO(), accessToken) require.NoError(t, err) userInfo, err := authRepo.GetAdminUserCacheInfo(context.TODO(), accessToken) require.NoError(t, err) assert.Nil(t, userInfo) } ================================================ FILE: internal/repo/repo_test/captcha_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "github.com/apache/answer/internal/repo/captcha" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( ip = "127.0.0.1" actionType = "actionType" amount = 1 ) func Test_captchaRepo_DelActionType(t *testing.T) { captchaRepo := captcha.NewCaptchaRepo(testDataSource) err := captchaRepo.SetActionType(context.TODO(), ip, actionType, "", amount) require.NoError(t, err) actionInfo, err := captchaRepo.GetActionType(context.TODO(), ip, actionType) require.NoError(t, err) assert.Equal(t, amount, actionInfo.Num) err = captchaRepo.DelActionType(context.TODO(), ip, actionType) require.NoError(t, err) } func Test_captchaRepo_SetCaptcha(t *testing.T) { captchaRepo := captcha.NewCaptchaRepo(testDataSource) key, capt := "key", "1234" err := captchaRepo.SetCaptcha(context.TODO(), key, capt) require.NoError(t, err) gotCaptcha, err := captchaRepo.GetCaptcha(context.TODO(), key) require.NoError(t, err) assert.Equal(t, capt, gotCaptcha) } ================================================ FILE: internal/repo/repo_test/comment_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/comment" "github.com/apache/answer/internal/repo/unique" commentService "github.com/apache/answer/internal/service/comment" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func buildCommentEntity() *entity.Comment { return &entity.Comment{ UserID: "1", ObjectID: "1", QuestionID: "1", VoteCount: 1, Status: entity.CommentStatusAvailable, OriginalText: "# title", ParsedText: "

Title

", } } func Test_commentRepo_AddComment(t *testing.T) { uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) testCommentEntity := buildCommentEntity() err := commentRepo.AddComment(context.TODO(), testCommentEntity) require.NoError(t, err) err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) require.NoError(t, err) } func Test_commentRepo_GetCommentPage(t *testing.T) { uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) testCommentEntity := buildCommentEntity() err := commentRepo.AddComment(context.TODO(), testCommentEntity) require.NoError(t, err) resp, total, err := commentRepo.GetCommentPage(context.TODO(), &commentService.CommentQuery{ PageCond: pager.PageCond{ Page: 1, PageSize: 10, }, }) require.NoError(t, err) assert.Equal(t, int64(1), total) assert.Equal(t, resp[0].ID, testCommentEntity.ID) err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) require.NoError(t, err) } func Test_commentRepo_UpdateComment(t *testing.T) { uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) commonCommentRepo := comment.NewCommentCommonRepo(testDataSource, uniqueIDRepo) testCommentEntity := buildCommentEntity() err := commentRepo.AddComment(context.TODO(), testCommentEntity) require.NoError(t, err) testCommentEntity.ParsedText = "test" err = commentRepo.UpdateCommentContent(context.TODO(), testCommentEntity.ID, "test", "test") require.NoError(t, err) newComment, exist, err := commonCommentRepo.GetComment(context.TODO(), testCommentEntity.ID) require.NoError(t, err) assert.True(t, exist) assert.Equal(t, testCommentEntity.ParsedText, newComment.ParsedText) err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) require.NoError(t, err) } func Test_commentRepo_CannotGetDeletedComment(t *testing.T) { uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) testCommentEntity := buildCommentEntity() err := commentRepo.AddComment(context.TODO(), testCommentEntity) require.NoError(t, err) err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) require.NoError(t, err) _, exist, err := commentRepo.GetComment(context.TODO(), testCommentEntity.ID) require.NoError(t, err) assert.False(t, exist) } ================================================ FILE: internal/repo/repo_test/email_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "time" "github.com/apache/answer/internal/repo/export" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_emailRepo_VerifyCode(t *testing.T) { emailRepo := export.NewEmailRepo(testDataSource) code, content := "1111", "{\"source_type\":\"\",\"e_mail\":\"\",\"user_id\":\"1\",\"skip_validation_latest_code\":false}" err := emailRepo.SetCode(context.TODO(), "1", code, content, time.Minute) require.NoError(t, err) verifyContent, err := emailRepo.VerifyCode(context.TODO(), code) require.NoError(t, err) assert.Equal(t, content, verifyContent) } ================================================ FILE: internal/repo/repo_test/meta_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/meta" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func buildMetaEntity() *entity.Meta { return &entity.Meta{ ObjectID: "1", Key: "1", Value: "1", } } func Test_metaRepo_GetMetaByObjectIdAndKey(t *testing.T) { metaRepo := meta.NewMetaRepo(testDataSource) metaEnt := buildMetaEntity() err := metaRepo.AddMeta(context.TODO(), metaEnt) require.NoError(t, err) gotMeta, exist, err := metaRepo.GetMetaByObjectIdAndKey(context.TODO(), metaEnt.ObjectID, metaEnt.Key) require.NoError(t, err) assert.True(t, exist) assert.Equal(t, metaEnt.ID, gotMeta.ID) err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) require.NoError(t, err) } func Test_metaRepo_GetMetaList(t *testing.T) { metaRepo := meta.NewMetaRepo(testDataSource) metaEnt := buildMetaEntity() err := metaRepo.AddMeta(context.TODO(), metaEnt) require.NoError(t, err) gotMetaList, err := metaRepo.GetMetaList(context.TODO(), metaEnt) require.NoError(t, err) assert.Len(t, gotMetaList, 1) assert.Equal(t, gotMetaList[0].ID, metaEnt.ID) err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) require.NoError(t, err) } func Test_metaRepo_GetMetaPage(t *testing.T) { metaRepo := meta.NewMetaRepo(testDataSource) metaEnt := buildMetaEntity() err := metaRepo.AddMeta(context.TODO(), metaEnt) require.NoError(t, err) gotMetaList, err := metaRepo.GetMetaList(context.TODO(), metaEnt) require.NoError(t, err) assert.Len(t, gotMetaList, 1) assert.Equal(t, gotMetaList[0].ID, metaEnt.ID) err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) require.NoError(t, err) } func Test_metaRepo_UpdateMeta(t *testing.T) { metaRepo := meta.NewMetaRepo(testDataSource) metaEnt := buildMetaEntity() err := metaRepo.AddMeta(context.TODO(), metaEnt) require.NoError(t, err) metaEnt.Value = "testing" err = metaRepo.UpdateMeta(context.TODO(), metaEnt) require.NoError(t, err) gotMeta, exist, err := metaRepo.GetMetaByObjectIdAndKey(context.TODO(), metaEnt.ObjectID, metaEnt.Key) require.NoError(t, err) assert.True(t, exist) assert.Equal(t, gotMeta.Value, metaEnt.Value) err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) require.NoError(t, err) } ================================================ FILE: internal/repo/repo_test/notification_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/notification" "github.com/apache/answer/internal/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func buildNotificationEntity() *entity.Notification { return &entity.Notification{ UserID: "1", ObjectID: "1", Content: "1", Type: schema.NotificationTypeInbox, IsRead: schema.NotificationNotRead, Status: schema.NotificationStatusNormal, } } func Test_notificationRepo_ClearIDUnRead(t *testing.T) { notificationRepo := notification.NewNotificationRepo(testDataSource) ent := buildNotificationEntity() err := notificationRepo.AddNotification(context.TODO(), ent) require.NoError(t, err) err = notificationRepo.ClearIDUnRead(context.TODO(), ent.UserID, ent.ID) require.NoError(t, err) got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) require.NoError(t, err) assert.True(t, exists) assert.Equal(t, schema.NotificationRead, got.IsRead) } func Test_notificationRepo_ClearUnRead(t *testing.T) { notificationRepo := notification.NewNotificationRepo(testDataSource) ent := buildNotificationEntity() err := notificationRepo.AddNotification(context.TODO(), ent) require.NoError(t, err) err = notificationRepo.ClearUnRead(context.TODO(), ent.UserID, ent.Type) require.NoError(t, err) got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) require.NoError(t, err) assert.True(t, exists) assert.Equal(t, schema.NotificationRead, got.IsRead) } func Test_notificationRepo_GetById(t *testing.T) { notificationRepo := notification.NewNotificationRepo(testDataSource) ent := buildNotificationEntity() err := notificationRepo.AddNotification(context.TODO(), ent) require.NoError(t, err) got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) require.NoError(t, err) assert.True(t, exists) assert.Equal(t, got.ID, ent.ID) } func Test_notificationRepo_GetByUserIdObjectIdTypeId(t *testing.T) { notificationRepo := notification.NewNotificationRepo(testDataSource) ent := buildNotificationEntity() err := notificationRepo.AddNotification(context.TODO(), ent) require.NoError(t, err) got, exists, err := notificationRepo.GetByUserIdObjectIdTypeId(context.TODO(), ent.UserID, ent.ObjectID, ent.Type) require.NoError(t, err) assert.True(t, exists) assert.Equal(t, got.ObjectID, ent.ObjectID) } func Test_notificationRepo_GetNotificationPage(t *testing.T) { notificationRepo := notification.NewNotificationRepo(testDataSource) ent := buildNotificationEntity() err := notificationRepo.AddNotification(context.TODO(), ent) require.NoError(t, err) notificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: ent.UserID}) require.NoError(t, err) assert.Positive(t, total) assert.Equal(t, notificationPage[0].UserID, ent.UserID) } func Test_notificationRepo_UpdateNotificationContent(t *testing.T) { notificationRepo := notification.NewNotificationRepo(testDataSource) ent := buildNotificationEntity() err := notificationRepo.AddNotification(context.TODO(), ent) require.NoError(t, err) ent.Content = "test" err = notificationRepo.UpdateNotificationContent(context.TODO(), ent) require.NoError(t, err) got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) require.NoError(t, err) assert.True(t, exists) assert.Equal(t, got.Content, ent.Content) } ================================================ FILE: internal/repo/repo_test/reason_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "github.com/apache/answer/internal/repo/config" serviceconfig "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/repo/reason" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_reasonRepo_ListReasons(t *testing.T) { configRepo := config.NewConfigRepo(testDataSource) reasonRepo := reason.NewReasonRepo(serviceconfig.NewConfigService(configRepo)) reasonItems, err := reasonRepo.ListReasons(context.TODO(), "question", "close") require.NoError(t, err) assert.Len(t, reasonItems, 4) } ================================================ FILE: internal/repo/repo_test/recommend_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/activity" "github.com/apache/answer/internal/repo/activity_common" "github.com/apache/answer/internal/repo/config" "github.com/apache/answer/internal/repo/question" "github.com/apache/answer/internal/repo/tag" "github.com/apache/answer/internal/repo/tag_common" "github.com/apache/answer/internal/repo/unique" "github.com/apache/answer/internal/repo/user" config2 "github.com/apache/answer/internal/service/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_questionRepo_GetRecommend(t *testing.T) { var ( uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) questionRepo = question.NewQuestionRepo(testDataSource, uniqueIDRepo) userRepo = user.NewUserRepo(testDataSource) tagRelRepo = tag.NewTagRelRepo(testDataSource, uniqueIDRepo) tagRepo = tag.NewTagRepo(testDataSource, uniqueIDRepo) tagCommenRepo = tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo) configRepo = config.NewConfigRepo(testDataSource) configService = config2.NewConfigService(configRepo) activityCommonRepo = activity_common.NewActivityRepo(testDataSource, uniqueIDRepo, configService) followRepo = activity.NewFollowRepo(testDataSource, uniqueIDRepo, activityCommonRepo) ) // create question and user user := &entity.User{ Username: "ferrischi201", Pass: "ferrischi201", EMail: "ferrischi201@example.com", MailStatus: entity.EmailStatusAvailable, Status: entity.UserStatusAvailable, DisplayName: "ferrischi201", IsAdmin: false, } err := userRepo.AddUser(context.TODO(), user) require.NoError(t, err) assert.NotEmpty(t, user.ID) questions := make([]*entity.Question, 0) // tag, unjoin, unfollow questions = append(questions, &entity.Question{ UserID: "1", Title: "Valid recommendation 1", OriginalText: "A go question", ParsedText: "Go question", Status: entity.QuestionStatusAvailable, Show: entity.QuestionShow, }) // tag, unjoin, follow questions = append(questions, &entity.Question{ UserID: "1", Title: "Valid recommendation 2", OriginalText: "A go question", ParsedText: "Go question", Status: entity.QuestionStatusAvailable, Show: entity.QuestionShow, }) // tag, join, unfollow questions = append(questions, &entity.Question{ UserID: user.ID, Title: "Invalid recommendation 1", OriginalText: "A go question 1", ParsedText: "Go question", Status: entity.QuestionStatusAvailable, Show: entity.QuestionShow, }) // tag, join, follow questions = append(questions, &entity.Question{ UserID: user.ID, Title: "Valid recommendation 3", OriginalText: "A java question", ParsedText: "Java question", Status: entity.QuestionStatusAvailable, Show: entity.QuestionShow, }) // untag, unjoin, unfollow questions = append(questions, &entity.Question{ UserID: "1", Title: "Invalid recommendation 2", OriginalText: "A go question", ParsedText: "Go question", Status: entity.QuestionStatusAvailable, Show: entity.QuestionShow, }) // untag, unjoin, follow questions = append(questions, &entity.Question{ UserID: "1", Title: "Valid recommendation 4", OriginalText: "A go question", ParsedText: "Go question", Status: entity.QuestionStatusAvailable, Show: entity.QuestionShow, }) // untag, join, unfollow questions = append(questions, &entity.Question{ UserID: user.ID, Title: "Invalid recommendation 3", OriginalText: "A go question 1", ParsedText: "Go question", Status: entity.QuestionStatusAvailable, Show: entity.QuestionShow, }) // untag, join, follow questions = append(questions, &entity.Question{ UserID: user.ID, Title: "Valid recommendation 5", OriginalText: "A java question", ParsedText: "Java question", Status: entity.QuestionStatusAvailable, Show: entity.QuestionShow, }) for _, question := range questions { err = questionRepo.AddQuestion(context.TODO(), question) require.NoError(t, err) assert.NotEmpty(t, question.ID) } tags := []*entity.Tag{ { SlugName: "go", DisplayName: "Golang", OriginalText: "golang", ParsedText: "

golang

", Status: entity.TagStatusAvailable, }, { SlugName: "java", DisplayName: "Java", OriginalText: "java", ParsedText: "

java

", Status: entity.TagStatusAvailable, }, } err = tagCommenRepo.AddTagList(context.TODO(), tags) require.NoError(t, err) tagRels := make([]*entity.TagRel, 0) for i, question := range questions { tagRel := &entity.TagRel{ TagID: tags[i/4].ID, ObjectID: question.ID, Status: entity.TagRelStatusAvailable, } tagRels = append(tagRels, tagRel) } err = tagRelRepo.AddTagRelList(context.TODO(), tagRels) require.NoError(t, err) followQuestionIDs := make([]string, 0) for i := range questions { if i%2 == 0 { continue } err = followRepo.Follow(context.TODO(), questions[i].ID, user.ID) require.NoError(t, err) followQuestionIDs = append(followQuestionIDs, questions[i].ID) } // get recommend questionList, total, err := questionRepo.GetRecommendQuestionPageByTags(context.TODO(), user.ID, []string{tags[0].ID}, followQuestionIDs, 1, 20) require.NoError(t, err) assert.Equal(t, int64(5), total) assert.Len(t, questionList, 5) // recovery t.Cleanup(func() { tagRelIDs := make([]int64, 0) for i, tagRel := range tagRels { if i%2 == 1 { err = followRepo.FollowCancel(context.TODO(), questions[i].ID, user.ID) require.NoError(t, err) } tagRelIDs = append(tagRelIDs, tagRel.ID) } err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), tagRelIDs) require.NoError(t, err) for _, tag := range tags { err = tagRepo.RemoveTag(context.TODO(), tag.ID) require.NoError(t, err) } for _, q := range questions { err = questionRepo.RemoveQuestion(context.TODO(), q.ID) require.NoError(t, err) } }) } ================================================ FILE: internal/repo/repo_test/repo_main_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "database/sql" "fmt" "os" "path/filepath" "testing" "time" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/migrations" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" "github.com/segmentfault/pacman/cache" "github.com/segmentfault/pacman/log" "xorm.io/xorm" "xorm.io/xorm/schemas" ) var ( mysqlDBSetting = TestDBSetting{ Driver: string(schemas.MYSQL), ImageName: "mariadb", ImageVersion: "10.4.7", ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=answer", "MYSQL_ROOT_HOST=%"}, PortID: "3306/tcp", Connection: "root:root@(localhost:%s)/answer?parseTime=true", // port is not fixed, it will be got by port id } postgresDBSetting = TestDBSetting{ Driver: string(schemas.POSTGRES), ImageName: "postgres", ImageVersion: "14", ENV: []string{"POSTGRES_USER=root", "POSTGRES_PASSWORD=root", "POSTGRES_DB=answer", "LISTEN_ADDRESSES='*'"}, PortID: "5432/tcp", Connection: "host=localhost port=%s user=root password=root dbname=answer sslmode=disable", } sqlite3DBSetting = TestDBSetting{ Driver: string(schemas.SQLITE), Connection: filepath.Join(os.TempDir(), "answer-test-data.db"), } dbSettingMapping = map[string]TestDBSetting{ mysqlDBSetting.Driver: mysqlDBSetting, sqlite3DBSetting.Driver: sqlite3DBSetting, postgresDBSetting.Driver: postgresDBSetting, } // after all test down will execute tearDown function to clean-up tearDown func() // testDataSource used for repo testing testDataSource *data.Data ) func TestMain(t *testing.M) { dbSetting, ok := dbSettingMapping[os.Getenv("TEST_DB_DRIVER")] if !ok { // Use sqlite3 to test. dbSetting = dbSettingMapping[string(schemas.SQLITE)] } if dbSetting.Driver == string(schemas.SQLITE) { _ = os.RemoveAll(dbSetting.Connection) } defer func() { if tearDown != nil { tearDown() } }() if err := initTestDataSource(dbSetting); err != nil { panic(err) } log.Info("init test database successfully") if ret := t.Run(); ret != 0 { panic(ret) } } type TestDBSetting struct { Driver string ImageName string ImageVersion string ENV []string PortID string Connection string } func initTestDataSource(dbSetting TestDBSetting) error { connection, imageCleanUp, err := initDatabaseImage(dbSetting) if err != nil { return err } dbSetting.Connection = connection dbEngine, err := initDatabase(dbSetting) if err != nil { return err } newCache, err := initCache() if err != nil { return err } newData, dbCleanUp, err := data.NewData(dbEngine, newCache) if err != nil { return err } testDataSource = newData tearDown = func() { dbCleanUp() log.Info("cleanup test database successfully") imageCleanUp() log.Info("cleanup test database image successfully") } return nil } func initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func(), err error) { // sqlite3 don't need to set up image if dbSetting.Driver == string(schemas.SQLITE) { return dbSetting.Connection, func() { log.Info("remove database", dbSetting.Connection) err = os.Remove(dbSetting.Connection) if err != nil { log.Error(err) } }, nil } pool, err := dockertest.NewPool("") pool.MaxWait = time.Minute * 5 if err != nil { return "", nil, fmt.Errorf("could not connect to docker: %s", err) } // resource, err := pool.Run(dbSetting.ImageName, dbSetting.ImageVersion, dbSetting.ENV) resource, err := pool.RunWithOptions(&dockertest.RunOptions{ Repository: dbSetting.ImageName, Tag: dbSetting.ImageVersion, Env: dbSetting.ENV, }, func(config *docker.HostConfig) { config.AutoRemove = true config.RestartPolicy = docker.RestartPolicy{Name: "no"} }) if err != nil { return "", nil, fmt.Errorf("could not pull resource: %s", err) } connection = fmt.Sprintf(dbSetting.Connection, resource.GetPort(dbSetting.PortID)) if err := pool.Retry(func() error { db, err := sql.Open(dbSetting.Driver, connection) if err != nil { return err } return db.Ping() }); err != nil { return "", nil, fmt.Errorf("could not connect to database: %s", err) } return connection, func() { _ = pool.Purge(resource) }, nil } func initDatabase(dbSetting TestDBSetting) (dbEngine *xorm.Engine, err error) { dataConf := &data.Database{Driver: dbSetting.Driver, Connection: dbSetting.Connection} dbEngine, err = data.NewDB(true, dataConf) if err != nil { return nil, fmt.Errorf("connection to database failed: %s", err) } if err := migrations.NewMentor(context.TODO(), dbEngine, &migrations.InitNeedUserInputData{ Language: "en_US", SiteName: "ANSWER", SiteURL: "http://127.0.0.1:8080/", ContactEmail: "answer@answer.com", AdminName: "admin", AdminPassword: "admin", AdminEmail: "answer@answer.com", }).InitDB(); err != nil { return nil, fmt.Errorf("migrations init database failed: %s", err) } return dbEngine, nil } func initCache() (newCache cache.Cache, err error) { newCache, _, err = data.NewCache(&data.CacheConf{}) return newCache, err } ================================================ FILE: internal/repo/repo_test/revision_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "encoding/json" "testing" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/question" "github.com/apache/answer/internal/repo/revision" "github.com/apache/answer/internal/repo/unique" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var q = &entity.Question{ ID: "", UserID: "1", Title: "test", OriginalText: "test", ParsedText: "test", Status: entity.QuestionStatusAvailable, ViewCount: 0, UniqueViewCount: 0, VoteCount: 0, AnswerCount: 0, CollectionCount: 0, FollowCount: 0, AcceptedAnswerID: "", LastAnswerID: "", RevisionID: "0", } func getRev(objID, title, content string) *entity.Revision { return &entity.Revision{ ID: "", UserID: "1", ObjectID: objID, Title: title, Content: content, Log: "add rev", } } func Test_revisionRepo_AddRevision(t *testing.T) { var ( uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo) questionRepo = question.NewQuestionRepo(testDataSource, uniqueIDRepo) ) // create question err := questionRepo.AddQuestion(context.TODO(), q) require.NoError(t, err) assert.NotEmpty(t, q.ID) content, err := json.Marshal(q) require.NoError(t, err) // auto update false rev := getRev(q.ID, q.Title, string(content)) err = revisionRepo.AddRevision(context.TODO(), rev, false) require.NoError(t, err) qr, _, _ := questionRepo.GetQuestion(context.TODO(), q.ID) assert.NotEqual(t, rev.ID, qr.RevisionID) // auto update false rev = getRev(q.ID, q.Title, string(content)) err = revisionRepo.AddRevision(context.TODO(), rev, true) require.NoError(t, err) qr, _, _ = questionRepo.GetQuestion(context.TODO(), q.ID) assert.Equal(t, rev.ID, qr.RevisionID) // recovery t.Cleanup(func() { err = questionRepo.RemoveQuestion(context.TODO(), q.ID) require.NoError(t, err) }) } func Test_revisionRepo_GetLastRevisionByObjectID(t *testing.T) { var ( uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo) ) Test_revisionRepo_AddRevision(t) rev, exists, err := revisionRepo.GetLastRevisionByObjectID(context.TODO(), q.ID) require.NoError(t, err) assert.True(t, exists) assert.NotNil(t, rev) } func Test_revisionRepo_GetRevisionList(t *testing.T) { var ( uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo) ) Test_revisionRepo_AddRevision(t) revs, err := revisionRepo.GetRevisionList(context.TODO(), &entity.Revision{ObjectID: q.ID}) require.NoError(t, err) assert.GreaterOrEqual(t, len(revs), 1) } ================================================ FILE: internal/repo/repo_test/siteinfo_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/site_info" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_siteInfoRepo_SaveByType(t *testing.T) { siteInfoRepo := site_info.NewSiteInfo(testDataSource) data := &entity.SiteInfo{Content: "site_info", Type: "test"} err := siteInfoRepo.SaveByType(context.TODO(), data.Type, data) require.NoError(t, err) got, exist, err := siteInfoRepo.GetByType(context.TODO(), data.Type) require.NoError(t, err) assert.True(t, exist) assert.Equal(t, data.Content, got.Content) data.Content = "new site_info" err = siteInfoRepo.SaveByType(context.TODO(), data.Type, data) require.NoError(t, err) got, exist, err = siteInfoRepo.GetByType(context.TODO(), data.Type) require.NoError(t, err) assert.True(t, exist) assert.Equal(t, data.Content, got.Content) } ================================================ FILE: internal/repo/repo_test/tag_rel_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "log" "sync" "testing" "github.com/apache/answer/internal/repo/unique" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/tag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( tagRelOnce sync.Once testTagRelList = []*entity.TagRel{ { ObjectID: "10010000000000101", TagID: "10030000000000101", Status: entity.TagRelStatusAvailable, }, { ObjectID: "10010000000000202", TagID: "10030000000000202", Status: entity.TagRelStatusAvailable, }, } ) func addTagRelList() { tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) err := tagRelRepo.AddTagRelList(context.TODO(), testTagRelList) if err != nil { log.Fatalf("%+v", err) } } func Test_tagListRepo_BatchGetObjectTagRelList(t *testing.T) { tagRelOnce.Do(addTagRelList) tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) relList, err := tagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID}) require.NoError(t, err) assert.Len(t, relList, 2) } func Test_tagListRepo_CountTagRelByTagID(t *testing.T) { tagRelOnce.Do(addTagRelList) tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101") require.NoError(t, err) assert.Equal(t, int64(1), count) } func Test_tagListRepo_GetObjectTagRelList(t *testing.T) { tagRelOnce.Do(addTagRelList) tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) relList, err := tagRelRepo.GetObjectTagRelList(context.TODO(), testTagRelList[0].ObjectID) require.NoError(t, err) assert.Len(t, relList, 1) } func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) { tagRelOnce.Do(addTagRelList) tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) relList, err := tagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID}) require.NoError(t, err) assert.Len(t, relList, 2) ids := []int64{relList[0].ID, relList[1].ID} err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), ids) require.NoError(t, err) count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101") require.NoError(t, err) assert.Equal(t, int64(0), count) _, exist, err := tagRelRepo.GetObjectTagRelWithoutStatus(context.TODO(), relList[0].ObjectID, relList[0].TagID) require.NoError(t, err) assert.True(t, exist) err = tagRelRepo.EnableTagRelByIDs(context.TODO(), ids, false) require.NoError(t, err) count, err = tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101") require.NoError(t, err) assert.Equal(t, int64(1), count) } ================================================ FILE: internal/repo/repo_test/tag_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "fmt" "log" "sync" "testing" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/tag" "github.com/apache/answer/internal/repo/tag_common" "github.com/apache/answer/internal/repo/unique" "github.com/apache/answer/pkg/converter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( tagOnce sync.Once testTagList = []*entity.Tag{ { SlugName: "go", DisplayName: "Golang", OriginalText: "golang", ParsedText: "

golang

", Status: entity.TagStatusAvailable, }, { SlugName: "js", DisplayName: "javascript", OriginalText: "javascript", ParsedText: "

javascript

", Status: entity.TagStatusAvailable, }, { SlugName: "go2", DisplayName: "Golang2", OriginalText: "golang2", ParsedText: "

golang2

", Status: entity.TagStatusAvailable, }, } ) func addTagList() { uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo) err := tagCommonRepo.AddTagList(context.TODO(), testTagList) if err != nil { log.Fatalf("%+v", err) } } func Test_tagRepo_GetTagByID(t *testing.T) { tagOnce.Do(addTagList) tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true) require.NoError(t, err) assert.True(t, exist) assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName) } func Test_tagRepo_GetTagBySlugName(t *testing.T) { tagOnce.Do(addTagList) tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) gotTag, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName) require.NoError(t, err) assert.True(t, exist) assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName) } func Test_tagRepo_GetTagList(t *testing.T) { tagOnce.Do(addTagList) tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) gotTags, err := tagRepo.GetTagList(context.TODO(), &entity.Tag{ID: testTagList[0].ID}) require.NoError(t, err) assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) } func Test_tagRepo_GetTagListByIDs(t *testing.T) { tagOnce.Do(addTagList) tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) gotTags, err := tagCommonRepo.GetTagListByIDs(context.TODO(), []string{testTagList[0].ID}) require.NoError(t, err) assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) } func Test_tagRepo_GetTagListByName(t *testing.T) { tagOnce.Do(addTagList) tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) gotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, false, false) require.NoError(t, err) assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) } func Test_tagRepo_GetTagListByNames(t *testing.T) { tagOnce.Do(addTagList) tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) gotTags, err := tagCommonRepo.GetTagListByNames(context.TODO(), []string{testTagList[0].SlugName}) require.NoError(t, err) assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) } func Test_tagRepo_GetTagPage(t *testing.T) { tagOnce.Do(addTagList) tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) gotTags, _, err := tagCommonRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, "") require.NoError(t, err) assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) } func Test_tagRepo_RemoveTag(t *testing.T) { tagOnce.Do(addTagList) uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo) err := tagRepo.RemoveTag(context.TODO(), testTagList[1].ID) require.NoError(t, err) tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) _, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName) require.NoError(t, err) assert.False(t, exist) } func Test_tagRepo_UpdateTag(t *testing.T) { uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo) testTagList[0].DisplayName = "golang" err := tagRepo.UpdateTag(context.TODO(), testTagList[0]) require.NoError(t, err) tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true) require.NoError(t, err) assert.True(t, exist) assert.Equal(t, testTagList[0].DisplayName, gotTag.DisplayName) } func Test_tagRepo_UpdateTagQuestionCount(t *testing.T) { tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) testTagList[0].DisplayName = "golang" err := tagCommonRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100) require.NoError(t, err) gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true) require.NoError(t, err) assert.True(t, exist) assert.Equal(t, 100, gotTag.QuestionCount) } func Test_tagRepo_UpdateTagSynonym(t *testing.T) { uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo) testTagList[0].DisplayName = "golang" err := tagRepo.UpdateTag(context.TODO(), testTagList[0]) require.NoError(t, err) err = tagRepo.UpdateTagSynonym(context.TODO(), []string{testTagList[2].SlugName}, converter.StringToInt64(testTagList[0].ID), testTagList[0].SlugName) require.NoError(t, err) tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID, true) require.NoError(t, err) assert.True(t, exist) assert.Equal(t, testTagList[0].ID, fmt.Sprintf("%d", gotTag.MainTagID)) } ================================================ FILE: internal/repo/repo_test/user_backyard_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "time" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/auth" "github.com/apache/answer/internal/repo/user" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_userAdminRepo_GetUserInfo(t *testing.T) { userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource)) got, exist, err := userAdminRepo.GetUserInfo(context.TODO(), "1") require.NoError(t, err) assert.True(t, exist) assert.Equal(t, "1", got.ID) } func Test_userAdminRepo_GetUserPage(t *testing.T) { userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource)) got, total, err := userAdminRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}, "", false) require.NoError(t, err) assert.Equal(t, int64(1), total) assert.Equal(t, "1", got[0].ID) } func Test_userAdminRepo_UpdateUserStatus(t *testing.T) { userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource)) got, exist, err := userAdminRepo.GetUserInfo(context.TODO(), "1") require.NoError(t, err) assert.True(t, exist) assert.Equal(t, entity.UserStatusAvailable, got.Status) err = userAdminRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusSuspended, entity.EmailStatusAvailable, "admin@admin.com", time.Now().Add(time.Minute*5)) require.NoError(t, err) got, exist, err = userAdminRepo.GetUserInfo(context.TODO(), "1") require.NoError(t, err) assert.True(t, exist) assert.Equal(t, entity.UserStatusSuspended, got.Status) err = userAdminRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusAvailable, entity.EmailStatusAvailable, "admin@admin.com", time.Time{}) require.NoError(t, err) got, exist, err = userAdminRepo.GetUserInfo(context.TODO(), "1") require.NoError(t, err) assert.True(t, exist) assert.Equal(t, entity.UserStatusAvailable, got.Status) } ================================================ FILE: internal/repo/repo_test/user_repo_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package repo_test import ( "context" "testing" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/user" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_userRepo_AddUser(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) userInfo := &entity.User{ Username: "answer", Pass: "answer", EMail: "answer@example.com", MailStatus: entity.EmailStatusAvailable, Status: entity.UserStatusAvailable, DisplayName: "answer", IsAdmin: false, } err := userRepo.AddUser(context.TODO(), userInfo) require.NoError(t, err) } func Test_userRepo_BatchGetByID(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) got, err := userRepo.BatchGetByID(context.TODO(), []string{"1"}) require.NoError(t, err) assert.Len(t, got, 1) assert.Equal(t, "admin", got[0].Username) } func Test_userRepo_GetByEmail(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) got, exist, err := userRepo.GetByEmail(context.TODO(), "admin@admin.com") require.NoError(t, err) assert.True(t, exist) assert.Equal(t, "admin", got.Username) } func Test_userRepo_GetByUserID(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) got, exist, err := userRepo.GetByUserID(context.TODO(), "1") require.NoError(t, err) assert.True(t, exist) assert.Equal(t, "admin", got.Username) } func Test_userRepo_GetByUsername(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) got, exist, err := userRepo.GetByUsername(context.TODO(), "admin") require.NoError(t, err) assert.True(t, exist) assert.Equal(t, "admin", got.Username) } func Test_userRepo_IncreaseAnswerCount(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) err := userRepo.IncreaseAnswerCount(context.TODO(), "1", 1) require.NoError(t, err) got, exist, err := userRepo.GetByUserID(context.TODO(), "1") require.NoError(t, err) assert.True(t, exist) assert.Equal(t, 1, got.AnswerCount) } func Test_userRepo_IncreaseQuestionCount(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) err := userRepo.IncreaseQuestionCount(context.TODO(), "1", 1) require.NoError(t, err) got, exist, err := userRepo.GetByUserID(context.TODO(), "1") require.NoError(t, err) assert.True(t, exist) assert.Equal(t, 1, got.AnswerCount) } func Test_userRepo_UpdateEmail(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdateEmail(context.TODO(), "1", "admin@admin.com") require.NoError(t, err) } func Test_userRepo_UpdateEmailStatus(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdateEmailStatus(context.TODO(), "1", entity.EmailStatusToBeVerified) require.NoError(t, err) } func Test_userRepo_UpdateInfo(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdateInfo(context.TODO(), &entity.User{ID: "1", Bio: "test"}) require.NoError(t, err) got, exist, err := userRepo.GetByUserID(context.TODO(), "1") require.NoError(t, err) assert.True(t, exist) assert.Equal(t, "test", got.Bio) } func Test_userRepo_UpdateLastLoginDate(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdateLastLoginDate(context.TODO(), "1") require.NoError(t, err) } func Test_userRepo_UpdateNoticeStatus(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdateNoticeStatus(context.TODO(), "1", 1) require.NoError(t, err) } func Test_userRepo_UpdatePass(t *testing.T) { userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdatePass(context.TODO(), "1", "admin") require.NoError(t, err) } ================================================ FILE: internal/repo/report/report_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package report import ( "context" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/report_common" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" ) // reportRepo report repository type reportRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } // NewReportRepo new repository func NewReportRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) report_common.ReportRepo { return &reportRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } // AddReport add report func (rr *reportRepo) AddReport(ctx context.Context, report *entity.Report) (err error) { report.ID, err = rr.uniqueIDRepo.GenUniqueIDStr(ctx, report.TableName()) if err != nil { return err } _, err = rr.data.DB.Context(ctx).Insert(report) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetReportListPage get report list page func (rr *reportRepo) GetReportListPage(ctx context.Context, dto *schema.GetReportListPageDTO) ( reports []*entity.Report, total int64, err error) { cond := &entity.Report{} cond.Status = dto.Status session := rr.data.DB.Context(ctx).Desc("updated_at") total, err = pager.Help(dto.Page, dto.PageSize, &reports, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetByID get report by ID func (rr *reportRepo) GetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error) { report = &entity.Report{} exist, err = rr.data.DB.Context(ctx).ID(id).Get(report) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateStatus update report status by ID func (rr *reportRepo) UpdateStatus(ctx context.Context, id string, status int) (err error) { _, err = rr.data.DB.Context(ctx).ID(id).Update(&entity.Report{Status: status}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (rr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) { list := make([]*entity.Report, 0) count, err = rr.data.DB.Context(ctx).Where("status =?", entity.ReportStatusPending).FindAndCount(&list) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/review/review_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package review import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/review" "github.com/segmentfault/pacman/errors" ) // reviewRepo review repository type reviewRepo struct { data *data.Data } // NewReviewRepo new repository func NewReviewRepo(data *data.Data) review.ReviewRepo { return &reviewRepo{ data: data, } } // AddReview add review func (cr *reviewRepo) AddReview(ctx context.Context, review *entity.Review) (err error) { _, err = cr.data.DB.Context(ctx).Insert(review) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateReviewStatus update review status func (cr *reviewRepo) UpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error) { _, err = cr.data.DB.Context(ctx).ID(reviewID).Update(&entity.Review{ ReviewerUserID: reviewerUserID, Status: status}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetReview get review one func (cr *reviewRepo) GetReview(ctx context.Context, reviewID int) ( review *entity.Review, exist bool, err error) { review = &entity.Review{} exist, err = cr.data.DB.Context(ctx).ID(reviewID).Get(review) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetReviewByObject get review by object func (cr *reviewRepo) GetReviewByObject(ctx context.Context, objectID string) (review *entity.Review, exist bool, err error) { review = &entity.Review{} exist, err = cr.data.DB.Context(ctx).Desc("id").Where("object_id = ?", objectID).Get(review) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetReviewCount get review count func (cr *reviewRepo) GetReviewCount(ctx context.Context, status int) (count int64, err error) { count, err = cr.data.DB.Context(ctx).Count(&entity.Review{Status: status}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetReviewPage get review page func (cr *reviewRepo) GetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) ( reviewList []*entity.Review, total int64, err error) { session := cr.data.DB.Context(ctx).Asc("created_at") reviewList = make([]*entity.Review, 0) total, err = pager.Help(page, pageSize, &reviewList, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/revision/revision_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package revision import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/revision" "github.com/apache/answer/internal/service/unique" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/obj" "github.com/segmentfault/pacman/errors" "xorm.io/builder" "xorm.io/xorm" ) // revisionRepo revision repository type revisionRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } // NewRevisionRepo new repository func NewRevisionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) revision.RevisionRepo { return &revisionRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } // AddRevision add revision // autoUpdateRevisionID bool : if autoUpdateRevisionID is true , the object.revision_id will be updated, // if not need auto update object.revision_id, it must be false. // example: user can edit the object, but need audit, the revision_id will be updated when admin approved func (rr *revisionRepo) AddRevision(ctx context.Context, revision *entity.Revision, autoUpdateRevisionID bool) (err error) { objectTypeNumber, err := obj.GetObjectTypeNumberByObjectID(revision.ObjectID) if err != nil { return errors.BadRequest(reason.ObjectNotFound) } revision.ObjectType = objectTypeNumber if !rr.allowRecord(revision.ObjectType) { return nil } _, err = rr.data.DB.Transaction(func(session *xorm.Session) (any, error) { session = session.Context(ctx) _, err = session.Insert(revision) if err != nil { _ = session.Rollback() return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if autoUpdateRevisionID { err = rr.UpdateObjectRevisionId(ctx, revision, session) if err != nil { _ = session.Rollback() return nil, err } } return nil, nil }) return err } // UpdateObjectRevisionId updates the object.revision_id field func (rr *revisionRepo) UpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error) { tableName, err := obj.GetObjectTypeStrByObjectID(revision.ObjectID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _, err = session.Table(tableName).Where("id = ?", revision.ObjectID).Cols("`revision_id`").Update(struct { RevisionID string `xorm:"revision_id"` }{ RevisionID: revision.ID, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // UpdateStatus update revision status func (rr *revisionRepo) UpdateStatus(ctx context.Context, id string, status int, reviewUserID string) (err error) { if id == "" { return nil } var data entity.Revision data.ID = id data.Status = status data.ReviewUserID = converter.StringToInt64(reviewUserID) _, err = rr.data.DB.Context(ctx).Where("id =?", id).Cols("status", "review_user_id").Update(&data) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // GetRevision get revision one func (rr *revisionRepo) GetRevision(ctx context.Context, id string) ( revision *entity.Revision, exist bool, err error, ) { revision = &entity.Revision{} exist, err = rr.data.DB.Context(ctx).ID(id).Get(revision) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetRevisionByID get object's last revision by object TagID func (rr *revisionRepo) GetRevisionByID(ctx context.Context, revisionID string) ( revision *entity.Revision, exist bool, err error) { revision = &entity.Revision{} exist, err = rr.data.DB.Context(ctx).Where("id = ?", revisionID).Get(revision) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (rr *revisionRepo) ExistUnreviewedByObjectID(ctx context.Context, objectID string) ( revision *entity.Revision, exist bool, err error) { revision = &entity.Revision{} exist, err = rr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.RevisionUnreviewedStatus).Get(revision) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetLastRevisionByObjectID get object's last revision by object TagID func (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID string) ( revision *entity.Revision, exist bool, err error, ) { revision = &entity.Revision{} exist, err = rr.data.DB.Context(ctx).Where("object_id = ?", objectID).Desc("created_at").Get(revision) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetLastRevisionByFileURL get object's last revision by file url func (rr *revisionRepo) GetLastRevisionByFileURL(ctx context.Context, fileURL string) (revision *entity.Revision, exist bool, err error) { revision = &entity.Revision{} exist, err = rr.data.DB.Context(ctx).Where("content LIKE ?", "%"+fileURL+"%").Desc("created_at").Get(revision) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetRevisionList get revision list all func (rr *revisionRepo) GetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error) { revisionList = []entity.Revision{} err = rr.data.DB.Context(ctx).Where(builder.Eq{ "object_id": revision.ObjectID, }).OrderBy("created_at DESC").Find(&revisionList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // allowRecord check the object type can record revision or not func (rr *revisionRepo) allowRecord(objectType int) (ok bool) { switch objectType { case constant.ObjectTypeStrMapping["question"]: return true case constant.ObjectTypeStrMapping["answer"]: return true case constant.ObjectTypeStrMapping["tag"]: return true default: return false } } // GetUnreviewedRevisionPage get unreviewed revision page func (rr *revisionRepo) GetUnreviewedRevisionPage(ctx context.Context, page int, pageSize int, objectTypeList []int) (revisionList []*entity.Revision, total int64, err error) { revisionList = make([]*entity.Revision, 0) if len(objectTypeList) == 0 { return revisionList, 0, nil } session := rr.data.DB.Context(ctx) session = session.And("status = ?", entity.RevisionUnreviewedStatus) session = session.In("object_type", objectTypeList) session = session.OrderBy("created_at asc") total, err = pager.Help(page, pageSize, &revisionList, &entity.Revision{}, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // CountUnreviewedRevision get unreviewed revision count func (rr *revisionRepo) CountUnreviewedRevision(ctx context.Context, objectTypeList []int) (count int64, err error) { if len(objectTypeList) == 0 { return 0, nil } session := rr.data.DB.Context(ctx) session = session.And("status = ?", entity.RevisionUnreviewedStatus) session = session.In("object_type", objectTypeList) count, err = session.Count(&entity.Revision{}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/role/power_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package role import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/role" "github.com/segmentfault/pacman/errors" ) // powerRepo power repository type powerRepo struct { data *data.Data } // NewPowerRepo new repository func NewPowerRepo(data *data.Data) role.PowerRepo { return &powerRepo{ data: data, } } // GetPowerList get list all func (pr *powerRepo) GetPowerList(ctx context.Context, power *entity.Power) (powerList []*entity.Power, err error) { powerList = make([]*entity.Power, 0) err = pr.data.DB.Context(ctx).Find(&powerList, power) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/role/role_power_rel_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package role import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/service/role" "github.com/segmentfault/pacman/errors" "xorm.io/builder" ) // rolePowerRelRepo rolePowerRel repository type rolePowerRelRepo struct { data *data.Data } // NewRolePowerRelRepo new repository func NewRolePowerRelRepo(data *data.Data) role.RolePowerRelRepo { return &rolePowerRelRepo{ data: data, } } // GetRolePowerTypeList get role power type list func (rr *rolePowerRelRepo) GetRolePowerTypeList(ctx context.Context, roleID int) (powers []string, err error) { powers = make([]string, 0) err = rr.data.DB.Context(ctx).Table("role_power_rel"). Cols("power_type").Where(builder.Eq{"role_id": roleID}).Find(&powers) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/role/role_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package role import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" service "github.com/apache/answer/internal/service/role" "github.com/segmentfault/pacman/errors" ) // roleRepo role repository type roleRepo struct { data *data.Data } // NewRoleRepo new repository func NewRoleRepo(data *data.Data) service.RoleRepo { return &roleRepo{ data: data, } } // GetRoleAllList get role list all func (rr *roleRepo) GetRoleAllList(ctx context.Context) (roleList []*entity.Role, err error) { roleList = make([]*entity.Role, 0) err = rr.data.DB.Context(ctx).Find(&roleList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetRoleAllMapping get role all mapping func (rr *roleRepo) GetRoleAllMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) { roleList, err := rr.GetRoleAllList(ctx) if err != nil { return nil, err } roleMapping = make(map[int]*entity.Role, 0) for _, role := range roleList { roleMapping[role.ID] = role } return roleMapping, nil } ================================================ FILE: internal/repo/role/user_role_rel_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package role import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/role" "github.com/segmentfault/pacman/errors" "xorm.io/builder" "xorm.io/xorm" ) // userRoleRelRepo userRoleRel repository type userRoleRelRepo struct { data *data.Data } // NewUserRoleRelRepo new repository func NewUserRoleRelRepo(data *data.Data) role.UserRoleRelRepo { return &userRoleRelRepo{ data: data, } } // SaveUserRoleRel save user role rel func (ur *userRoleRelRepo) SaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error) { _, err = ur.data.DB.Transaction(func(session *xorm.Session) (any, error) { session = session.Context(ctx) item := &entity.UserRoleRel{UserID: userID} exist, err := session.Get(item) if err != nil { return nil, err } if exist { item.RoleID = roleID _, err = session.ID(item.ID).Update(item) } else { _, err = session.Insert(&entity.UserRoleRel{UserID: userID, RoleID: roleID}) } if err != nil { return nil, err } return nil, nil }) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetUserRoleRelList get user role all func (ur *userRoleRelRepo) GetUserRoleRelList(ctx context.Context, userIDs []string) ( userRoleRelList []*entity.UserRoleRel, err error) { userRoleRelList = make([]*entity.UserRoleRel, 0) err = ur.data.DB.Context(ctx).In("user_id", userIDs).Find(&userRoleRelList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetUserRoleRelListByRoleID get user role all by role id func (ur *userRoleRelRepo) GetUserRoleRelListByRoleID(ctx context.Context, roleIDs []int) ( userRoleRelList []*entity.UserRoleRel, err error) { userRoleRelList = make([]*entity.UserRoleRel, 0) err = ur.data.DB.Context(ctx).In("role_id", roleIDs).Find(&userRoleRelList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetUserRoleRel get user role func (ur *userRoleRelRepo) GetUserRoleRel(ctx context.Context, userID string) ( rolePowerRel *entity.UserRoleRel, exist bool, err error) { rolePowerRel = &entity.UserRoleRel{} exist, err = ur.data.DB.Context(ctx).Where(builder.Eq{"user_id": userID}).Get(rolePowerRel) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/search_common/search_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package search_common import ( "context" "fmt" "strconv" "strings" "time" tagcommon "github.com/apache/answer/internal/service/tag_common" "github.com/apache/answer/plugin" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/search_common" "github.com/apache/answer/internal/service/unique" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/obj" "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" "xorm.io/builder" ) var ( qFields = []string{ "`question`.`id`", "`question`.`id` as `question_id`", "`title`", "`parsed_text`", "`question`.`created_at` as `created_at`", "`user_id`", "`vote_count`", "`answer_count`", "CASE WHEN `accepted_answer_id` > 0 THEN 2 ELSE 0 END as `accepted`", "`question`.`status` as `status`", "`post_update_time`", } aFields = []string{ "`answer`.`id` as `id`", "`question_id`", "`question`.`title` as `title`", "`answer`.`parsed_text` as `parsed_text`", "`answer`.`created_at` as `created_at`", "`answer`.`user_id` as `user_id`", "`answer`.`vote_count` as `vote_count`", "0 as `answer_count`", "`adopted` as `accepted`", "`answer`.`status` as `status`", "`answer`.`created_at` as `post_update_time`", } ) // searchRepo tag repository type searchRepo struct { data *data.Data userCommon *usercommon.UserCommon uniqueIDRepo unique.UniqueIDRepo tagCommon *tagcommon.TagCommonService } // NewSearchRepo new repository func NewSearchRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userCommon *usercommon.UserCommon, tagCommon *tagcommon.TagCommonService, ) search_common.SearchRepo { return &searchRepo{ data: data, uniqueIDRepo: uniqueIDRepo, userCommon: userCommon, tagCommon: tagCommon, } } // SearchContents search question and answer data func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes int, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) { words = filterWords(words) var ( b *builder.Builder ub *builder.Builder qfs = qFields afs = aFields argsQ = []any{} argsA = []any{} ) if order == "relevance" { if len(words) > 0 { qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs) afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs) } else { order = "newest" } } b = builder.MySQL().Select(qfs...).From("`question`") ub = builder.MySQL().Select(afs...).From("`answer`"). LeftJoin("`question`", "`question`.id = `answer`.question_id") b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). And(builder.Eq{"`question`.`show`": entity.QuestionShow}) ub.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}). And(builder.Eq{"`question`.`show`": entity.QuestionShow}) argsQ = append(argsQ, entity.QuestionStatusDeleted, entity.QuestionShow) argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow) likeConQ := builder.NewCond() likeConA := builder.NewCond() for _, word := range words { likeConQ = likeConQ.Or(builder.Like{"title", word}). Or(builder.Like{"original_text", word}) argsQ = append(argsQ, "%"+word+"%") argsQ = append(argsQ, "%"+word+"%") likeConA = likeConA.Or(builder.Like{"`answer`.original_text", word}) argsA = append(argsA, "%"+word+"%") } b.Where(likeConQ) ub.Where(likeConA) // check tag for ti, tagID := range tagIDs { ast := "tag_rel" + strconv.Itoa(ti) b.Join("INNER", "tag_rel as "+ast, "question.id = "+ast+".object_id"). And(builder.Eq{ ast + ".status": entity.TagRelStatusAvailable, }). And(builder.In(ast+".tag_id", tagID)) ub.Join("INNER", "tag_rel as "+ast, "question_id = "+ast+".object_id"). And(builder.Eq{ ast + ".status": entity.TagRelStatusAvailable, }). And(builder.In(ast+".tag_id", tagID)) argsQ = append(argsQ, entity.TagRelStatusAvailable) argsA = append(argsA, entity.TagRelStatusAvailable) for _, t := range tagID { argsQ = append(argsQ, t) argsA = append(argsA, t) } } // check user if userID != "" { b.Where(builder.Eq{"question.user_id": userID}) ub.Where(builder.Eq{"answer.user_id": userID}) argsQ = append(argsQ, userID) argsA = append(argsA, userID) } // check vote if votes == 0 { b.Where(builder.Eq{"question.vote_count": votes}) ub.Where(builder.Eq{"answer.vote_count": votes}) argsQ = append(argsQ, votes) argsA = append(argsA, votes) } else if votes > 0 { b.Where(builder.Gte{"question.vote_count": votes}) ub.Where(builder.Gte{"answer.vote_count": votes}) argsQ = append(argsQ, votes) argsA = append(argsA, votes) } // b = b.Union("all", ub) ubSQL, _, err := ub.ToSQL() if err != nil { return } bSQL, _, err := b.ToSQL() if err != nil { return } sql := fmt.Sprintf("(%s UNION ALL %s)", bSQL, ubSQL) countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL() if err != nil { return } startNum := (page - 1) * pageSize querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL() if err != nil { return } queryArgs := []any{} countArgs := []any{} queryArgs = append(queryArgs, querySQL) queryArgs = append(queryArgs, argsQ...) queryArgs = append(queryArgs, argsA...) countArgs = append(countArgs, countSQL) countArgs = append(countArgs, argsQ...) countArgs = append(countArgs, argsA...) res, err := sr.data.DB.Context(ctx).Query(queryArgs...) if err != nil { return } tr, err := sr.data.DB.Context(ctx).Query(countArgs...) if len(tr) != 0 { total = converter.StringToInt64(string(tr[0]["total"])) } if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } else { resp, err = sr.parseResult(ctx, res, words) return } } // SearchQuestions search question data func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) { words = filterWords(words) var ( qfs = qFields args = []any{} ) if order == "relevance" { if len(words) > 0 { qfs, args = addRelevanceField([]string{"title", "original_text"}, words, qfs) } else { order = "newest" } } b := builder.MySQL().Select(qfs...).From("question") b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow}) args = append(args, entity.QuestionStatusDeleted, entity.QuestionShow) likeConQ := builder.NewCond() for _, word := range words { likeConQ = likeConQ.Or(builder.Like{"title", word}). Or(builder.Like{"original_text", word}) args = append(args, "%"+word+"%") args = append(args, "%"+word+"%") } b.Where(likeConQ) // check tag for ti, tagID := range tagIDs { ast := "tag_rel" + strconv.Itoa(ti) b.Join("INNER", "tag_rel as "+ast, "question.id = "+ast+".object_id"). And(builder.Eq{ ast + ".status": entity.TagRelStatusAvailable, }). And(builder.In(ast+".tag_id", tagID)) args = append(args, entity.TagRelStatusAvailable) for _, t := range tagID { args = append(args, t) } } // check need filter has not accepted if notAccepted { b.And(builder.Eq{"accepted_answer_id": 0}) args = append(args, 0) } // check views if views > -1 { b.And(builder.Gte{"view_count": views}) args = append(args, views) } // check answers if answers == 0 { b.And(builder.Eq{"answer_count": answers}) args = append(args, answers) } else if answers > 0 { b.And(builder.Gte{"answer_count": answers}) args = append(args, answers) } if answers == 0 { b.And(builder.Eq{"answer_count": 0}) args = append(args, 0) } else if answers > 0 { b.And(builder.Gte{"answer_count": answers}) args = append(args, answers) } queryArgs := []any{} countArgs := []any{} countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() if err != nil { return } startNum := (page - 1) * pageSize querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL() if err != nil { return } queryArgs = append(queryArgs, querySQL) queryArgs = append(queryArgs, args...) countArgs = append(countArgs, countSQL) countArgs = append(countArgs, args...) res, err := sr.data.DB.Context(ctx).Query(queryArgs...) if err != nil { return } tr, err := sr.data.DB.Context(ctx).Query(countArgs...) if err != nil { return } if len(tr) != 0 { total = converter.StringToInt64(string(tr[0]["total"])) } resp, err = sr.parseResult(ctx, res, words) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // SearchAnswers search answer data func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) { words = filterWords(words) var ( afs = aFields args = []any{} ) if order == "relevance" { if len(words) > 0 { afs, args = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs) } else { order = "newest" } } b := builder.MySQL().Select(afs...).From("`answer`"). LeftJoin("`question`", "`question`.id = `answer`.question_id") b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow}) args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow) likeConA := builder.NewCond() for _, word := range words { likeConA = likeConA.Or(builder.Like{"`answer`.original_text", word}) args = append(args, "%"+word+"%") } b.Where(likeConA) // check tag for ti, tagID := range tagIDs { ast := "tag_rel" + strconv.Itoa(ti) b.Join("INNER", "tag_rel as "+ast, "question_id = "+ast+".object_id"). And(builder.Eq{ ast + ".status": entity.TagRelStatusAvailable, }). And(builder.In(ast+".tag_id", tagID)) args = append(args, entity.TagRelStatusAvailable) for _, t := range tagID { args = append(args, t) } } // check limit accepted if accepted { b.Where(builder.Eq{"adopted": schema.AnswerAcceptedEnable}) args = append(args, schema.AnswerAcceptedEnable) } // check question id if questionID != "" { b.Where(builder.Eq{"question_id": questionID}) args = append(args, questionID) } queryArgs := []any{} countArgs := []any{} countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() if err != nil { return } startNum := (page - 1) * pageSize querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL() if err != nil { return } queryArgs = append(queryArgs, querySQL) queryArgs = append(queryArgs, args...) countArgs = append(countArgs, countSQL) countArgs = append(countArgs, args...) res, err := sr.data.DB.Context(ctx).Query(queryArgs...) if err != nil { return } tr, err := sr.data.DB.Context(ctx).Query(countArgs...) if err != nil { return } total = converter.StringToInt64(string(tr[0]["total"])) resp, err = sr.parseResult(ctx, res, words) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (sr *searchRepo) parseOrder(_ context.Context, order string) (res string) { switch order { case "newest": res = "created_at desc" case "active": res = "post_update_time desc" case "score": res = "vote_count desc" case "relevance": res = "relevance desc" default: res = "created_at desc" } return } // ParseSearchPluginResult parse search plugin result func (sr *searchRepo) ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult, words []string) (resp []*schema.SearchResult, err error) { var ( qres []map[string][]byte res = make([]map[string][]byte, 0) b *builder.Builder ) for _, r := range sres { switch r.Type { case "question": b = builder.MySQL().Select(qFields...).From("question").Where(builder.Eq{"id": r.ID}). And(builder.Lt{"`status`": entity.QuestionStatusDeleted}) case "answer": b = builder.MySQL().Select(aFields...).From("answer").LeftJoin("`question`", "`question`.`id` = `answer`.`question_id`"). Where(builder.Eq{"`answer`.`id`": r.ID}). And(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow}) } qres, err = sr.data.DB.Context(ctx).Query(b) if err != nil || len(qres) == 0 { continue } res = append(res, qres[0]) } return sr.parseResult(ctx, res, words) } // parseResult parse search result, return the data structure func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte, words []string) (resp []*schema.SearchResult, err error) { questionIDs := make([]string, 0) userIDs := make([]string, 0) resultList := make([]*schema.SearchResult, 0) for _, r := range res { questionIDs = append(questionIDs, string(r["question_id"])) userIDs = append(userIDs, string(r["user_id"])) tp, _ := time.ParseInLocation("2006-01-02 15:04:05", string(r["created_at"]), time.Local) var ID = string(r["id"]) var QuestionID = string(r["question_id"]) if handler.GetEnableShortID(ctx) { ID = uid.EnShortID(ID) QuestionID = uid.EnShortID(QuestionID) } object := &schema.SearchObject{ ID: ID, QuestionID: QuestionID, Title: string(r["title"]), UrlTitle: htmltext.UrlTitle(string(r["title"])), Excerpt: htmltext.FetchMatchedExcerpt(string(r["parsed_text"]), words, "...", 100), CreatedAtParsed: tp.Unix(), UserInfo: &schema.SearchObjectUser{ ID: string(r["user_id"]), }, Tags: make([]*schema.TagResp, 0), VoteCount: converter.StringToInt(string(r["vote_count"])), Accepted: string(r["accepted"]) == "2", AnswerCount: converter.StringToInt(string(r["answer_count"])), } objectKey, err := obj.GetObjectTypeStrByObjectID(string(r["id"])) if err != nil { continue } switch objectKey { case "question": for k, v := range entity.AdminQuestionSearchStatus { if v == converter.StringToInt(string(r["status"])) { object.StatusStr = k break } } case "answer": for k, v := range entity.AdminAnswerSearchStatus { if v == converter.StringToInt(string(r["status"])) { object.StatusStr = k break } } } resultList = append(resultList, &schema.SearchResult{ ObjectType: objectKey, Object: object, }) } tagsMap, err := sr.tagCommon.BatchGetObjectTag(ctx, questionIDs) if err != nil { return nil, err } userInfoMap, err := sr.userCommon.BatchUserBasicInfoByID(ctx, userIDs) if err != nil { return nil, err } for _, item := range resultList { tags, ok := tagsMap[item.Object.QuestionID] if ok { item.Object.Tags = tags } if userInfo := userInfoMap[item.Object.UserInfo.ID]; userInfo != nil { item.Object.UserInfo.Username = userInfo.Username item.Object.UserInfo.DisplayName = userInfo.DisplayName item.Object.UserInfo.Rank = userInfo.Rank item.Object.UserInfo.Status = userInfo.Status } } return resultList, nil } func addRelevanceField(searchFields, words, fields []string) (res []string, args []any) { relevanceRes := []string{} args = []any{} for _, searchField := range searchFields { var ( relevance = "(LENGTH(" + searchField + ") - LENGTH(%s))" replacement = "REPLACE(%s, ?, '')" replaceField = searchField replaced string argsField = []any{} ) res = fields for i, word := range words { if i == 0 { argsField = append(argsField, word) replaced = fmt.Sprintf(replacement, replaceField) } else { argsField = append(argsField, word) replaced = fmt.Sprintf(replacement, replaced) } } args = append(args, argsField...) relevance = fmt.Sprintf(relevance, replaced) relevanceRes = append(relevanceRes, relevance) } res = append(res, "("+strings.Join(relevanceRes, " + ")+") as relevance") return } func filterWords(words []string) (res []string) { for _, word := range words { if strings.TrimSpace(word) != "" { res = append(res, word) } } return } ================================================ FILE: internal/repo/search_sync/search_sync.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package search_sync import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/log" ) func NewPluginSyncer(data *data.Data) plugin.SearchSyncer { return &PluginSyncer{data: data} } type PluginSyncer struct { data *data.Data } func (p *PluginSyncer) GetAnswersPage(ctx context.Context, page, pageSize int) ( answerList []*plugin.SearchContent, err error) { answers := make([]*entity.Answer, 0) startNum := (page - 1) * pageSize err = p.data.DB.Context(ctx).Limit(pageSize, startNum).Find(&answers) if err != nil { return nil, err } return p.convertAnswers(ctx, answers) } func (p *PluginSyncer) GetQuestionsPage(ctx context.Context, page, pageSize int) ( questionList []*plugin.SearchContent, err error) { questions := make([]*entity.Question, 0) startNum := (page - 1) * pageSize err = p.data.DB.Context(ctx).Limit(pageSize, startNum).Find(&questions) if err != nil { return nil, err } return p.convertQuestions(ctx, questions) } func (p *PluginSyncer) convertAnswers(ctx context.Context, answers []*entity.Answer) ( answerList []*plugin.SearchContent, err error) { for _, answer := range answers { question := &entity.Question{} exist, err := p.data.DB.Context(ctx).Where("id = ?", answer.QuestionID).Get(question) if err != nil { log.Errorf("get question failed %s", err) continue } if !exist { continue } tagListList := make([]*entity.TagRel, 0) tags := make([]string, 0) err = p.data.DB.Context(ctx).Where("object_id = ?", uid.DeShortID(question.ID)). Where("status = ?", entity.TagRelStatusAvailable).Find(&tagListList) if err != nil { log.Errorf("get tag list failed %s", err) } for _, tag := range tagListList { tags = append(tags, tag.TagID) } content := &plugin.SearchContent{ ObjectID: answer.ID, Title: question.Title, Type: constant.AnswerObjectType, Content: answer.ParsedText, Answers: 0, Status: plugin.SearchContentStatus(answer.Status), Tags: tags, QuestionID: answer.QuestionID, UserID: answer.UserID, Views: int64(question.ViewCount), Created: answer.CreatedAt.Unix(), Active: answer.UpdatedAt.Unix(), Score: int64(answer.VoteCount), HasAccepted: answer.Accepted == schema.AnswerAcceptedEnable, } answerList = append(answerList, content) } return answerList, nil } func (p *PluginSyncer) convertQuestions(ctx context.Context, questions []*entity.Question) ( questionList []*plugin.SearchContent, err error) { for _, question := range questions { tagListList := make([]*entity.TagRel, 0) tags := make([]string, 0) err := p.data.DB.Context(ctx).Where("object_id = ?", question.ID). Where("status = ?", entity.TagRelStatusAvailable).Find(&tagListList) if err != nil { log.Errorf("get tag list failed %s", err) } for _, tag := range tagListList { tags = append(tags, tag.TagID) } content := &plugin.SearchContent{ ObjectID: question.ID, Title: question.Title, Type: constant.QuestionObjectType, Content: question.ParsedText, Answers: int64(question.AnswerCount), Status: plugin.SearchContentStatus(question.Status), Tags: tags, QuestionID: question.ID, UserID: question.UserID, Views: int64(question.ViewCount), Created: question.CreatedAt.Unix(), Active: question.UpdatedAt.Unix(), Score: int64(question.VoteCount), HasAccepted: question.AcceptedAnswerID != "" && question.AcceptedAnswerID != "0", } questionList = append(questionList, content) } return questionList, nil } ================================================ FILE: internal/repo/site_info/siteinfo_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package site_info import ( "context" "encoding/json" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "xorm.io/builder" ) type siteInfoRepo struct { data *data.Data } func NewSiteInfo(data *data.Data) siteinfo_common.SiteInfoRepo { return &siteInfoRepo{ data: data, } } // SaveByType save site setting by type func (sr *siteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error) { old := &entity.SiteInfo{} exist, err := sr.data.DB.Context(ctx).Where(builder.Eq{"type": siteType}).Get(old) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if exist { _, err = sr.data.DB.Context(ctx).ID(old.ID).Update(data) } else { _, err = sr.data.DB.Context(ctx).Insert(data) } if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } sr.setCache(ctx, siteType, data) return } // GetByType get site info by type func (sr *siteInfoRepo) GetByType(ctx context.Context, siteType string, withoutCache ...bool) (siteInfo *entity.SiteInfo, exist bool, err error) { if len(withoutCache) == 0 { siteInfo = sr.getCache(ctx, siteType) if siteInfo != nil { return siteInfo, true, nil } } siteInfo = &entity.SiteInfo{} exist, err = sr.data.DB.Context(ctx).Where(builder.Eq{"type": siteType}).Get(siteInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return nil, false, err } if exist { sr.setCache(ctx, siteType, siteInfo) } return } func (sr *siteInfoRepo) getCache(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo) { siteInfoCache, exist, err := sr.data.Cache.GetString(ctx, constant.SiteInfoCacheKey+siteType) if err != nil { return nil } if !exist { return nil } siteInfo = &entity.SiteInfo{} _ = json.Unmarshal([]byte(siteInfoCache), siteInfo) return siteInfo } func (sr *siteInfoRepo) setCache(ctx context.Context, siteType string, siteInfo *entity.SiteInfo) { siteInfoCache, _ := json.Marshal(siteInfo) err := sr.data.Cache.SetString(ctx, constant.SiteInfoCacheKey+siteType, string(siteInfoCache), constant.SiteInfoCacheTime) if err != nil { log.Error(err) } } func (sr *siteInfoRepo) IsBrandingFileUsed(ctx context.Context, filePath string) (bool, error) { siteInfo := &entity.SiteInfo{} count, err := sr.data.DB.Context(ctx). Table("site_info"). Where(builder.Eq{"type": "branding"}). And(builder.Like{"content", "%" + filePath + "%"}). Count(&siteInfo) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count > 0, nil } ================================================ FILE: internal/repo/tag/tag_rel_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package tag import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" tagcommon "github.com/apache/answer/internal/service/tag_common" "github.com/apache/answer/internal/service/unique" "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) // tagRelRepo tag rel repository type tagRelRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } // NewTagRelRepo new repository func NewTagRelRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) tagcommon.TagRelRepo { return &tagRelRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } // AddTagRelList add tag list func (tr *tagRelRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) { for _, item := range tagList { item.ObjectID = uid.DeShortID(item.ObjectID) } _, err = tr.data.DB.Context(ctx).Insert(tagList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range tagList { item.ObjectID = uid.EnShortID(item.ObjectID) } } return } // RemoveTagRelListByObjectID delete tag list func (tr *tagRelRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) { objectID = uid.DeShortID(objectID) _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // RecoverTagRelListByObjectID recover tag list func (tr *tagRelRepo) RecoverTagRelListByObjectID(ctx context.Context, objectID string) (err error) { objectID = uid.DeShortID(objectID) _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) { objectID = uid.DeShortID(objectID) _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.TagRelStatusAvailable).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (tr *tagRelRepo) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) { objectID = uid.DeShortID(objectID) _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.TagRelStatusHide).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // RemoveTagRelListByIDs delete tag list func (tr *tagRelRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) { _, err = tr.data.DB.Context(ctx).In("id", ids).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetObjectTagRelWithoutStatus get object tag relation no matter status func (tr *tagRelRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectID, tagID string) ( tagRel *entity.TagRel, exist bool, err error, ) { objectID = uid.DeShortID(objectID) tagRel = &entity.TagRel{} session := tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("tag_id = ?", tagID) exist, err = session.Get(tagRel) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } if handler.GetEnableShortID(ctx) { tagRel.ObjectID = uid.EnShortID(tagRel.ObjectID) } return } // EnableTagRelByIDs update tag status to available func (tr *tagRelRepo) EnableTagRelByIDs(ctx context.Context, ids []int64, hide bool) (err error) { status := entity.TagRelStatusAvailable if hide { status = entity.TagRelStatusHide } _, err = tr.data.DB.Context(ctx).In("id", ids).Update(&entity.TagRel{Status: status}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetObjectTagRelList get object tag relation list all func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string) (tagListList []*entity.TagRel, err error) { objectID = uid.DeShortID(objectID) tagListList = make([]*entity.TagRel, 0) session := tr.data.DB.Context(ctx).Where("object_id = ?", objectID) session.In("status", []int{entity.TagRelStatusAvailable, entity.TagRelStatusHide}) err = session.Find(&tagListList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } if handler.GetEnableShortID(ctx) { for _, item := range tagListList { item.ObjectID = uid.EnShortID(item.ObjectID) } } return } // BatchGetObjectTagRelList get object tag relation list all func (tr *tagRelRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error) { for num, item := range objectIds { objectIds[num] = uid.DeShortID(item) } tagListList = make([]*entity.TagRel, 0) session := tr.data.DB.Context(ctx).In("object_id", objectIds) session.Where("status = ?", entity.TagRelStatusAvailable) err = session.Find(&tagListList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } if handler.GetEnableShortID(ctx) { for _, item := range tagListList { item.ObjectID = uid.EnShortID(item.ObjectID) } } return } // CountTagRelByTagID count tag relation func (tr *tagRelRepo) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) { count, err = tr.data.DB.Context(ctx).Count(&entity.TagRel{TagID: tagID, Status: entity.AnswerStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetTagRelDefaultStatusByObjectID get tag rel default status func (tr *tagRelRepo) GetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) (status int, err error) { question := entity.Question{} exist, err := tr.data.DB.Context(ctx).ID(objectID).Cols("show", "status").Get(&question) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } if exist && (question.Show == entity.QuestionHide || question.Status == entity.QuestionStatusDeleted) { return entity.TagRelStatusHide, nil } return entity.TagRelStatusAvailable, nil } // MigrateTagObjects migrate tag objects func (tr *tagRelRepo) MigrateTagObjects(ctx context.Context, sourceTagId, targetTagId string) error { _, err := tr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { // 1. Get all objects related to source tag var sourceObjects []entity.TagRel err = session.Where("tag_id = ?", sourceTagId).Find(&sourceObjects) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // 2. Get existing target tag relations var existingTargets []entity.TagRel err = session.Where("tag_id = ?", targetTagId).Find(&existingTargets) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // Create map of existing target objects for quick lookup existingMap := make(map[string]bool) for _, target := range existingTargets { existingMap[target.ObjectID] = true } // 3. Create new relations for objects not already tagged with target newRelations := make([]*entity.TagRel, 0) for _, source := range sourceObjects { if !existingMap[source.ObjectID] { newRelations = append(newRelations, &entity.TagRel{ TagID: targetTagId, ObjectID: source.ObjectID, Status: source.Status, }) } } if len(newRelations) > 0 { _, err = session.Insert(newRelations) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } } // 4. Remove old relations _, err = session.Where("tag_id = ?", sourceTagId).Delete(&entity.TagRel{}) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil, nil }) return err } ================================================ FILE: internal/repo/tag/tag_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package tag import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/tag_common" "github.com/apache/answer/internal/service/unique" "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" "xorm.io/builder" ) // tagRepo tag repository type tagRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } // NewTagRepo new repository func NewTagRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, ) tag_common.TagRepo { return &tagRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } // RemoveTag delete tag func (tr *tagRepo) RemoveTag(ctx context.Context, tagID string) (err error) { session := tr.data.DB.Context(ctx).Where(builder.Eq{"id": tagID}) _, err = session.Update(&entity.Tag{Status: entity.TagStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateTag update tag func (tr *tagRepo) UpdateTag(ctx context.Context, tag *entity.Tag) (err error) { _, err = tr.data.DB.Context(ctx).Where(builder.Eq{"id": tag.ID}).Update(tag) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // RecoverTag recover deleted tag func (tr *tagRepo) RecoverTag(ctx context.Context, tagID string) (err error) { _, err = tr.data.DB.Context(ctx).ID(tagID).Update(&entity.Tag{Status: entity.TagStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // MustGetTagByNameOrID get tag by name or id func (tr *tagRepo) MustGetTagByNameOrID(ctx context.Context, tagID, slugName string) ( tag *entity.Tag, exist bool, err error) { if len(tagID) == 0 && len(slugName) == 0 { return nil, false, nil } tag = &entity.Tag{} session := tr.data.DB.Context(ctx) if len(tagID) > 0 { session.ID(tagID) } if len(slugName) > 0 { session.Where(builder.Eq{"slug_name": slugName}) } exist, err = session.Get(tag) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateTagSynonym update synonym tag func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string, ) (err error) { bean := &entity.Tag{MainTagID: mainTagID, MainTagSlugName: mainTagSlugName} session := tr.data.DB.Context(ctx).In("slug_name", tagSlugNameList).MustCols("main_tag_id", "main_tag_slug_name") _, err = session.Update(bean) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (tr *tagRepo) GetTagSynonymCount(ctx context.Context, tagID string) (count int64, err error) { count, err = tr.data.DB.Context(ctx).Count(&entity.Tag{MainTagID: converter.StringToInt64(tagID), Status: entity.TagStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (tr *tagRepo) GetIDsByMainTagId(ctx context.Context, mainTagID string) (tagIDs []string, err error) { session := tr.data.DB.Context(ctx).Table(entity.Tag{}.TableName()).Where(builder.Eq{"status": entity.TagStatusAvailable, "main_tag_id": converter.StringToInt64(mainTagID)}).Cols("id") err = session.Find(&tagIDs) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetTagList get tag list all func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) { tagList = make([]*entity.Tag, 0) session := tr.data.DB.Context(ctx).Where(builder.Eq{"status": entity.TagStatusAvailable}) err = session.Find(&tagList, tag) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/tag_common/tag_common_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package tag_common import ( "context" "fmt" "strconv" "strings" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" tagcommon "github.com/apache/answer/internal/service/tag_common" "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" "xorm.io/builder" ) // tagCommonRepo tag repository type tagCommonRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo } // NewTagCommonRepo new repository func NewTagCommonRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, ) tagcommon.TagCommonRepo { return &tagCommonRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } // GetTagListByIDs get tag list all func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) { tagList = make([]*entity.Tag, 0) session := tr.data.DB.Context(ctx).In("id", ids) session.Where(builder.Eq{"status": entity.TagStatusAvailable}) err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetTagBySlugName get tag by slug name func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) { tagInfo = &entity.Tag{} session := tr.data.DB.Context(ctx).Where("LOWER(slug_name) = ?", slugName) session.Where(builder.Eq{"status": entity.TagStatusAvailable}) exist, err = session.Get(tagInfo) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetTagListByName get tag list all like name func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, recommend, reserved bool) (tagList []*entity.Tag, err error) { cond := &entity.Tag{} session := tr.data.DB.Context(ctx) if len(name) > 0 { session.Where("slug_name LIKE ? OR display_name LIKE ?", strings.ToLower(name)+"%", name+"%") } var columns []string if recommend { columns = append(columns, "recommend") cond.Recommend = true } if reserved { columns = append(columns, "reserved") cond.Reserved = true } if len(columns) > 0 { session.UseBool(columns...) } session.Where(builder.Eq{"status": entity.TagStatusAvailable}) tagList = make([]*entity.Tag, 0) err = session.OrderBy("recommend DESC,reserved DESC,slug_name ASC").Find(&tagList, cond) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (tr *tagCommonRepo) GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error) { tagList = make([]*entity.Tag, 0) cond := &entity.Tag{} session := tr.data.DB.Context(ctx).Where("") cond.Recommend = true // session.Where(builder.Eq{"status": entity.TagStatusAvailable}) session.Asc("slug_name") session.UseBool("recommend") err = session.Find(&tagList, cond) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error) { tagList = make([]*entity.Tag, 0) cond := &entity.Tag{} session := tr.data.DB.Context(ctx).Where("") cond.Reserved = true // session.Where(builder.Eq{"status": entity.TagStatusAvailable}) session.Asc("slug_name") session.UseBool("reserved") err = session.Find(&tagList, cond) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetTagListByNames get tag list all like name func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) { tagList = make([]*entity.Tag, 0) session := tr.data.DB.Context(ctx).In("slug_name", names).UseBool("recommend", "reserved") session.Where(builder.Eq{"status": entity.TagStatusAvailable}) err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetTagByID get tag one func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string, includeDeleted bool) ( tag *entity.Tag, exist bool, err error, ) { tag = &entity.Tag{} session := tr.data.DB.Context(ctx).Where(builder.Eq{"id": tagID}) if !includeDeleted { session.Where(builder.Eq{"status": entity.TagStatusAvailable}) } exist, err = session.Get(tag) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetTagPage get tag page func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) ( tagList []*entity.Tag, total int64, err error, ) { tagList = make([]*entity.Tag, 0) session := tr.data.DB.Context(ctx) if len(tag.SlugName) > 0 { mainTagCond := builder.And( builder.Or( builder.Like{"slug_name", fmt.Sprintf("LOWER(%s)", tag.SlugName)}, builder.Like{"display_name", tag.SlugName}, ), builder.Eq{"main_tag_id": 0}, ) synonymCond := builder.And( builder.Eq{"slug_name": tag.SlugName}, builder.Neq{"main_tag_id": 0}, ) session.Where(builder.Or(mainTagCond, synonymCond)) tag.SlugName = "" } else { session.Where(builder.Eq{"main_tag_id": 0}) } session.Where(builder.Eq{"status": entity.TagStatusAvailable}) switch queryCond { case "popular": session.Desc("question_count") case "name": session.Asc("slug_name") case "newest": session.Desc("created_at") } total, err = pager.Help(page, pageSize, &tagList, tag, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } for i := 0; i < len(tagList); i++ { if tagList[i].MainTagID != 0 { mainTag, exist, errSynonym := tr.GetTagByID(ctx, strconv.FormatInt(tagList[i].MainTagID, 10), false) if errSynonym != nil { err = errors.InternalServer(reason.DatabaseError).WithError(errSynonym).WithStack() return } if exist { tagList[i] = mainTag } } } return } // AddTagList add tag func (tr *tagCommonRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) { addTags := make([]*entity.Tag, 0) for _, item := range tagList { exist, err := tr.updateDeletedTag(ctx, item) if err != nil { return err } if exist { continue } addTags = append(addTags, item) item.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName()) if err != nil { return err } item.RevisionID = "0" } if len(addTags) == 0 { return nil } _, err = tr.data.DB.Context(ctx).Insert(addTags) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (tr *tagCommonRepo) updateDeletedTag(ctx context.Context, tag *entity.Tag) (exist bool, err error) { old := &entity.Tag{SlugName: tag.SlugName} exist, err = tr.data.DB.Context(ctx).Get(old) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist || old.Status != entity.TagStatusDeleted { return false, nil } tag.ID = old.ID tag.Status = entity.TagStatusAvailable tag.RevisionID = "0" if _, err = tr.data.DB.Context(ctx).ID(tag.ID).Update(tag); err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return true, nil } // UpdateTagQuestionCount update tag question count func (tr *tagCommonRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) { cond := &entity.Tag{QuestionCount: questionCount} _, err = tr.data.DB.Context(ctx).Where(builder.Eq{"id": tagID}).MustCols("question_count").Update(cond) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (tr *tagCommonRepo) UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error) { bean := &entity.Tag{} switch attribute { case "recommend": bean.Recommend = value case "reserved": bean.Reserved = value default: return } session := tr.data.DB.Context(ctx).In("slug_name", tags).Cols(attribute).UseBool(attribute) _, err = session.Update(bean) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } ================================================ FILE: internal/repo/unique/uniqid_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package unique import ( "context" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" ) // uniqueIDRepo Unique id repository type uniqueIDRepo struct { data *data.Data } // NewUniqueIDRepo new repository func NewUniqueIDRepo(data *data.Data) unique.UniqueIDRepo { return &uniqueIDRepo{ data: data, } } // GenUniqueIDStr generate unique id string // 1 + 00x(objectType) + 000000000000x(id) func (ur *uniqueIDRepo) GenUniqueIDStr(ctx context.Context, key string) (uniqueID string, err error) { objectType := constant.ObjectTypeStrMapping[key] bean := &entity.Uniqid{UniqidType: objectType} _, err = ur.data.DB.Context(ctx).Insert(bean) if err != nil { return "", errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return fmt.Sprintf("1%03d%013d", objectType, bean.ID), nil } ================================================ FILE: internal/repo/user/user_backyard_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package user import ( "context" "encoding/json" "time" "xorm.io/builder" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/internal/service/user_admin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // userAdminRepo user repository type userAdminRepo struct { data *data.Data authRepo auth.AuthRepo } // NewUserAdminRepo new repository func NewUserAdminRepo(data *data.Data, authRepo auth.AuthRepo) user_admin.UserAdminRepo { return &userAdminRepo{ data: data, authRepo: authRepo, } } // UpdateUserStatus update user status func (ur *userAdminRepo) UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, email string, suspendedUntil time.Time, ) (err error) { cond := &entity.User{Status: userStatus, MailStatus: mailStatus, EMail: email} switch userStatus { case entity.UserStatusSuspended: cond.SuspendedAt = time.Now() cond.SuspendedUntil = suspendedUntil case entity.UserStatusDeleted: cond.DeletedAt = time.Now() case entity.UserStatusAvailable: // When restoring user status, clear suspended until time to zero cond.SuspendedUntil = time.Time{} } _, err = ur.data.DB.Context(ctx).ID(userID).MustCols("status", "mail_status", "e_mail", "suspended_at", "suspended_until", "deleted_at").Update(cond) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } userCacheInfo := &entity.UserCacheInfo{ UserID: userID, EmailStatus: mailStatus, UserStatus: userStatus, } t, _ := json.Marshal(userCacheInfo) log.Infof("user change status: %s", string(t)) err = ur.authRepo.SetUserStatus(ctx, userID, userCacheInfo) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // AddUser add user func (ur *userAdminRepo) AddUser(ctx context.Context, user *entity.User) (err error) { _, err = ur.data.DB.Context(ctx).Insert(user) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // AddUsers add users func (ur *userAdminRepo) AddUsers(ctx context.Context, users []*entity.User) (err error) { _, err = ur.data.DB.Context(ctx).Insert(users) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateUserPassword update user password func (ur *userAdminRepo) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) { _, err = ur.data.DB.Context(ctx).ID(userID).Update(&entity.User{Pass: password}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetUserInfo get user info func (ur *userAdminRepo) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) { user = &entity.User{} exist, err = ur.data.DB.Context(ctx).ID(userID).Get(user) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return } err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user) if err != nil { return nil, false, err } return } // GetUserInfoByEmail get user info func (ur *userAdminRepo) GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) { userInfo := &entity.User{} exist, err = ur.data.DB.Context(ctx).Where("e_mail = ?", email). Where("status != ?", entity.UserStatusDeleted).Get(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } if !exist { return } err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user) if err != nil { return nil, false, err } return } // GetUserPage get user page func (ur *userAdminRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User, usernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error) { users = make([]*entity.User, 0) session := ur.data.DB.Context(ctx) switch user.Status { case entity.UserStatusDeleted: session.Desc("`user`.deleted_at") case entity.UserStatusSuspended: session.Desc("`user`.suspended_at") default: session.Desc("`user`.created_at") } if len(usernameOrDisplayName) > 0 { session.And(builder.Or( builder.Like{"`user`.username", usernameOrDisplayName}, builder.Like{"`user`.display_name", usernameOrDisplayName}, )) } if isStaff { session.Join("INNER", "user_role_rel", "`user`.id = `user_role_rel`.user_id AND `user_role_rel`.role_id > 1") } total, err = pager.Help(page, pageSize, &users, user, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } tryToDecorateUserListFromUserCenter(ctx, ur.data, users) return } // DeletePermanentlyUsers delete permanently users func (ur *userAdminRepo) DeletePermanentlyUsers(ctx context.Context) (err error) { _, err = ur.data.DB.Context(ctx).Where("status = ?", entity.UserStatusDeleted).Delete(&entity.User{}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetExpiredSuspendedUsers gets all suspended users whose suspension has expired func (ur *userAdminRepo) GetExpiredSuspendedUsers(ctx context.Context) (users []*entity.User, err error) { users = make([]*entity.User, 0) now := time.Now() err = ur.data.DB.Context(ctx). Where("status = ?", entity.UserStatusSuspended). Where("suspended_until < ?", now). Find(&users) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return users, nil } ================================================ FILE: internal/repo/user/user_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package user import ( "context" "strings" "time" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "xorm.io/builder" "xorm.io/xorm" ) // userRepo user repository type userRepo struct { data *data.Data } // NewUserRepo new repository func NewUserRepo(data *data.Data) usercommon.UserRepo { return &userRepo{ data: data, } } // AddUser add user func (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) { _, err = ur.data.DB.Transaction(func(session *xorm.Session) (any, error) { session = session.Context(ctx) userInfo := &entity.User{} exist, err := session.Where("username = ?", user.Username).Get(userInfo) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if exist { return nil, errors.InternalServer(reason.UsernameDuplicate) } _, err = session.Insert(user) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil, nil }) return } // IncreaseAnswerCount increase answer count func (ur *userRepo) IncreaseAnswerCount(ctx context.Context, userID string, amount int) (err error) { user := &entity.User{} _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Incr("answer_count", amount).Update(user) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // IncreaseQuestionCount increase question count func (ur *userRepo) IncreaseQuestionCount(ctx context.Context, userID string, amount int) (err error) { user := &entity.User{} _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Incr("question_count", amount).Update(user) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } func (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) { user := &entity.User{} user.QuestionCount = int(count) _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("question_count").Update(user) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } func (ur *userRepo) UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) { user := &entity.User{} user.AnswerCount = count _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("answer_count").Update(user) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // UpdateLastLoginDate update last login date func (ur *userRepo) UpdateLastLoginDate(ctx context.Context, userID string) (err error) { user := &entity.User{LastLoginDate: time.Now()} _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("last_login_date").Update(user) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // UpdateEmailStatus update email status func (ur *userRepo) UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error { cond := &entity.User{MailStatus: emailStatus} _, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("mail_status").Update(cond) if err != nil { return err } return nil } // UpdateNoticeStatus update notice status func (ur *userRepo) UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error { cond := &entity.User{NoticeStatus: noticeStatus} _, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("notice_status").Update(cond) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } func (ur *userRepo) UpdatePass(ctx context.Context, userID, pass string) error { _, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("pass").Update(&entity.User{Pass: pass}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err error) { _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Update(&entity.User{EMail: email}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (ur *userRepo) UpdateUserInterface(ctx context.Context, userID, language, colorSchema string) (err error) { session := ur.data.DB.Context(ctx).Where("id = ?", userID) _, err = session.Cols("language", "color_scheme").Update(&entity.User{Language: language, ColorScheme: colorSchema}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateInfo update user info func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) { _, err = ur.data.DB.Context(ctx).Where("id = ?", userInfo.ID). Cols("username", "display_name", "avatar", "bio", "bio_html", "website", "location").Update(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateUserProfile update user profile func (ur *userRepo) UpdateUserProfile(ctx context.Context, userInfo *entity.User) (err error) { _, err = ur.data.DB.Context(ctx).Where("id = ?", userInfo.ID). Cols("username", "e_mail", "mail_status", "display_name").Update(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetByUserID get user info by user id func (ur *userRepo) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) { userInfo = &entity.User{} exist, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Get(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo) if err != nil { return nil, false, err } return } func (ur *userRepo) BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) { list := make([]*entity.User, 0) err := ur.data.DB.Context(ctx).In("id", ids).Find(&list) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } tryToDecorateUserListFromUserCenter(ctx, ur.data, list) return list, nil } // GetByUsername get user by username func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) { userInfo = &entity.User{} exist, err = ur.data.DB.Context(ctx).Where("username = ?", username).Get(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo) if err != nil { return nil, false, err } return } func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) { list := make([]*entity.User, 0) err := ur.data.DB.Context(ctx).Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return list, err } tryToDecorateUserListFromUserCenter(ctx, ur.data, list) return list, nil } // GetByEmail get user by email func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) { userInfo = &entity.User{} exist, err = ur.data.DB.Context(ctx).Where("e_mail = ?", email). Where("status != ?", entity.UserStatusDeleted).Get(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) { session := ur.data.DB.Context(ctx) session.Where("status = ? OR status = ?", entity.UserStatusAvailable, entity.UserStatusSuspended) count, err = session.Count(&entity.User{}) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } func (ur *userRepo) SearchUserListByName(ctx context.Context, name string, limit int, onlyStaff bool) (userList []*entity.User, err error) { userList = make([]*entity.User, 0) session := ur.data.DB.Context(ctx) if onlyStaff { session.Join("INNER", "user_role_rel", "`user`.id = `user_role_rel`.user_id AND `user_role_rel`.role_id > 1") } session.Where("status = ?", entity.UserStatusAvailable) session.Where("username LIKE ? OR display_name LIKE ?", strings.ToLower(name)+"%", name+"%") session.OrderBy("username ASC, `user`.id DESC") session.Limit(limit) err = session.Find(&userList) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } tryToDecorateUserListFromUserCenter(ctx, ur.data, userList) return } func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) { if original == nil { return nil } uc, ok := plugin.GetUserCenter() if !ok { return nil } userInfo := &entity.UserExternalLogin{} session := data.DB.Context(ctx).Where("user_id = ?", original.ID) session.Where("provider = ?", uc.Info().SlugName) exist, err := session.Get(userInfo) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return nil } userCenterBasicUserInfo, err := uc.UserInfo(userInfo.ExternalID) if err != nil { log.Error(err) return errors.BadRequest(reason.UserNotFound).WithError(err).WithStack() } decorateByUserCenterUser(original, userCenterBasicUserInfo) return nil } func tryToDecorateUserListFromUserCenter(ctx context.Context, data *data.Data, original []*entity.User) { uc, ok := plugin.GetUserCenter() if !ok { return } ids := make([]string, 0) originalUserIDMapping := make(map[string]*entity.User, 0) for _, user := range original { originalUserIDMapping[user.ID] = user ids = append(ids, user.ID) } userExternalLoginList := make([]*entity.UserExternalLogin, 0) session := data.DB.Context(ctx).Where("provider = ?", uc.Info().SlugName) session.In("user_id", ids) err := session.Find(&userExternalLoginList) if err != nil { log.Error(err) return } userExternalIDs := make([]string, 0) originalExternalIDMapping := make(map[string]*entity.User, 0) for _, u := range userExternalLoginList { originalExternalIDMapping[u.ExternalID] = originalUserIDMapping[u.UserID] userExternalIDs = append(userExternalIDs, u.ExternalID) } if len(userExternalIDs) == 0 { return } ucUsers, err := uc.UserList(userExternalIDs) if err != nil { log.Errorf("get user list from user center failed: %v, %v", err, userExternalIDs) return } for _, ucUser := range ucUsers { decorateByUserCenterUser(originalExternalIDMapping[ucUser.ExternalID], ucUser) } } func decorateByUserCenterUser(original *entity.User, ucUser *plugin.UserCenterBasicUserInfo) { if original == nil || ucUser == nil { return } // In general, usernames should be guaranteed unique by the User Center plugin, so there are no inconsistencies. if original.Username != ucUser.Username { log.Warnf("user %s username is inconsistent with user center", original.ID) } if len(ucUser.DisplayName) > 0 { original.DisplayName = ucUser.DisplayName } if len(ucUser.Email) > 0 { original.EMail = ucUser.Email } if len(ucUser.Avatar) > 0 { original.Avatar = schema.CustomAvatar(ucUser.Avatar).ToJsonString() } if len(ucUser.Mobile) > 0 { original.Mobile = ucUser.Mobile } if len(ucUser.Bio) > 0 { original.BioHTML = converter.Markdown2HTML(ucUser.Bio) + original.BioHTML } // If plugin enable rank agent, use rank from user center. if plugin.RankAgentEnabled() { original.Rank = ucUser.Rank } if ucUser.Status != plugin.UserStatusAvailable { original.Status = int(ucUser.Status) } } func (ur *userRepo) IsAvatarFileUsed(ctx context.Context, filePath string) (bool, error) { user := &entity.User{} count, err := ur.data.DB.Context(ctx). Table("user"). Where(builder.Like{"avatar", "%" + filePath + "%"}). Count(&user) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count > 0, nil } ================================================ FILE: internal/repo/user_external_login/user_external_login_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package user_external_login import ( "context" "encoding/json" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/user_external_login" "github.com/segmentfault/pacman/errors" ) type userExternalLoginRepo struct { data *data.Data } // NewUserExternalLoginRepo new repository func NewUserExternalLoginRepo(data *data.Data) user_external_login.UserExternalLoginRepo { return &userExternalLoginRepo{ data: data, } } // AddUserExternalLogin add external login information func (ur *userExternalLoginRepo) AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error) { _, err = ur.data.DB.Context(ctx).Insert(user) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateInfo update user info func (ur *userExternalLoginRepo) UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error) { _, err = ur.data.DB.Context(ctx).ID(userInfo.ID).Update(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetByExternalID get by external ID func (ur *userExternalLoginRepo) GetByExternalID(ctx context.Context, provider, externalID string) ( userInfo *entity.UserExternalLogin, exist bool, err error) { userInfo = &entity.UserExternalLogin{} exist, err = ur.data.DB.Context(ctx).Where("external_id = ?", externalID).Where("provider = ?", provider).Get(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetByUserID get by user ID func (ur *userExternalLoginRepo) GetByUserID(ctx context.Context, provider, userID string) ( userInfo *entity.UserExternalLogin, exist bool, err error) { userInfo = &entity.UserExternalLogin{} exist, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Where("provider = ?", provider).Get(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetUserExternalLoginList get by external ID func (ur *userExternalLoginRepo) GetUserExternalLoginList(ctx context.Context, userID string) ( resp []*entity.UserExternalLogin, err error) { resp = make([]*entity.UserExternalLogin, 0) err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).OrderBy("updated_at DESC").Find(&resp) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // DeleteUserExternalLogin delete external user login info func (ur *userExternalLoginRepo) DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error) { cond := &entity.UserExternalLogin{} _, err = ur.data.DB.Context(ctx).Where("user_id = ? AND external_id = ?", userID, externalID).Delete(cond) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // DeleteUserExternalLoginByUserID delete external user login info by user ID func (ur *userExternalLoginRepo) DeleteUserExternalLoginByUserID(ctx context.Context, userID string) (err error) { cond := &entity.UserExternalLogin{} _, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(cond) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // SetCacheUserExternalLoginInfo cache user info for external login func (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo( ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) { cacheData, _ := json.Marshal(info) return ur.data.Cache.SetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key, string(cacheData), constant.ConnectorUserExternalInfoCacheTime) } // GetCacheUserExternalLoginInfo cache user info for external login func (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo( ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) { res, exist, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key) if err != nil { return info, err } if !exist { return nil, nil } info = &schema.ExternalLoginUserInfoCache{} _ = json.Unmarshal([]byte(res), &info) return info, nil } ================================================ FILE: internal/repo/user_notification_config/user_notification_config_repo.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package user_notification_config import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/user_notification_config" "github.com/segmentfault/pacman/errors" ) // userNotificationConfigRepo notification repository type userNotificationConfigRepo struct { data *data.Data } // NewUserNotificationConfigRepo new repository func NewUserNotificationConfigRepo(data *data.Data) user_notification_config.UserNotificationConfigRepo { return &userNotificationConfigRepo{ data: data, } } // Add add notification config func (ur *userNotificationConfigRepo) Add(ctx context.Context, userIDs []string, source, channels string) (err error) { var configs []*entity.UserNotificationConfig for _, userID := range userIDs { configs = append(configs, &entity.UserNotificationConfig{ UserID: userID, Source: source, Channels: channels, Enabled: true, }) } _, err = ur.data.DB.Context(ctx).Insert(configs) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // Save save notification config, if existed, update, if not exist, insert func (ur *userNotificationConfigRepo) Save(ctx context.Context, uc *entity.UserNotificationConfig) (err error) { old := &entity.UserNotificationConfig{UserID: uc.UserID, Source: uc.Source} exist, err := ur.data.DB.Context(ctx).Get(old) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if exist { old.Channels = uc.Channels old.Enabled = uc.Enabled _, err = ur.data.DB.Context(ctx).ID(old.ID).UseBool("enabled").Cols("channels", "enabled").Update(old) } else { _, err = ur.data.DB.Context(ctx).Insert(uc) } if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // GetByUserID get notification config by user id func (ur *userNotificationConfigRepo) GetByUserID(ctx context.Context, userID string) ( []*entity.UserNotificationConfig, error) { var configs []*entity.UserNotificationConfig err := ur.data.DB.Context(ctx).Where("user_id = ?", userID).Find(&configs) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return configs, nil } // GetBySource get notification config by source func (ur *userNotificationConfigRepo) GetBySource(ctx context.Context, source constant.NotificationSource) ( []*entity.UserNotificationConfig, error) { var configs []*entity.UserNotificationConfig err := ur.data.DB.Context(ctx).UseBool("enabled"). Find(&configs, &entity.UserNotificationConfig{Source: string(source), Enabled: true}) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return configs, nil } // GetByUserIDAndSource get notification config by user id and source func (ur *userNotificationConfigRepo) GetByUserIDAndSource(ctx context.Context, userID string, source constant.NotificationSource) ( conf *entity.UserNotificationConfig, exist bool, err error) { config := &entity.UserNotificationConfig{UserID: userID, Source: string(source)} exist, err = ur.data.DB.Context(ctx).Get(config) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return config, exist, nil } // GetByUsersAndSource get notification config by user ids and source func (ur *userNotificationConfigRepo) GetByUsersAndSource( ctx context.Context, userIDs []string, source constant.NotificationSource) ( []*entity.UserNotificationConfig, error) { var configs []*entity.UserNotificationConfig err := ur.data.DB.Context(ctx).UseBool("enabled").In("user_id", userIDs). Find(&configs, &entity.UserNotificationConfig{Source: string(source), Enabled: true}) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return configs, nil } ================================================ FILE: internal/router/answer_api_router.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package router import ( "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/controller" "github.com/apache/answer/internal/controller_admin" "github.com/gin-gonic/gin" ) type AnswerAPIRouter struct { langController *controller.LangController userController *controller.UserController commentController *controller.CommentController reportController *controller.ReportController voteController *controller.VoteController tagController *controller.TagController followController *controller.FollowController collectionController *controller.CollectionController questionController *controller.QuestionController answerController *controller.AnswerController searchController *controller.SearchController revisionController *controller.RevisionController rankController *controller.RankController adminUserController *controller_admin.UserAdminController reasonController *controller.ReasonController themeController *controller_admin.ThemeController adminSiteInfoController *controller_admin.SiteInfoController siteInfoController *controller.SiteInfoController notificationController *controller.NotificationController dashboardController *controller.DashboardController uploadController *controller.UploadController activityController *controller.ActivityController roleController *controller_admin.RoleController pluginController *controller_admin.PluginController permissionController *controller.PermissionController userPluginController *controller.UserPluginController reviewController *controller.ReviewController metaController *controller.MetaController badgeController *controller.BadgeController adminBadgeController *controller_admin.BadgeController apiKeyController *controller_admin.AdminAPIKeyController aiController *controller.AIController aiConversationController *controller.AIConversationController aiConversationAdminController *controller_admin.AIConversationAdminController mcpController *controller.MCPController } func NewAnswerAPIRouter( langController *controller.LangController, userController *controller.UserController, commentController *controller.CommentController, reportController *controller.ReportController, voteController *controller.VoteController, tagController *controller.TagController, followController *controller.FollowController, collectionController *controller.CollectionController, questionController *controller.QuestionController, answerController *controller.AnswerController, searchController *controller.SearchController, revisionController *controller.RevisionController, rankController *controller.RankController, adminUserController *controller_admin.UserAdminController, reasonController *controller.ReasonController, themeController *controller_admin.ThemeController, adminSiteInfoController *controller_admin.SiteInfoController, siteInfoController *controller.SiteInfoController, notificationController *controller.NotificationController, dashboardController *controller.DashboardController, uploadController *controller.UploadController, activityController *controller.ActivityController, roleController *controller_admin.RoleController, pluginController *controller_admin.PluginController, permissionController *controller.PermissionController, userPluginController *controller.UserPluginController, reviewController *controller.ReviewController, metaController *controller.MetaController, badgeController *controller.BadgeController, adminBadgeController *controller_admin.BadgeController, apiKeyController *controller_admin.AdminAPIKeyController, aiController *controller.AIController, aiConversationController *controller.AIConversationController, aiConversationAdminController *controller_admin.AIConversationAdminController, mcpController *controller.MCPController, ) *AnswerAPIRouter { return &AnswerAPIRouter{ langController: langController, userController: userController, commentController: commentController, reportController: reportController, voteController: voteController, tagController: tagController, followController: followController, collectionController: collectionController, questionController: questionController, answerController: answerController, searchController: searchController, revisionController: revisionController, rankController: rankController, adminUserController: adminUserController, reasonController: reasonController, themeController: themeController, adminSiteInfoController: adminSiteInfoController, notificationController: notificationController, siteInfoController: siteInfoController, dashboardController: dashboardController, uploadController: uploadController, activityController: activityController, roleController: roleController, pluginController: pluginController, permissionController: permissionController, userPluginController: userPluginController, reviewController: reviewController, metaController: metaController, badgeController: badgeController, adminBadgeController: adminBadgeController, apiKeyController: apiKeyController, aiController: aiController, aiConversationController: aiConversationController, aiConversationAdminController: aiConversationAdminController, mcpController: mcpController, } } func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(authUserMiddleware *middleware.AuthUserMiddleware, r *gin.RouterGroup) { // i18n r.GET("/language/config", a.langController.GetLangMapping) r.GET("/language/options", a.langController.GetUserLangOptions) // siteinfo r.GET("/siteinfo", a.siteInfoController.GetSiteInfo) r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegalInfo) // user r.GET("/user/info", a.userController.GetUserInfoByUserID) r.GET("/user/action/record", authUserMiddleware.Auth(), a.userController.ActionRecord) routerGroup := r.Group("", middleware.BanAPIForUserCenter) routerGroup.POST("/user/login/email", a.userController.UserEmailLogin) routerGroup.POST("/user/register/email", a.userController.UserRegisterByEmail) routerGroup.POST("/user/email/verification", a.userController.UserVerifyEmail) routerGroup.PUT("/user/email", a.userController.UserChangeEmailVerify) routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord) routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord) routerGroup.PUT("/user/notification/unsubscribe", a.userController.UserUnsubscribeNotification) // plugins r.GET("/plugin/status", a.pluginController.GetAllPluginStatus) } func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { // user r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername) r.GET("/user/ranking", a.userController.UserRanking) r.GET("/user/staff", a.userController.UserStaff) // answer r.GET("/answer/info", a.answerController.GetAnswerInfo) r.GET("/answer/page", a.answerController.AnswerList) r.GET("/personal/answer/page", a.questionController.PersonalAnswerPage) // question r.GET("/question/info", a.questionController.GetQuestion) r.GET("/question/invite", a.questionController.GetQuestionInviteUserInfo) r.GET("/question/page", a.questionController.QuestionPage) r.GET("/question/recommend/page", a.questionController.QuestionRecommendPage) r.GET("/question/similar/tag", a.questionController.SimilarQuestion) r.GET("/personal/qa/top", a.questionController.UserTop) r.GET("/personal/question/page", a.questionController.PersonalQuestionPage) r.GET("/question/link", a.questionController.GetQuestionLink) // comment r.GET("/comment/page", a.commentController.GetCommentWithPage) r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage) r.GET("/comment", a.commentController.GetComment) // tag r.GET("/tags/page", a.tagController.GetTagWithPage) r.GET("/tags/following", a.tagController.GetFollowingTags) r.GET("/tag", a.tagController.GetTagInfo) r.GET("/tags", a.tagController.GetTagsBySlugName) r.GET("/tag/synonyms", a.tagController.GetTagSynonyms) // search r.GET("/search", a.searchController.Search) r.GET("/search/desc", a.searchController.SearchDesc) // rank r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage) // reaction r.GET("/meta/reaction", a.metaController.GetReaction) // badges r.GET("/badge", a.badgeController.GetBadgeInfo) r.GET("/badge/awards/page", a.badgeController.GetBadgeAwardList) r.GET("/badge/user/awards/recent", a.badgeController.GetRecentBadgeAwardListByUsername) r.GET("/badge/user/awards", a.badgeController.GetAllBadgeAwardListByUsername) r.GET("/badges", a.badgeController.GetBadgeList) } func (a *AnswerAPIRouter) RegisterAuthUserWithAnyStatusAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/user/logout", a.userController.UserLogout) r.POST("/user/email/change/code", middleware.BanAPIForUserCenter, a.userController.UserChangeEmailSendCode) r.POST("/user/email/verification/send", middleware.BanAPIForUserCenter, a.userController.UserVerifyEmailSend) } func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { // revisions r.GET("/revisions", a.revisionController.GetRevisionList) r.GET("/revisions/unreviewed", a.revisionController.GetUnreviewedRevisionList) r.PUT("/revisions/audit", a.revisionController.RevisionAudit) r.GET("/revisions/edit/check", a.revisionController.CheckCanUpdateRevision) r.GET("/reviewing/type", a.revisionController.GetReviewingType) // comment r.POST("/comment", a.commentController.AddComment) r.DELETE("/comment", a.commentController.RemoveComment) r.PUT("/comment", a.commentController.UpdateComment) // report r.POST("/report", a.reportController.AddReport) r.GET("/report/unreviewed/post", a.reportController.GetUnreviewedReportPostPage) r.PUT("/report/review", a.reportController.ReviewReport) // review r.GET("/review/pending/post/page", a.reviewController.GetUnreviewedPostPage) r.PUT("/review/pending/post", a.reviewController.UpdateReview) // vote r.POST("/vote/up", a.voteController.VoteUp) r.POST("/vote/down", a.voteController.VoteDown) // follow r.POST("/follow", a.followController.Follow) r.PUT("/follow/tags", a.followController.UpdateFollowTags) // tag r.GET("/question/tags", a.tagController.SearchTagLike) r.POST("/tag", a.tagController.AddTag) r.PUT("/tag", a.tagController.UpdateTag) r.POST("/tag/recover", a.tagController.RecoverTag) r.DELETE("/tag", a.tagController.RemoveTag) r.PUT("/tag/synonym", a.tagController.UpdateTagSynonym) r.POST("/tag/merge", a.tagController.MergeTag) // collection r.POST("/collection/switch", a.collectionController.CollectionSwitch) r.GET("/personal/collection/page", a.questionController.PersonalCollectionPage) // question r.POST("/question", a.questionController.AddQuestion) r.POST("/question/answer", a.questionController.AddQuestionByAnswer) r.PUT("/question", a.questionController.UpdateQuestion) r.PUT("/question/invite", a.questionController.UpdateQuestionInviteUser) r.DELETE("/question", a.questionController.RemoveQuestion) r.PUT("/question/status", a.questionController.CloseQuestion) r.PUT("/question/operation", a.questionController.OperationQuestion) r.PUT("/question/reopen", a.questionController.ReopenQuestion) r.GET("/question/similar", a.questionController.GetSimilarQuestions) r.POST("/question/recover", a.questionController.QuestionRecover) // answer r.POST("/answer", a.answerController.AddAnswer) r.PUT("/answer", a.answerController.UpdateAnswer) r.POST("/answer/acceptance", a.answerController.AcceptAnswer) r.DELETE("/answer", a.answerController.RemoveAnswer) r.POST("/answer/recover", a.answerController.RecoverAnswer) // user r.PUT("/user/password", middleware.BanAPIForUserCenter, a.userController.UserModifyPassWord) r.PUT("/user/info", a.userController.UserUpdateInfo) r.PUT("/user/interface", a.userController.UserUpdateInterface) r.GET("/user/notification/config", a.userController.GetUserNotificationConfig) r.PUT("/user/notification/config", a.userController.UpdateUserNotificationConfig) r.GET("/user/info/search", a.userController.SearchUserListByName) // vote r.GET("/personal/vote/page", a.voteController.UserVotes) // reason r.GET("/reasons", a.reasonController.Reasons) // permission r.GET("/permission", a.permissionController.GetPermission) // notification r.GET("/notification/status", a.notificationController.GetRedDot) r.PUT("/notification/status", a.notificationController.ClearRedDot) r.GET("/notification/page", a.notificationController.GetList) r.PUT("/notification/read/state/all", a.notificationController.ClearUnRead) r.PUT("/notification/read/state", a.notificationController.ClearIDUnRead) // upload file r.POST("/file", a.uploadController.UploadFile) r.POST("/post/render", a.uploadController.PostRender) // activity r.GET("/activity/timeline", a.activityController.GetObjectTimeline) r.GET("/activity/timeline/detail", a.activityController.GetObjectTimelineDetail) // plugin r.GET("/user/plugin/configs", a.userPluginController.GetUserPluginList) r.GET("/user/plugin/config", a.userPluginController.GetUserPluginConfig) r.PUT("/user/plugin/config", a.userPluginController.UpdatePluginUserConfig) // meta r.PUT("/meta/reaction", a.metaController.AddOrUpdateReaction) // AI chat r.POST("/chat/completions", a.aiController.ChatCompletions) // AI conversation r.GET("/ai/conversation/page", a.aiConversationController.GetConversationList) r.GET("/ai/conversation", a.aiConversationController.GetConversationDetail) r.POST("/ai/conversation/vote", a.aiConversationController.VoteRecord) } func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) { r.GET("/question/page", a.questionController.AdminQuestionPage) r.PUT("/question/status", a.questionController.AdminUpdateQuestionStatus) r.GET("/answer/page", a.questionController.AdminAnswerPage) r.PUT("/answer/status", a.answerController.AdminUpdateAnswerStatus) // user r.GET("/users/page", a.adminUserController.GetUserPage) r.PUT("/user/status", a.adminUserController.UpdateUserStatus) r.PUT("/user/role", a.adminUserController.UpdateUserRole) r.GET("/user/activation", a.adminUserController.GetUserActivation) r.POST("/user/activation", a.adminUserController.SendUserActivation) r.POST("/user", a.adminUserController.AddUser) r.POST("/users", a.adminUserController.AddUsers) r.PUT("/user/password", a.adminUserController.UpdateUserPassword) r.PUT("/user/profile", a.adminUserController.EditUserProfile) r.DELETE("/delete/permanently", a.adminUserController.DeletePermanently) // reason r.GET("/reasons", a.reasonController.Reasons) // language r.GET("/language/options", a.langController.GetAdminLangOptions) // theme r.GET("/theme/options", a.themeController.GetThemeOptions) // siteinfo r.GET("/siteinfo/general", a.adminSiteInfoController.GetGeneral) r.PUT("/siteinfo/general", a.adminSiteInfoController.UpdateGeneral) r.GET("/siteinfo/interface", a.adminSiteInfoController.GetInterface) r.PUT("/siteinfo/interface", a.adminSiteInfoController.UpdateInterface) r.GET("/siteinfo/users-settings", a.adminSiteInfoController.GetUsersSettings) r.PUT("/siteinfo/users-settings", a.adminSiteInfoController.UpdateUsersSettings) r.GET("/siteinfo/branding", a.adminSiteInfoController.GetSiteBranding) r.PUT("/siteinfo/branding", a.adminSiteInfoController.UpdateBranding) r.GET("/siteinfo/question", a.adminSiteInfoController.GetSiteQuestion) r.PUT("/siteinfo/question", a.adminSiteInfoController.UpdateSiteQuestion) r.GET("/siteinfo/tag", a.adminSiteInfoController.GetSiteTag) r.PUT("/siteinfo/tag", a.adminSiteInfoController.UpdateSiteTag) r.GET("/siteinfo/advanced", a.adminSiteInfoController.GetSiteAdvanced) r.PUT("/siteinfo/advanced", a.adminSiteInfoController.UpdateSiteAdvanced) r.GET("/siteinfo/polices", a.adminSiteInfoController.GetSitePolicies) r.PUT("/siteinfo/polices", a.adminSiteInfoController.UpdateSitePolices) r.GET("/siteinfo/security", a.adminSiteInfoController.GetSiteSecurity) r.PUT("/siteinfo/security", a.adminSiteInfoController.UpdateSiteSecurity) r.GET("/siteinfo/seo", a.adminSiteInfoController.GetSeo) r.PUT("/siteinfo/seo", a.adminSiteInfoController.UpdateSeo) r.GET("/siteinfo/login", a.adminSiteInfoController.GetSiteLogin) r.PUT("/siteinfo/login", a.adminSiteInfoController.UpdateSiteLogin) r.GET("/siteinfo/custom-css-html", a.adminSiteInfoController.GetSiteCustomCssHTML) r.PUT("/siteinfo/custom-css-html", a.adminSiteInfoController.UpdateSiteCustomCssHTML) r.GET("/siteinfo/theme", a.adminSiteInfoController.GetSiteTheme) r.PUT("/siteinfo/theme", a.adminSiteInfoController.SaveSiteTheme) r.GET("/siteinfo/users", a.adminSiteInfoController.GetSiteUsers) r.PUT("/siteinfo/users", a.adminSiteInfoController.UpdateSiteUsers) r.GET("/setting/smtp", a.adminSiteInfoController.GetSMTPConfig) r.PUT("/setting/smtp", a.adminSiteInfoController.UpdateSMTPConfig) r.GET("/setting/privileges", a.adminSiteInfoController.GetPrivilegesConfig) r.PUT("/setting/privileges", a.adminSiteInfoController.UpdatePrivilegesConfig) // dashboard r.GET("/dashboard", a.dashboardController.DashboardInfo) // roles r.GET("/roles", a.roleController.GetRoleList) // plugin r.GET("/plugins", a.pluginController.GetPluginList) r.PUT("/plugin/status", a.pluginController.UpdatePluginStatus) r.GET("/plugin/config", a.pluginController.GetPluginConfig) r.PUT("/plugin/config", a.pluginController.UpdatePluginConfig) // badge r.GET("/badges", a.adminBadgeController.GetBadgeList) r.PUT("/badge/status", a.adminBadgeController.UpdateBadgeStatus) // api key r.GET("/api-key/all", a.apiKeyController.GetAllAPIKeys) r.POST("/api-key", a.apiKeyController.AddAPIKey) r.PUT("/api-key", a.apiKeyController.UpdateAPIKey) r.DELETE("/api-key", a.apiKeyController.DeleteAPIKey) // ai config r.GET("/ai-config", a.adminSiteInfoController.GetAIConfig) r.PUT("/ai-config", a.adminSiteInfoController.UpdateAIConfig) r.GET("/ai-provider", a.adminSiteInfoController.GetAIProvider) r.POST("/ai-models", a.adminSiteInfoController.RequestAIModels) // mcp config r.GET("/mcp-config", a.adminSiteInfoController.GetMCPConfig) r.PUT("/mcp-config", a.adminSiteInfoController.UpdateMCPConfig) // AI conversation management r.GET("/ai/conversation/page", a.aiConversationAdminController.GetConversationList) r.GET("/ai/conversation", a.aiConversationAdminController.GetConversationDetail) r.DELETE("/ai/conversation", a.aiConversationAdminController.DeleteConversation) } ================================================ FILE: internal/router/config.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package router // SwaggerConfig struct describes configure for the Swagger API endpoint type SwaggerConfig struct { Show bool `json:"show" mapstructure:"show" yaml:"show"` Protocol string `json:"protocol" mapstructure:"protocol" yaml:"protocol"` Host string `json:"host" mapstructure:"host" yaml:"host"` Address string `json:"address" mapstructure:"address" yaml:"address"` } ================================================ FILE: internal/router/mcp_router.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package router import ( "github.com/apache/answer/internal/schema/mcp_tools" "github.com/gin-gonic/gin" "github.com/mark3labs/mcp-go/server" ) func (a *AnswerAPIRouter) RegisterMCPRouter(r *gin.RouterGroup) { s := server.NewMCPServer("Answer Enterprise MCP Server", "1.0.0") s.AddTool(mcp_tools.NewQuestionsTool(), a.mcpController.MCPQuestionsHandler()) s.AddTool(mcp_tools.NewAnswersTool(), a.mcpController.MCPAnswersHandler()) s.AddTool(mcp_tools.NewCommentsTool(), a.mcpController.MCPCommentsHandler()) s.AddTool(mcp_tools.NewTagsTool(), a.mcpController.MCPTagsHandler()) s.AddTool(mcp_tools.NewTagDetailTool(), a.mcpController.MCPTagDetailsHandler()) s.AddTool(mcp_tools.NewUserTool(), a.mcpController.MCPUserDetailsHandler()) sseServer := server.NewSSEServer(s, server.WithSSEEndpoint("/answer/api/v1/mcp/see"), server.WithMessageEndpoint("/answer/api/v1/mcp/message"), ) r.GET("/mcp/sse", gin.WrapH(sseServer.SSEHandler())) r.POST("/mcp/message", gin.WrapH(sseServer.MessageHandler())) } ================================================ FILE: internal/router/plugin_api_router.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package router import ( "github.com/apache/answer/internal/controller" "github.com/gin-gonic/gin" ) type PluginAPIRouter struct { connectorController *controller.ConnectorController userCenterController *controller.UserCenterController captchaController *controller.CaptchaController embedController *controller.EmbedController renderController *controller.RenderController sidebarController *controller.SidebarController } func NewPluginAPIRouter( connectorController *controller.ConnectorController, userCenterController *controller.UserCenterController, captchaController *controller.CaptchaController, embedController *controller.EmbedController, renderController *controller.RenderController, sidebarController *controller.SidebarController, ) *PluginAPIRouter { return &PluginAPIRouter{ connectorController: connectorController, userCenterController: userCenterController, captchaController: captchaController, embedController: embedController, renderController: renderController, sidebarController: sidebarController, } } func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) { // connector plugin connectorController := pr.connectorController r.GET(controller.ConnectorLoginRouterPrefix+":name", connectorController.ConnectorLoginDispatcher) r.GET(controller.ConnectorRedirectRouterPrefix+":name", connectorController.ConnectorRedirectDispatcher) r.GET("/connector/info", connectorController.ConnectorsInfo) r.POST("/connector/binding/email", connectorController.ExternalLoginBindingUserSendEmail) // user center plugin r.GET("/user-center/agent", pr.userCenterController.UserCenterAgent) r.GET("/user-center/personal/branding", pr.userCenterController.UserCenterPersonalBranding) r.GET(controller.UserCenterLoginRouter, pr.userCenterController.UserCenterLoginRedirect) r.GET(controller.UserCenterSignUpRedirectRouter, pr.userCenterController.UserCenterSignUpRedirect) r.GET("/user-center/login/callback", pr.userCenterController.UserCenterLoginCallback) r.GET("/user-center/sign-up/callback", pr.userCenterController.UserCenterSignUpCallback) // captcha plugin r.GET("/captcha/config", pr.captchaController.GetCaptchaConfig) r.GET("/embed/config", pr.embedController.GetEmbedConfig) r.GET("/render/config", pr.renderController.GetRenderConfig) // sidebar plugin r.GET("/sidebar/config", pr.sidebarController.GetSidebarConfig) } func (pr *PluginAPIRouter) RegisterAuthUserConnectorRouter(r *gin.RouterGroup) { connectorController := pr.connectorController r.GET("/connector/user/info", connectorController.ConnectorsUserInfo) r.DELETE("/connector/user/unbinding", connectorController.ExternalLoginUnbinding) r.GET("/user-center/user/settings", pr.userCenterController.UserCenterUserSettings) } func (pr *PluginAPIRouter) RegisterAuthAdminConnectorRouter(r *gin.RouterGroup) { r.GET("/user-center/agent", pr.userCenterController.UserCenterAdminFunctionAgent) } ================================================ FILE: internal/router/provider.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package router import "github.com/google/wire" // ProviderSetRouter is providers. var ProviderSetRouter = wire.NewSet( NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewUIRouter, NewTemplateRouter, NewPluginAPIRouter, ) ================================================ FILE: internal/router/static_router.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package router import ( "net/http" "path/filepath" "strings" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/service/service_config" "github.com/apache/answer/pkg/dir" "github.com/gin-gonic/gin" ) // StaticRouter static api router type StaticRouter struct { serviceConfig *service_config.ServiceConfig } // NewStaticRouter new static api router func NewStaticRouter(serviceConfig *service_config.ServiceConfig) *StaticRouter { return &StaticRouter{ serviceConfig: serviceConfig, } } // RegisterStaticRouter register static api router func (a *StaticRouter) RegisterStaticRouter(r *gin.RouterGroup) { r.Static("/uploads/"+constant.AvatarSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.AvatarSubPath)) r.Static("/uploads/"+constant.AvatarThumbSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.AvatarThumbSubPath)) r.Static("/uploads/"+constant.PostSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.PostSubPath)) r.Static("/uploads/"+constant.BrandingSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.BrandingSubPath)) r.GET("/uploads/"+constant.FilesPostSubPath+"/*filepath", func(c *gin.Context) { // The filepath such as hash/123.pdf filePath := c.Param("filepath") // The original filename is 123.pdf originalFilename := filepath.Base(filePath) // The real filename is hash.pdf realFilename := strings.TrimSuffix(filePath, "/"+originalFilename) + filepath.Ext(originalFilename) // The file local path is /uploads/files/post/hash.pdf fileLocalPath := filepath.Join(a.serviceConfig.UploadPath, constant.FilesPostSubPath, realFilename) // If the file is not exist, return 404 if !dir.CheckFileExist(fileLocalPath) { c.Redirect(http.StatusFound, "/404") return } c.FileAttachment(fileLocalPath, originalFilename) }) } ================================================ FILE: internal/router/swagger_router.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package router import ( "fmt" "github.com/apache/answer/docs" "github.com/gin-gonic/gin" swaggerfiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) // SwaggerRouter swagger api router type SwaggerRouter struct { config *SwaggerConfig } // NewSwaggerRouter new swagger api router func NewSwaggerRouter(config *SwaggerConfig) *SwaggerRouter { return &SwaggerRouter{ config: config, } } // Register register swagger api router func (a *SwaggerRouter) Register(r *gin.RouterGroup) { if a.config.Show { a.InitSwaggerDocs() gofmt := fmt.Sprintf("%s://%s%s/swagger/doc.json", a.config.Protocol, a.config.Host, a.config.Address) r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler, ginSwagger.URL(gofmt))) } } // InitSwaggerDocs init swagger docs func (a *SwaggerRouter) InitSwaggerDocs() { docs.SwaggerInfo.Host = fmt.Sprintf("%s%s", a.config.Host, a.config.Address) } ================================================ FILE: internal/router/template_router.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package router import ( "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/controller" templaterender "github.com/apache/answer/internal/controller/template_render" "github.com/apache/answer/internal/controller_admin" "github.com/gin-gonic/gin" ) type TemplateRouter struct { templateController *controller.TemplateController templateRenderController *templaterender.TemplateRenderController siteInfoController *controller_admin.SiteInfoController authUserMiddleware *middleware.AuthUserMiddleware } func NewTemplateRouter( templateController *controller.TemplateController, templateRenderController *templaterender.TemplateRenderController, siteInfoController *controller_admin.SiteInfoController, authUserMiddleware *middleware.AuthUserMiddleware, ) *TemplateRouter { return &TemplateRouter{ templateController: templateController, templateRenderController: templateRenderController, siteInfoController: siteInfoController, authUserMiddleware: authUserMiddleware, } } // RegisterTemplateRouter template router func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup, baseURLPath string) { seoNoAuth := r.Group(baseURLPath) seoNoAuth.GET("/sitemap.xml", a.templateController.Sitemap) seoNoAuth.GET("/sitemap/:page", a.templateController.SitemapPage) seoNoAuth.GET("/robots.txt", a.siteInfoController.GetRobots) seoNoAuth.GET("/custom.css", a.siteInfoController.GetCss) seoNoAuth.GET("/404", a.templateController.Page404) seoNoAuth.GET("/opensearch.xml", a.templateController.OpenSearch) seo := r.Group(baseURLPath) seo.Use(a.authUserMiddleware.CheckPrivateMode()) seo.GET("/", a.templateController.Index) seo.GET("/questions", a.templateController.QuestionList) seo.GET("/questions/:id", a.templateController.QuestionInfo) seo.GET("/questions/:id/:title", a.templateController.QuestionInfo) seo.GET("/questions/:id/:title/:answerid", a.templateController.QuestionInfo) seo.GET("/tags", a.templateController.TagList) seo.GET("/tags/:tag", a.templateController.TagInfo) seo.GET("/users/:username", a.templateController.UserInfo) } ================================================ FILE: internal/router/ui.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package router import ( "embed" "fmt" "io/fs" "net/http" "os" "strings" "github.com/apache/answer/internal/controller" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/plugin" "github.com/apache/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) const UIIndexFilePath = "build/index.html" const UIRootFilePath = "build" const UIStaticPath = "build/static" // UIRouter is an interface that provides ui static file routers type UIRouter struct { siteInfoController *controller.SiteInfoController siteInfoService siteinfo_common.SiteInfoCommonService } // NewUIRouter creates a new UIRouter instance with the embed resources func NewUIRouter( siteInfoController *controller.SiteInfoController, siteInfoService siteinfo_common.SiteInfoCommonService, ) *UIRouter { return &UIRouter{ siteInfoController: siteInfoController, siteInfoService: siteInfoService, } } // _resource is an interface that provides static file, it's a private interface type _resource struct { fs embed.FS } // Open to implement the interface by http.FS required func (r *_resource) Open(name string) (fs.File, error) { name = fmt.Sprintf(UIStaticPath+"/%s", name) log.Debugf("open static path %s", name) return r.fs.Open(name) } // Register a new static resource which generated by ui directory func (a *UIRouter) Register(r *gin.Engine, baseURLPath string) { staticPath := os.Getenv("ANSWER_STATIC_PATH") // if ANSWER_STATIC_PATH is set and not empty, ignore embed resource if staticPath != "" { info, err := os.Stat(staticPath) if err != nil || !info.IsDir() { log.Error(err) } else { log.Debugf("registering static path %s", staticPath) r.LoadHTMLGlob(staticPath + "/*.html") r.Static(baseURLPath+"/static", staticPath+"/static") r.NoRoute(func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{}) }) // return immediately if the static path is set return } } // handle the static file by default ui static files r.StaticFS(baseURLPath+"/static", http.FS(&_resource{ fs: ui.Build, })) // specify the not router for default routes and redirect r.NoRoute(func(c *gin.Context) { urlPath := c.Request.URL.Path if len(baseURLPath) > 0 { urlPath = strings.TrimPrefix(urlPath, baseURLPath) } filePath := "" switch urlPath { case "/favicon.ico": branding, err := a.siteInfoService.GetSiteBranding(c) if err != nil { log.Error(err) } if branding != nil && branding.Favicon != "" { c.String(http.StatusOK, htmltext.GetPicByUrl(branding.Favicon)) return } else if branding != nil && branding.SquareIcon != "" { c.String(http.StatusOK, htmltext.GetPicByUrl(branding.SquareIcon)) return } else { c.Header("content-type", "image/vnd.microsoft.icon") filePath = UIRootFilePath + urlPath } case "/manifest.json": a.siteInfoController.GetManifestJson(c) return case "/install": // if answer is running by run command user can not access install page. c.Redirect(http.StatusFound, "/") return default: filePath = UIIndexFilePath c.Header("content-type", "text/html;charset=utf-8") c.Header("X-Frame-Options", "DENY") } file, err := ui.Build.ReadFile(filePath) if err != nil { log.Error(err) c.Status(http.StatusNotFound) return } cdnPrefix := "" _ = plugin.CallCDN(func(fn plugin.CDN) error { cdnPrefix = fn.GetStaticPrefix() return nil }) if cdnPrefix != "" { if cdnPrefix[len(cdnPrefix)-1:] == "/" { cdnPrefix = strings.TrimSuffix(cdnPrefix, "/") } c.String(http.StatusOK, strings.ReplaceAll(string(file), "/static", cdnPrefix+"/static")) return } // This part is to solve the problem of returning 404 when the access path does not exist. // However, there is no way to check whether the current route exists in the frontend. // We can only hand over the route to the frontend for processing. // And the plugin, frontend routes can now be dynamically registered, // so there's no good way to get all frontend routes //if filePath == UIIndexFilePath { // c.String(http.StatusNotFound, string(file)) // return //} c.String(http.StatusOK, string(file)) }) } ================================================ FILE: internal/router/ui_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package router import ( "errors" "net/http" "net/http/httptest" "testing" "github.com/apache/answer/internal/service/mock" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" ) func TestUIRouter_FaviconWithNilBranding(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockSiteInfoService := mock.NewMockSiteInfoCommonService(ctrl) // Simulate a database error mockSiteInfoService.EXPECT(). GetSiteBranding(gomock.Any()). Return(nil, errors.New("database connection failed")) router := &UIRouter{ siteInfoService: mockSiteInfoService, } gin.SetMode(gin.TestMode) r := gin.New() router.Register(r, "") req := httptest.NewRequest(http.MethodGet, "/favicon.ico", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } ================================================ FILE: internal/schema/activity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import "github.com/apache/answer/internal/base/constant" // ActivityMsg activity message type ActivityMsg struct { UserID string TriggerUserID int64 ObjectID string OriginalObjectID string ActivityTypeKey constant.ActivityTypeKey RevisionID string ExtraInfo map[string]string } // GetObjectTimelineReq get object timeline request type GetObjectTimelineReq struct { ObjectID string `validate:"omitempty,gt=0,lte=100" form:"object_id"` ShowVote bool `validate:"omitempty" form:"show_vote"` UserID string `json:"-"` IsAdmin bool `json:"-"` } // GetObjectTimelineResp get object timeline response type GetObjectTimelineResp struct { ObjectInfo *ActObjectInfo `json:"object_info"` Timeline []*ActObjectTimeline `json:"timeline"` } // ActObjectTimeline act object timeline type ActObjectTimeline struct { ActivityID string `json:"activity_id"` RevisionID string `json:"revision_id"` CreatedAt int64 `json:"created_at"` ActivityType string `json:"activity_type"` Comment string `json:"comment"` ObjectID string `json:"object_id"` ObjectType string `json:"object_type"` Cancelled bool `json:"cancelled"` CancelledAt int64 `json:"cancelled_at"` UserInfo *UserBasicInfo `json:"user_info,omitempty"` } // ActObjectInfo act object info type ActObjectInfo struct { Title string `json:"title"` ObjectType string `json:"object_type"` QuestionID string `json:"question_id"` AnswerID string `json:"answer_id"` Username string `json:"username"` DisplayName string `json:"display_name"` MainTagSlugName string `json:"main_tag_slug_name"` } // GetObjectTimelineDetailReq get object timeline detail request type GetObjectTimelineDetailReq struct { NewRevisionID string `validate:"required,gt=0,lte=100" form:"new_revision_id"` OldRevisionID string `validate:"required,gt=0,lte=100" form:"old_revision_id"` UserID string `json:"-"` } // GetObjectTimelineDetailResp get object timeline detail response type GetObjectTimelineDetailResp struct { NewRevision *ObjectTimelineDetail `json:"new_revision"` OldRevision *ObjectTimelineDetail `json:"old_revision"` } // ObjectTimelineDetail object timeline detail type ObjectTimelineDetail struct { Title string `json:"title"` Tags []*ObjectTimelineTag `json:"tags"` OriginalText string `json:"original_text"` SlugName string `json:"slug_name"` MainTagSlugName string `json:"main_tag_slug_name"` } // ObjectTimelineTag object timeline tags type ObjectTimelineTag struct { SlugName string `json:"slug_name"` DisplayName string `json:"display_name"` MainTagSlugName string `json:"main_tag_slug_name"` Recommend bool `json:"recommend"` Reserved bool `json:"reserved"` } // PassReviewActivity pass review activity type PassReviewActivity struct { UserID string `json:"user_id"` TriggerUserID string `json:"trigger_user_id"` ObjectID string `json:"object_id"` OriginalObjectID string `json:"original_object_id"` RevisionID string `json:"revision_id"` } ================================================ FILE: internal/schema/ai_config_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // GetAIProviderResp get AI providers response type GetAIProviderResp struct { Name string `json:"name"` DisplayName string `json:"display_name"` DefaultAPIHost string `json:"default_api_host"` } // GetAIModelsResp get AI model response type GetAIModelsResp struct { Object string `json:"object"` Data []struct { Id string `json:"id"` Object string `json:"object"` Created int `json:"created"` OwnedBy string `json:"owned_by"` } `json:"data"` } type GetAIModelsReq struct { APIHost string `json:"api_host"` APIKey string `json:"api_key"` } // GetAIModelResp get AI model response type GetAIModelResp struct { Id string `json:"id"` Object string `json:"object"` Created int `json:"created"` OwnedBy string `json:"owned_by"` } ================================================ FILE: internal/schema/ai_conversation_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "github.com/apache/answer/internal/base/validator" ) // AIConversationListReq ai conversation list req type AIConversationListReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` UserID string `validate:"omitempty" json:"-"` } // AIConversationListItem ai conversation list item type AIConversationListItem struct { ConversationID string `json:"conversation_id"` Topic string `json:"topic"` CreatedAt int64 `json:"created_at"` } // AIConversationDetailReq ai conversation detail req type AIConversationDetailReq struct { ConversationID string `validate:"required" form:"conversation_id" json:"conversation_id"` UserID string `validate:"omitempty" json:"-"` } // AIConversationRecord ai conversation record type AIConversationRecord struct { ChatCompletionID string `json:"chat_completion_id"` Role string `json:"role"` Content string `json:"content"` Helpful int `json:"helpful"` Unhelpful int `json:"unhelpful"` CreatedAt int64 `json:"created_at"` } // AIConversationDetailResp ai conversation detail resp type AIConversationDetailResp struct { ConversationID string `json:"conversation_id"` Topic string `json:"topic"` Records []*AIConversationRecord `json:"records"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } // AIConversationVoteReq ai conversation vote req type AIConversationVoteReq struct { ChatCompletionID string `validate:"required" json:"chat_completion_id"` VoteType string `validate:"required,oneof=helpful unhelpful" json:"vote_type"` Cancel bool `validate:"omitempty" json:"cancel"` UserID string `validate:"omitempty" json:"-"` } // AIConversationAdminListReq ai conversation admin list req type AIConversationAdminListReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` } // AIConversationAdminListItem ai conversation admin list item type AIConversationAdminListItem struct { ID string `json:"id"` Topic string `json:"topic"` UserInfo AIConversationUserInfo `json:"user_info"` HelpfulCount int64 `json:"helpful_count"` UnhelpfulCount int64 `json:"unhelpful_count"` CreatedAt int64 `json:"created_at"` } // AIConversationUserInfo ai conversation user info type AIConversationUserInfo struct { ID string `json:"id"` Username string `json:"username"` DisplayName string `json:"display_name"` Avatar string `json:"avatar"` Rank int `json:"rank"` } // AIConversationAdminDetailReq ai conversation admin detail req type AIConversationAdminDetailReq struct { ConversationID string `validate:"required" form:"conversation_id" json:"conversation_id"` } // AIConversationAdminDetailResp ai conversation admin detail resp type AIConversationAdminDetailResp struct { ConversationID string `json:"conversation_id"` Topic string `json:"topic"` UserInfo AIConversationUserInfo `json:"user_info"` Records []AIConversationRecord `json:"records"` CreatedAt int64 `json:"created_at"` } // AIConversationAdminDeleteReq admin delete ai type AIConversationAdminDeleteReq struct { ConversationID string `validate:"required" json:"conversation_id"` } func (req *AIConversationDetailReq) Check() (errFields []*validator.FormErrorField, err error) { return nil, nil } func (req *AIConversationVoteReq) Check() (errFields []*validator.FormErrorField, err error) { return nil, nil } ================================================ FILE: internal/schema/answer_activity_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // AcceptAnswerOperationInfo accept answer operation info type AcceptAnswerOperationInfo struct { TriggerUserID string QuestionObjectID string QuestionUserID string AnswerObjectID string AnswerUserID string // vote activity info Activities []*AcceptAnswerActivity } // AcceptAnswerActivity accept answer activity type AcceptAnswerActivity struct { ActivityType int ActivityUserID string TriggerUserID string OriginalObjectID string Rank int } func (v *AcceptAnswerActivity) HasRank() int { if v.Rank != 0 { return 1 } return 0 } func (a *AcceptAnswerOperationInfo) GetUserIDs() (userIDs []string) { for _, act := range a.Activities { userIDs = append(userIDs, act.ActivityUserID) } return userIDs } ================================================ FILE: internal/schema/answer_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" ) // RemoveAnswerReq delete answer request type RemoveAnswerReq struct { ID string `validate:"required" json:"id"` UserID string `json:"-"` CanDelete bool `json:"-"` CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` } // RecoverAnswerReq recover answer request type RecoverAnswerReq struct { AnswerID string `validate:"required" json:"answer_id"` UserID string `json:"-"` } const ( AnswerAcceptedFailed = 1 AnswerAcceptedEnable = 2 ) type AnswerAddReq struct { QuestionID string `json:"question_id"` Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` HTML string `json:"-"` UserID string `json:"-"` CanEdit bool `json:"-"` CanDelete bool `json:"-"` CanRecover bool `json:"-"` CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` IP string `json:"-"` UserAgent string `json:"-"` } func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) { req.HTML = converter.Markdown2HTML(req.Content) if req.HTML == "" { return append(errFields, &validator.FormErrorField{ ErrorField: "content", ErrorMsg: reason.AnswerContentCannotEmpty, }), errors.BadRequest(reason.AnswerContentCannotEmpty) } return nil, nil } type GetAnswerInfoResp struct { Info *AnswerInfo `json:"info"` Question *QuestionInfoResp `json:"question"` } type AnswerUpdateReq struct { ID string `json:"id"` Title string `json:"title"` Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` EditSummary string `validate:"omitempty" json:"edit_summary"` HTML string `json:"-"` UserID string `json:"-"` NoNeedReview bool `json:"-"` CanEdit bool `json:"-"` CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` } func (req *AnswerUpdateReq) Check() (errFields []*validator.FormErrorField, err error) { req.HTML = converter.Markdown2HTML(req.Content) if req.HTML == "" { return append(errFields, &validator.FormErrorField{ ErrorField: "content", ErrorMsg: reason.AnswerContentCannotEmpty, }), errors.BadRequest(reason.AnswerContentCannotEmpty) } return nil, nil } // AnswerUpdateResp answer update resp type AnswerUpdateResp struct { WaitForReview bool `json:"wait_for_review"` } type AnswerListReq struct { QuestionID string `json:"question_id" form:"question_id"` Order string `json:"order" form:"order"` Page int `json:"page" form:"page"` PageSize int `json:"page_size" form:"page_size"` UserID string `json:"-"` IsAdmin bool `json:"-"` CanEdit bool `json:"-"` CanDelete bool `json:"-"` CanRecover bool `json:"-"` } type AnswerInfo struct { ID string `json:"id"` QuestionID string `json:"question_id"` Content string `json:"content"` HTML string `json:"html"` CreateTime int64 `json:"create_time"` UpdateTime int64 `json:"update_time"` Accepted int `json:"accepted"` UserID string `json:"-"` UpdateUserID string `json:"-"` UserInfo *UserBasicInfo `json:"user_info,omitempty"` UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` Collected bool `json:"collected"` VoteStatus string `json:"vote_status"` VoteCount int `json:"vote_count"` QuestionInfo *QuestionInfoResp `json:"question_info,omitempty"` Status int `json:"status"` // MemberActions MemberActions []*PermissionMemberAction `json:"member_actions"` } type AdminAnswerInfo struct { ID string `json:"id"` QuestionID string `json:"question_id"` Description string `json:"description"` CreateTime int64 `json:"create_time"` UpdateTime int64 `json:"update_time"` Accepted int `json:"accepted"` UserID string `json:"-"` UpdateUserID string `json:"-"` UserInfo *UserBasicInfo `json:"user_info"` VoteCount int `json:"vote_count"` QuestionInfo struct { Title string `json:"title"` } `json:"question_info"` } type AcceptAnswerReq struct { QuestionID string `validate:"required,gt=0,lte=30" json:"question_id"` AnswerID string `validate:"omitempty" json:"answer_id"` UserID string `json:"-"` } func (req *AcceptAnswerReq) Check() (errFields []*validator.FormErrorField, err error) { if len(req.AnswerID) == 0 { req.AnswerID = "0" } return nil, nil } type AdminUpdateAnswerStatusReq struct { AnswerID string `validate:"required" json:"answer_id"` Status string `validate:"required,oneof=available deleted" json:"status"` UserID string `json:"-"` } ================================================ FILE: internal/schema/api_key_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // GetAPIKeyReq get api key request type GetAPIKeyReq struct { UserID string `json:"-"` } // GetAPIKeyResp get api keys response type GetAPIKeyResp struct { ID int `json:"id"` AccessKey string `json:"access_key"` Description string `json:"description"` Scope string `json:"scope"` CreatedAt int64 `json:"created_at"` LastUsedAt int64 `json:"last_used_at"` } // AddAPIKeyReq add api key request type AddAPIKeyReq struct { Description string `validate:"required,notblank,lte=150" json:"description"` Scope string `validate:"required,oneof=read-only global" json:"scope"` UserID string `json:"-"` } // AddAPIKeyResp add api key response type AddAPIKeyResp struct { AccessKey string `json:"access_key"` } // UpdateAPIKeyReq update api key request type UpdateAPIKeyReq struct { ID int `validate:"required" json:"id"` Description string `validate:"required,notblank,lte=150" json:"description"` UserID string `json:"-"` } // DeleteAPIKeyReq delete api key request type DeleteAPIKeyReq struct { ID int `json:"id"` UserID string `json:"-"` } ================================================ FILE: internal/schema/backyard_user_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "context" "strings" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/errors" ) // UpdateUserStatusReq update user request type UpdateUserStatusReq struct { UserID string `validate:"required" json:"user_id"` Status string `validate:"required,oneof=normal suspended deleted inactive" json:"status" enums:"normal,suspended,deleted,inactive"` SuspendDuration string `validate:"omitempty,oneof=24h 48h 72h 7d 14d 1m 2m 3m 6m 1y forever" json:"suspend_duration"` RemoveAllContent bool `validate:"omitempty" json:"remove_all_content"` LoginUserID string `json:"-"` } func (r *UpdateUserStatusReq) IsNormal() bool { return r.Status == constant.UserNormal } func (r *UpdateUserStatusReq) IsSuspended() bool { return r.Status == constant.UserSuspended } func (r *UpdateUserStatusReq) IsDeleted() bool { return r.Status == constant.UserDeleted } func (r *UpdateUserStatusReq) IsInactive() bool { return r.Status == constant.UserInactive } // GetSuspendedUntil calculates the suspended until time based on duration func (r *UpdateUserStatusReq) GetSuspendedUntil() time.Time { if !r.IsSuspended() || r.SuspendDuration == "" || r.SuspendDuration == "forever" { return entity.PermanentSuspensionTime // permanent suspension } now := time.Now() switch r.SuspendDuration { case "24h": return now.Add(24 * time.Hour) case "48h": return now.Add(48 * time.Hour) case "72h": return now.Add(72 * time.Hour) case "7d": return now.Add(7 * 24 * time.Hour) case "14d": return now.Add(14 * 24 * time.Hour) case "1m": return now.AddDate(0, 1, 0) case "2m": return now.AddDate(0, 2, 0) case "3m": return now.AddDate(0, 3, 0) case "6m": return now.AddDate(0, 6, 0) case "1y": return now.AddDate(1, 0, 0) default: return entity.PermanentSuspensionTime // fallback to permanent } } // GetUserPageReq get user list page request type GetUserPageReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // email Query string `validate:"omitempty,gt=0,lte=100" form:"query"` // user status Status string `validate:"omitempty,oneof=normal suspended deleted inactive" form:"status"` // staff, if staff is true means query admin or moderator Staff bool `validate:"omitempty" form:"staff"` } func (r *GetUserPageReq) IsSuspended() bool { return r.Status == constant.UserSuspended } func (r *GetUserPageReq) IsDeleted() bool { return r.Status == constant.UserDeleted } func (r *GetUserPageReq) IsInactive() bool { return r.Status == constant.UserInactive } // GetUserPageResp get user response type GetUserPageResp struct { // user id UserID string `json:"user_id"` // create time CreatedAt int64 `json:"created_at"` // delete time DeletedAt int64 `json:"deleted_at"` // suspended time SuspendedAt int64 `json:"suspended_at"` // suspended until time SuspendedUntil int64 `json:"suspended_until"` // username Username string `json:"username"` // email EMail string `json:"e_mail"` // rank Rank int `json:"rank"` // user status(normal,suspended,deleted,inactive) Status string `json:"status"` // display name DisplayName string `json:"display_name"` // avatar Avatar string `json:"avatar"` // role id RoleID int `json:"role_id"` // role name RoleName string `json:"role_name"` } // GetUserInfoReq get user request type GetUserInfoReq struct { UserID string `validate:"required" json:"user_id"` } // GetUserInfoResp get user response type GetUserInfoResp struct { // suspended until SuspendedUntil time.Time `json:"suspended_until"` } // UpdateUserRoleReq update user role request type UpdateUserRoleReq struct { // user id UserID string `validate:"required" json:"user_id"` // role id RoleID int `validate:"required" json:"role_id"` // login user id LoginUserID string `json:"-"` } // EditUserProfileReq edit user profile request type EditUserProfileReq struct { UserID string `validate:"required" json:"user_id"` DisplayName string `validate:"required,gte=2,lte=30" json:"display_name"` Username string `validate:"omitempty,gte=2,lte=30" json:"username"` Email string `validate:"required,email,gt=0,lte=500" json:"email"` LoginUserID string `json:"-"` IsAdmin bool `json:"-"` } // AddUserReq add user request type AddUserReq struct { DisplayName string `validate:"required,gte=2,lte=30" json:"display_name"` Email string `validate:"required,email,gt=0,lte=500" json:"email"` Password string `validate:"required,gte=8,lte=32" json:"password"` LoginUserID string `json:"-"` } // AddUsersReq add users request type AddUsersReq struct { // users info line by line UsersStr string `json:"users"` Users []*AddUserReq `json:"-"` } // DeletePermanentlyReq delete permanently request type DeletePermanentlyReq struct { Type string `validate:"required,oneof=users questions answers" json:"type"` } type AddUsersErrorData struct { // optional. error field name. Field string `json:"field"` // must. error line number. Line int `json:"line"` // must. error content. Content string `json:"content"` // optional. error message. ExtraMessage string `json:"extra_message"` } func (e *AddUsersErrorData) GetErrField(ctx context.Context) (errFields []*validator.FormErrorField) { return append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "users", ErrorMsg: translator.TrWithData(handler.GetLangByCtx(ctx), reason.AddBulkUsersFormatError, e), }) } func (req *AddUsersReq) ParseUsers(ctx context.Context) (errFields []*validator.FormErrorField, err error) { req.UsersStr = strings.TrimSpace(req.UsersStr) lines := strings.Split(req.UsersStr, "\n") req.Users = make([]*AddUserReq, 0) for i, line := range lines { arr := strings.Split(line, ",") if len(arr) != 3 { errFields = append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "users", ErrorMsg: translator.TrWithData(handler.GetLangByCtx(ctx), reason.AddBulkUsersFormatError, &AddUsersErrorData{ Line: i + 1, Content: line, }), }) return errFields, errors.BadRequest(reason.RequestFormatError) } req.Users = append(req.Users, &AddUserReq{ DisplayName: strings.TrimSpace(arr[0]), Email: strings.TrimSpace(arr[1]), Password: strings.TrimSpace(arr[2]), }) } // check users amount if len(req.Users) == 0 || len(req.Users) > constant.DefaultBulkUser { errFields = append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "users", ErrorMsg: translator.TrWithData(handler.GetLangByCtx(ctx), reason.AddBulkUsersAmountError, map[string]int{ "MaxAmount": constant.DefaultBulkUser, }), }) return errFields, errors.BadRequest(reason.RequestFormatError) } return nil, nil } // UpdateUserPasswordReq update user password request type UpdateUserPasswordReq struct { UserID string `validate:"required" json:"user_id"` Password string `validate:"required,gte=8,lte=32" json:"password"` LoginUserID string `json:"-"` } // GetUserActivationReq get user activation type GetUserActivationReq struct { UserID string `validate:"required" form:"user_id"` } // GetUserActivationResp get user activation type GetUserActivationResp struct { ActivationURL string `json:"activation_url"` } // SendUserActivationReq send user activation type SendUserActivationReq struct { UserID string `validate:"required" json:"user_id"` } ================================================ FILE: internal/schema/badge_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import "github.com/apache/answer/internal/entity" const ( BadgeStatusActive BadgeStatus = "active" BadgeStatusInactive BadgeStatus = "inactive" ) type BadgeStatus string var BadgeStatusMap = map[int8]BadgeStatus{ entity.BadgeStatusActive: BadgeStatusActive, entity.BadgeStatusInactive: BadgeStatusInactive, } var BadgeStatusEMap = map[BadgeStatus]int8{ BadgeStatusActive: entity.BadgeStatusActive, BadgeStatusInactive: entity.BadgeStatusInactive, } // BadgeListInfo get badge list response type BadgeListInfo struct { // badge id ID string `json:"id" ` // badge name Name string `json:"name" ` // badge icon Icon string `json:"icon" ` // badge award count AwardCount int `json:"award_count" ` // badge earned count EarnedCount int64 `json:"earned_count" ` // badge level Level entity.BadgeLevel `json:"level" ` } type GetBadgeListResp struct { // badge list info Badges []*BadgeListInfo `json:"badges" ` // badge group name GroupName string `json:"group_name" ` } type UpdateBadgeStatusReq struct { // badge id ID string `validate:"required" json:"id"` // badge status Status BadgeStatus `validate:"required" json:"status"` } type GetBadgeListPagedReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // badge status Status BadgeStatus `validate:"omitempty" form:"status"` // query condition Query string `validate:"omitempty" form:"q"` } type GetBadgeListPagedResp struct { // badge id ID string `json:"id" ` // badge name Name string `json:"name" ` // badge description Description string `json:"description" ` // badge icon Icon string `json:"icon" ` // badge award count AwardCount int `json:"award_count" ` // badge earned count Earned bool `json:"earned" ` // badge level Level entity.BadgeLevel `json:"level" ` // badge group name GroupName string `json:"group_name" ` // badge status Status BadgeStatus `json:"status"` } type GetBadgeInfoResp struct { // badge id ID string `json:"id" ` // badge name Name string `json:"name" ` // badge description Description string `json:"description" ` // badge icon Icon string `json:"icon" ` // badge award count AwardCount int `json:"award_count" ` // badge earned count EarnedCount int64 `json:"earned_count" ` // badge is single or multiple IsSingle bool `json:"is_single" ` // badge level Level entity.BadgeLevel `json:"level" ` } type GetBadgeAwardWithPageReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // badge id BadgeID string `validate:"required" form:"badge_id"` // username Username string `validate:"omitempty,gt=0,lte=100" form:"username"` // user id UserID string `json:"-"` } type GetBadgeAwardWithPageResp struct { // created time CreatedAt int64 `json:"created_at"` // object id ObjectID string `json:"object_id"` // question id QuestionID string `json:"question_id"` // answer id AnswerID string `json:"answer_id"` // comment id CommentID string `json:"comment_id"` // object type ObjectType string `json:"object_type" enums:"question,answer,comment"` // url title UrlTitle string `json:"url_title"` // author user info AuthorUserInfo UserBasicInfo `json:"author_user_info"` } type GetUserBadgeAwardListReq struct { // username Username string `validate:"required,gt=0,lte=100" form:"username"` // user id UserID string `json:"-"` Limit int `json:"-"` } type GetUserBadgeAwardListResp struct { // badge id ID string `json:"id" ` // badge name Name string `json:"name" ` // badge icon Icon string `json:"icon" ` // badge award count EarnedCount int64 `json:"earned_count" ` // badge level Level entity.BadgeLevel `json:"level" ` } type BadgeTplData struct { ProfileURL string } ================================================ FILE: internal/schema/collection_group_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import "time" const ( CGDefault = 1 CGDIY = 2 ) // CollectionSwitchReq switch collection request type CollectionSwitchReq struct { ObjectID string `validate:"required" json:"object_id"` GroupID string `validate:"required" json:"group_id"` Bookmark bool `validate:"omitempty" json:"bookmark"` UserID string `json:"-"` } // CollectionSwitchResp switch collection response type CollectionSwitchResp struct { ObjectCollectionCount int64 `json:"object_collection_count"` } // AddCollectionGroupReq add collection group request type AddCollectionGroupReq struct { // UserID int64 `validate:"required" comment:"" json:"user_id"` // the collection group name Name string `validate:"required,gt=0,lte=50" comment:"the collection group name" json:"name"` // mark this group is default, default 1 DefaultGroup int `validate:"required" comment:"mark this group is default, default 1" json:"default_group"` // CreateTime time.Time `validate:"required" comment:"" json:"create_time"` // UpdateTime time.Time `validate:"required" comment:"" json:"update_time"` } // UpdateCollectionGroupReq update collection group request type UpdateCollectionGroupReq struct { // ID int64 `validate:"required" comment:"" json:"id"` // UserID int64 `validate:"omitempty" comment:"" json:"user_id"` // the collection group name Name string `validate:"omitempty,gt=0,lte=50" comment:"the collection group name" json:"name"` // mark this group is default, default 1 DefaultGroup int `validate:"omitempty" comment:"mark this group is default, default 1" json:"default_group"` // CreateTime time.Time `validate:"omitempty" comment:"" json:"create_time"` // UpdateTime time.Time `validate:"omitempty" comment:"" json:"update_time"` } // GetCollectionGroupResp get collection group response type GetCollectionGroupResp struct { // ID int64 `json:"id"` // UserID int64 `json:"user_id"` // the collection group name Name string `json:"name"` // mark this group is default, default 1 DefaultGroup int `json:"default_group"` // CreateTime time.Time `json:"create_time"` // UpdateTime time.Time `json:"update_time"` } ================================================ FILE: internal/schema/comment_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/pkg/converter" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" ) // AddCommentReq add comment request type AddCommentReq struct { // object id ObjectID string `validate:"required" json:"object_id"` // reply comment id ReplyCommentID string `validate:"omitempty" json:"reply_comment_id"` // original comment content OriginalText string `validate:"required,notblank,gte=2,lte=600" json:"original_text"` // parsed comment content ParsedText string `json:"-"` // @ user id list MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"` CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` // user id UserID string `json:"-"` // whether user can add it CanAdd bool `json:"-"` // whether user can edit it CanEdit bool `json:"-"` // whether user can delete it CanDelete bool `json:"-"` IP string `json:"-"` UserAgent string `json:"-"` } func (req *AddCommentReq) Check() (errFields []*validator.FormErrorField, err error) { req.ParsedText = converter.Markdown2HTML(req.OriginalText) if req.ParsedText == "" { return append(errFields, &validator.FormErrorField{ ErrorField: "original_text", ErrorMsg: reason.CommentContentCannotEmpty, }), errors.BadRequest(reason.CommentContentCannotEmpty) } return nil, nil } // RemoveCommentReq remove comment type RemoveCommentReq struct { // comment id CommentID string `validate:"required" json:"comment_id"` // user id UserID string `json:"-"` CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` } // UpdateCommentReq update comment request type UpdateCommentReq struct { // comment id CommentID string `validate:"required" json:"comment_id"` // original comment content OriginalText string `validate:"required,notblank,gte=2,lte=600" json:"original_text"` // parsed comment content ParsedText string `json:"-"` // user id UserID string `json:"-"` IsAdmin bool `json:"-"` // whether user can edit it CanEdit bool `json:"-"` // whether user can delete it CaptchaID string `json:"captcha_id"` // captcha_id CaptchaCode string `json:"captcha_code"` } func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) { req.ParsedText = converter.Markdown2HTML(req.OriginalText) if req.ParsedText == "" { return append(errFields, &validator.FormErrorField{ ErrorField: "original_text", ErrorMsg: reason.CommentContentCannotEmpty, }), errors.BadRequest(reason.CommentContentCannotEmpty) } return nil, nil } type UpdateCommentResp struct { // comment id CommentID string `json:"comment_id"` // original comment content OriginalText string `json:"original_text"` // parsed comment content ParsedText string `json:"parsed_text"` } // GetCommentListReq get comment list all request type GetCommentListReq struct { // user id UserID int64 `validate:"omitempty" comment:"user id" form:"user_id"` // reply user id ReplyUserID int64 `validate:"omitempty" comment:"reply user id" form:"reply_user_id"` // reply comment id ReplyCommentID int64 `validate:"omitempty" comment:"reply comment id" form:"reply_comment_id"` // object id ObjectID int64 `validate:"omitempty" comment:"object id" form:"object_id"` // user vote amount VoteCount int `validate:"omitempty" comment:"user vote amount" form:"vote_count"` // comment status(available: 0; deleted: 10) Status int `validate:"omitempty" comment:"comment status(available: 0; deleted: 10)" form:"status"` // original comment content OriginalText string `validate:"omitempty" comment:"original comment content" form:"original_text"` // parsed comment content ParsedText string `validate:"omitempty" comment:"parsed comment content" form:"parsed_text"` } // GetCommentWithPageReq get comment list page request type GetCommentWithPageReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // object id ObjectID string `validate:"required" form:"object_id"` // comment id CommentID string `validate:"omitempty" form:"comment_id"` // query condition QueryCond string `validate:"omitempty,oneof=vote created_at" form:"query_cond"` // user id UserID string `json:"-"` // whether user can edit it CanEdit bool `json:"-"` // whether user can delete it CanDelete bool `json:"-"` } // GetCommentReq get comment list page request type GetCommentReq struct { // object id ID string `validate:"required" form:"id"` // user id UserID string `json:"-"` // whether user can edit it CanEdit bool `json:"-"` // whether user can delete it CanDelete bool `json:"-"` } // GetCommentResp comment response type GetCommentResp struct { // comment id CommentID string `json:"comment_id"` // create time CreatedAt int64 `json:"created_at"` // object id ObjectID string `json:"object_id"` // user vote amount VoteCount int `json:"vote_count"` // current user if already vote this comment IsVote bool `json:"is_vote"` // original comment content OriginalText string `json:"original_text"` // parsed comment content ParsedText string `json:"parsed_text"` // user id UserID string `json:"user_id"` // username Username string `json:"username"` // user display name UserDisplayName string `json:"user_display_name"` // user avatar UserAvatar string `json:"user_avatar"` // user status UserStatus string `json:"user_status"` // reply user id ReplyUserID string `json:"reply_user_id"` // reply user username ReplyUsername string `json:"reply_username"` // reply user display name ReplyUserDisplayName string `json:"reply_user_display_name"` // reply comment id ReplyCommentID string `json:"reply_comment_id"` // reply user status ReplyUserStatus string `json:"reply_user_status"` // MemberActions MemberActions []*PermissionMemberAction `json:"member_actions"` } func (r *GetCommentResp) SetFromComment(comment *entity.Comment) { _ = copier.Copy(r, comment) r.CommentID = comment.ID r.CreatedAt = comment.CreatedAt.Unix() r.ReplyUserID = comment.GetReplyUserID() r.ReplyCommentID = comment.GetReplyCommentID() } // GetCommentPersonalWithPageReq get comment list page request type GetCommentPersonalWithPageReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // username Username string `validate:"omitempty,gt=0,lte=100" form:"username"` // user id UserID string `json:"-"` } // GetCommentPersonalWithPageResp comment response type GetCommentPersonalWithPageResp struct { // comment id CommentID string `json:"comment_id"` // create time CreatedAt int64 `json:"created_at"` // object id ObjectID string `json:"object_id"` // question id QuestionID string `json:"question_id"` // answer id AnswerID string `json:"answer_id"` // object type ObjectType string `json:"object_type" enums:"question,answer,tag,comment"` // title Title string `json:"title"` // url title UrlTitle string `json:"url_title"` // content Content string `json:"content"` } ================================================ FILE: internal/schema/config_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // AddConfigReq add config request type AddConfigReq struct { // the config key Key string `validate:"omitempty,gt=0,lte=32" comment:"the config key" json:"key"` // the config value, custom data structures and types Value string `validate:"omitempty,gt=0,lte=128" comment:"the config value, custom data structures and types" json:"value"` } // RemoveConfigReq delete config request type RemoveConfigReq struct { // config id ID int `validate:"required" comment:"config id" json:"id"` } // UpdateConfigReq update config request type UpdateConfigReq struct { // config id ID int `validate:"required" comment:"config id" json:"id"` // the config key Key string `validate:"omitempty,gt=0,lte=32" comment:"the config key" json:"key"` // the config value, custom data structures and types Value string `validate:"omitempty,gt=0,lte=128" comment:"the config value, custom data structures and types" json:"value"` } // GetConfigListReq get config list all request type GetConfigListReq struct { // the config key Key string `validate:"omitempty,gt=0,lte=32" comment:"the config key" form:"key"` // the config value, custom data structures and types Value string `validate:"omitempty,gt=0,lte=128" comment:"the config value, custom data structures and types" form:"value"` } // GetConfigWithPageReq get config list page request type GetConfigWithPageReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // the config key Key string `validate:"omitempty,gt=0,lte=32" comment:"the config key" form:"key"` // the config value, custom data structures and types Value string `validate:"omitempty,gt=0,lte=128" comment:"the config value, custom data structures and types" form:"value"` } // GetConfigResp get config response type GetConfigResp struct { // config id ID int `json:"id"` // the config key Key string `json:"key"` // the config value, custom data structures and types Value string `json:"value"` } ================================================ FILE: internal/schema/connector_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema type ConnectorInfoResp struct { Name string `json:"name"` Icon string `json:"icon"` Link string `json:"link"` } type ConnectorUserInfoResp struct { Name string `json:"name"` Icon string `json:"icon"` Link string `json:"link"` Binding bool `json:"binding"` ExternalID string `json:"external_id"` } ================================================ FILE: internal/schema/dashboard_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import "time" var AppStartTime time.Time const ( DashboardCacheKey = "answer:dashboard" DashboardCacheTime = 60 * time.Minute ) type DashboardInfo struct { QuestionCount int64 `json:"question_count"` ResolvedCount int64 `json:"resolved_count"` ResolvedRate string `json:"resolved_rate"` UnansweredCount int64 `json:"unanswered_count"` UnansweredRate string `json:"unanswered_rate"` AnswerCount int64 `json:"answer_count"` CommentCount int64 `json:"comment_count"` VoteCount int64 `json:"vote_count"` UserCount int64 `json:"user_count"` ReportCount int64 `json:"report_count"` UploadingFiles bool `json:"uploading_files"` SMTP string `json:"smtp"` HTTPS bool `json:"https"` TimeZone string `json:"time_zone"` OccupyingStorageSpace string `json:"occupying_storage_space"` AppStartTime string `json:"app_start_time"` VersionInfo DashboardInfoVersion `json:"version_info"` LoginRequired bool `json:"login_required"` GoVersion string `json:"go_version"` DatabaseVersion string `json:"database_version"` DatabaseSize string `json:"database_size"` } type DashboardInfoVersion struct { Version string `json:"version"` Revision string `json:"revision"` RemoteVersion string `json:"remote_version"` } type RemoteVersion struct { Release struct { Version string `json:"version"` URL string `json:"url"` } `json:"release"` } ================================================ FILE: internal/schema/email_template.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "encoding/json" "github.com/apache/answer/internal/base/constant" ) const ( AccountActivationSourceType EmailSourceType = "account-activation" PasswordResetSourceType EmailSourceType = "password-reset" ConfirmNewEmailSourceType EmailSourceType = "password-reset" UnsubscribeSourceType EmailSourceType = "unsubscribe" BindingSourceType EmailSourceType = "binding" ) type EmailSourceType string type EmailCodeContent struct { SourceType EmailSourceType `json:"source_type"` Email string `json:"e_mail"` UserID string `json:"user_id"` // Used for unsubscribe notification NotificationSources []constant.NotificationSource `json:"notification_source,omitempty"` // Used for third-party login account binding BindingKey string `json:"binding_key,omitempty"` // Skip the validation of the latest code SkipValidationLatestCode bool `json:"skip_validation_latest_code"` } func (r *EmailCodeContent) ToJSONString() string { codeBytes, _ := json.Marshal(r) return string(codeBytes) } func (r *EmailCodeContent) FromJSONString(data string) error { return json.Unmarshal([]byte(data), &r) } type RegisterTemplateData struct { SiteName string RegisterUrl string } type PassResetTemplateData struct { SiteName string PassResetUrl string } type ChangeEmailTemplateData struct { SiteName string ChangeEmailUrl string } type TestTemplateData struct { SiteName string } type NewAnswerTemplateRawData struct { AnswerUserDisplayName string QuestionTitle string QuestionID string AnswerID string AnswerSummary string UnsubscribeCode string } type NewAnswerTemplateData struct { SiteName string DisplayName string QuestionTitle string AnswerUrl string AnswerSummary string UnsubscribeUrl string } type NewInviteAnswerTemplateRawData struct { InviterDisplayName string QuestionTitle string QuestionID string UnsubscribeCode string } type NewInviteAnswerTemplateData struct { SiteName string DisplayName string QuestionTitle string InviteUrl string UnsubscribeUrl string } type NewCommentTemplateRawData struct { CommentUserDisplayName string QuestionTitle string QuestionID string AnswerID string CommentID string CommentSummary string UnsubscribeCode string } type NewCommentTemplateData struct { SiteName string DisplayName string QuestionTitle string CommentUrl string CommentSummary string UnsubscribeUrl string } type NewQuestionTemplateRawData struct { QuestionAuthorUserID string QuestionTitle string QuestionID string UnsubscribeCode string Tags []string TagIDs []string } type NewQuestionTemplateData struct { SiteName string QuestionTitle string QuestionUrl string Tags string UnsubscribeUrl string } ================================================ FILE: internal/schema/err_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema type ErrTypeData struct { ErrType string `json:"err_type"` } var ErrTypeModal = ErrTypeData{ErrType: "modal"} var ErrTypeToast = ErrTypeData{ErrType: "toast"} var ErrTypeAlert = ErrTypeData{ErrType: "alert"} ================================================ FILE: internal/schema/event_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/pkg/uid" ) // EventMsg event message type EventMsg struct { EventType constant.EventType UserID string TriggerObjectID string QuestionID string QuestionUserID string AnswerID string AnswerUserID string CommentID string CommentUserID string ExtraInfo map[string]string } // NewEvent create a new event func NewEvent(e constant.EventType, userID string) *EventMsg { return &EventMsg{ UserID: userID, EventType: e, ExtraInfo: make(map[string]string), } } // QID get question id func (e *EventMsg) QID(questionID, userID string) *EventMsg { if len(questionID) > 0 { e.QuestionID = uid.DeShortID(questionID) } e.QuestionUserID = userID return e } // AID get answer id func (e *EventMsg) AID(answerID, userID string) *EventMsg { if len(answerID) > 0 { e.AnswerID = uid.DeShortID(answerID) } e.AnswerUserID = userID return e } // CID get comment id func (e *EventMsg) CID(comment, userID string) *EventMsg { e.CommentID = comment e.CommentUserID = userID return e } // TID get trigger object id func (e *EventMsg) TID(triggerObjectID string) *EventMsg { if len(triggerObjectID) > 0 { e.TriggerObjectID = uid.DeShortID(triggerObjectID) } return e } // AddExtra add extra info func (e *EventMsg) AddExtra(key, value string) *EventMsg { e.ExtraInfo[key] = value return e } // GetExtra get extra info func (e *EventMsg) GetExtra(key string) string { if v, ok := e.ExtraInfo[key]; ok { return v } return "" } // GetObjectID get object id func (e *EventMsg) GetObjectID() string { if len(e.TriggerObjectID) > 0 { return e.TriggerObjectID } if len(e.CommentID) > 0 { return e.CommentID } if len(e.AnswerID) > 0 { return e.AnswerID } return e.QuestionID } ================================================ FILE: internal/schema/follow_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // FollowReq follow object request type FollowReq struct { // object id ObjectID string `validate:"required" form:"object_id" json:"object_id"` // is cancel IsCancel bool `validate:"omitempty" form:"is_cancel" json:"is_cancel"` } // FollowResp response object's follows and current user follow status type FollowResp struct { // the followers of object Follows int `json:"follows"` // if user is followed object will be true,otherwise false IsFollowed bool `json:"is_followed"` } type FollowDTO struct { // object TagID ObjectID string // is cancel IsCancel bool // user TagID UserID string } // UpdateFollowTagsReq update user follow tags type UpdateFollowTagsReq struct { // tag slug name list SlugNameList []string `json:"slug_name_list"` // user id UserID string `json:"-"` } ================================================ FILE: internal/schema/forbidden_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema const ( ForbiddenReasonTypeInactive = "inactive" ForbiddenReasonTypeURLExpired = "url_expired" ForbiddenReasonTypeUserSuspended = "suspended" ) // ForbiddenResp forbidden response type ForbiddenResp struct { // forbidden reason type Type string `json:"type" enums:"inactive,url_expired"` } ================================================ FILE: internal/schema/mcp_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "strings" "github.com/apache/answer/pkg/converter" "github.com/mark3labs/mcp-go/mcp" ) const ( MCPSearchCondKeyword = "keyword" MCPSearchCondUsername = "username" MCPSearchCondScore = "score" MCPSearchCondTag = "tag" MCPSearchCondPage = "page" MCPSearchCondPageSize = "page_size" MCPSearchCondTagName = "tag_name" MCPSearchCondQuestionID = "question_id" MCPSearchCondObjectID = "object_id" ) type MCPSearchCond struct { Keyword string `json:"keyword"` Username string `json:"username"` Score int `json:"score"` Tags []string `json:"tags"` QuestionID string `json:"question_id"` } type MCPSearchQuestionDetail struct { QuestionID string `json:"question_id"` } type MCPSearchCommentCond struct { ObjectID string `json:"object_id"` } type MCPSearchTagCond struct { TagName string `json:"tag_name"` } type MCPSearchUserCond struct { Username string `json:"username"` } type MCPSearchQuestionInfoResp struct { QuestionID string `json:"question_id"` Title string `json:"title"` Content string `json:"content"` Link string `json:"link"` } type MCPSearchAnswerInfoResp struct { QuestionID string `json:"question_id"` QuestionTitle string `json:"question_title,omitempty"` AnswerID string `json:"answer_id"` AnswerContent string `json:"answer_content"` Link string `json:"link"` } type MCPSearchTagResp struct { TagName string `json:"tag_name"` DisplayName string `json:"display_name"` Description string `json:"description"` Link string `json:"link"` } type MCPSearchUserInfoResp struct { Username string `json:"username"` DisplayName string `json:"display_name"` Avatar string `json:"avatar"` Link string `json:"link"` } type MCPSearchCommentInfoResp struct { CommentID string `json:"comment_id"` Content string `json:"content"` ObjectID string `json:"object_id"` Link string `json:"link"` } func NewMCPSearchCond(request mcp.CallToolRequest) *MCPSearchCond { cond := &MCPSearchCond{} if keyword, ok := getRequestValue(request, MCPSearchCondKeyword); ok { cond.Keyword = keyword } if username, ok := getRequestValue(request, MCPSearchCondUsername); ok { cond.Username = username } if score, ok := getRequestNumber(request, MCPSearchCondScore); ok { cond.Score = score } if tag, ok := getRequestValue(request, MCPSearchCondTag); ok { cond.Tags = strings.Split(tag, ",") } if questionID, ok := getRequestValue(request, MCPSearchCondQuestionID); ok { cond.QuestionID = questionID } return cond } func NewMCPSearchAnswerCond(request mcp.CallToolRequest) *MCPSearchCond { cond := &MCPSearchCond{} if questionID, ok := getRequestValue(request, MCPSearchCondQuestionID); ok { cond.QuestionID = questionID } return cond } func NewMCPSearchQuestionDetail(request mcp.CallToolRequest) *MCPSearchQuestionDetail { cond := &MCPSearchQuestionDetail{} if questionID, ok := getRequestValue(request, MCPSearchCondQuestionID); ok { cond.QuestionID = questionID } return cond } func NewMCPSearchCommentCond(request mcp.CallToolRequest) *MCPSearchCommentCond { cond := &MCPSearchCommentCond{} if keyword, ok := getRequestValue(request, MCPSearchCondObjectID); ok { cond.ObjectID = keyword } return cond } func NewMCPSearchTagCond(request mcp.CallToolRequest) *MCPSearchTagCond { cond := &MCPSearchTagCond{} if tagName, ok := getRequestValue(request, MCPSearchCondTagName); ok { cond.TagName = tagName } return cond } func NewMCPSearchUserCond(request mcp.CallToolRequest) *MCPSearchUserCond { cond := &MCPSearchUserCond{} if username, ok := getRequestValue(request, MCPSearchCondUsername); ok { cond.Username = username } return cond } func getRequestValue(request mcp.CallToolRequest, key string) (string, bool) { value, ok := request.GetArguments()[key].(string) if !ok { return "", false } return value, true } func getRequestNumber(request mcp.CallToolRequest, key string) (int, bool) { value, ok := request.GetArguments()[key].(float64) if !ok { return 0, false } return int(value), true } func (cond *MCPSearchCond) ToQueryString() string { var queryBuilder strings.Builder if len(cond.Keyword) > 0 { queryBuilder.WriteString(cond.Keyword) } if len(cond.Username) > 0 { queryBuilder.WriteString(" user:" + cond.Username) } if cond.Score > 0 { queryBuilder.WriteString(" score:" + converter.IntToString(int64(cond.Score))) } if len(cond.Tags) > 0 { for _, tag := range cond.Tags { queryBuilder.WriteString(" [" + tag + "]") } } return strings.TrimSpace(queryBuilder.String()) } ================================================ FILE: internal/schema/mcp_tools/mcp_tools.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package mcp_tools import ( "github.com/apache/answer/internal/schema" "github.com/mark3labs/mcp-go/mcp" ) var ( MCPToolsList = []mcp.Tool{ NewQuestionsTool(), NewAnswersTool(), NewCommentsTool(), NewTagsTool(), NewTagDetailTool(), NewUserTool(), } ) func NewQuestionsTool() mcp.Tool { listFilesTool := mcp.NewTool("get_questions", mcp.WithDescription("Searching for questions that already existed in the system. After the search, you can use the get_answers_by_question_id tool to get answers for the questions."), mcp.WithString(schema.MCPSearchCondKeyword, mcp.Description("Keyword to search for questions. Multiple keywords separated by spaces"), ), mcp.WithString(schema.MCPSearchCondUsername, mcp.Description("Search for questions that contain only those created by the specified user"), ), mcp.WithString(schema.MCPSearchCondTag, mcp.Description("Filter by tag (semicolon separated for multiple tags)"), ), mcp.WithString(schema.MCPSearchCondScore, mcp.Description("Minimum score that the question must have"), ), ) return listFilesTool } func NewAnswersTool() mcp.Tool { listFilesTool := mcp.NewTool("get_answers_by_question_id", mcp.WithDescription("Search for all answers corresponding to the question ID. The question ID is provided by get_questions tool."), mcp.WithString(schema.MCPSearchCondQuestionID, mcp.Description("The ID of the question to which the answer belongs. The question ID is provided by get_questions tool."), ), ) return listFilesTool } func NewCommentsTool() mcp.Tool { listFilesTool := mcp.NewTool("get_comments", mcp.WithDescription("Searching for comments that already existed in the system"), mcp.WithString(schema.MCPSearchCondObjectID, mcp.Description("Queries comments on an object, either a question or an answer. object_id is the id of the object."), ), ) return listFilesTool } func NewTagsTool() mcp.Tool { listFilesTool := mcp.NewTool("get_tags", mcp.WithDescription("Searching for tags that already existed in the system"), mcp.WithString(schema.MCPSearchCondTagName, mcp.Description("Tag name"), ), ) return listFilesTool } func NewTagDetailTool() mcp.Tool { listFilesTool := mcp.NewTool("get_tag_detail", mcp.WithDescription("Get detailed information about a specific tag"), mcp.WithString(schema.MCPSearchCondTagName, mcp.Description("Tag name"), ), ) return listFilesTool } func NewUserTool() mcp.Tool { listFilesTool := mcp.NewTool("get_user", mcp.WithDescription("Searching for users that already existed in the system"), mcp.WithString(schema.MCPSearchCondUsername, mcp.Description("Username"), ), ) return listFilesTool } ================================================ FILE: internal/schema/meta_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import "slices" type UpdateReactionReq struct { ObjectID string `validate:"required" json:"object_id"` Emoji string `validate:"required,oneof=heart smile frown" json:"emoji"` Reaction string `validate:"required,oneof=activate deactivate" json:"reaction"` UserID string `json:"-"` } type GetReactionReq struct { ObjectID string `validate:"required" form:"object_id"` UserID string `json:"-"` } // ReactionsSummaryMeta reactions summary meta type ReactionsSummaryMeta struct { Reactions []*ReactionSummaryMeta `json:"reactions"` } // ReactionSummaryMeta reaction summary meta type ReactionSummaryMeta struct { Emoji string `json:"emoji"` UserIDs []string `json:"user_ids"` } // AddReactionSummary add user operation to reaction summary func (r *ReactionsSummaryMeta) AddReactionSummary(emoji, userID string) { for _, reaction := range r.Reactions { if reaction.Emoji != emoji { continue } exist := slices.Contains(reaction.UserIDs, userID) if !exist { reaction.UserIDs = append(reaction.UserIDs, userID) } return } r.Reactions = append(r.Reactions, &ReactionSummaryMeta{ Emoji: emoji, UserIDs: []string{userID}, }) } // RemoveReactionSummary remove user operation from reaction summary func (r *ReactionsSummaryMeta) RemoveReactionSummary(emoji, userID string) { updatedReactions := make([]*ReactionSummaryMeta, 0) for _, reaction := range r.Reactions { if reaction.Emoji != emoji && len(reaction.UserIDs) > 0 { updatedReactions = append(updatedReactions, reaction) continue } updatedUserIDs := make([]string, 0, len(r.Reactions)) for _, id := range reaction.UserIDs { if id != userID { updatedUserIDs = append(updatedUserIDs, id) } } if len(updatedUserIDs) > 0 { reaction.UserIDs = updatedUserIDs updatedReactions = append(updatedReactions, reaction) } } r.Reactions = updatedReactions } // CheckUserInReactionSummary check user's operation if in reaction summary func (r *ReactionsSummaryMeta) CheckUserInReactionSummary(emoji, userID string) bool { for _, reaction := range r.Reactions { if reaction.Emoji != emoji { continue } if slices.Contains(reaction.UserIDs, userID) { return true } } return false } // GetReactionByObjectIdResp get reaction by object id response type GetReactionByObjectIdResp struct { ReactionSummary []*ReactionRespItem `json:"reaction_summary"` } // ReactionRespItem reaction response item type ReactionRespItem struct { // Emoji is the reaction emoji Emoji string `json:"emoji"` // Count is the number of users who reacted Count int `json:"count"` // Tooltip is the user's name who reacted Tooltip string `json:"tooltip"` // IsActive is if current user has reacted IsActive bool `json:"is_active"` } ================================================ FILE: internal/schema/new_question_queue_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "github.com/apache/answer/internal/entity" "github.com/apache/answer/pkg/uid" ) type ExternalNotificationMsg struct { ReceiverUserID string `json:"receiver_user_id"` ReceiverEmail string `json:"receiver_email"` ReceiverLang string `json:"receiver_lang"` NewAnswerTemplateRawData *NewAnswerTemplateRawData `json:"new_answer_template_raw_data,omitempty"` NewInviteAnswerTemplateRawData *NewInviteAnswerTemplateRawData `json:"new_invite_answer_template_raw_data,omitempty"` NewCommentTemplateRawData *NewCommentTemplateRawData `json:"new_comment_template_raw_data,omitempty"` NewQuestionTemplateRawData *NewQuestionTemplateRawData `json:"new_question_template_raw_data,omitempty"` } func CreateNewQuestionNotificationMsg( questionID, questionTitle, questionAuthorUserID string, tags []*entity.Tag) *ExternalNotificationMsg { questionID = uid.DeShortID(questionID) msg := &ExternalNotificationMsg{ NewQuestionTemplateRawData: &NewQuestionTemplateRawData{ QuestionAuthorUserID: questionAuthorUserID, QuestionID: questionID, QuestionTitle: questionTitle, }, } for _, tag := range tags { msg.NewQuestionTemplateRawData.Tags = append(msg.NewQuestionTemplateRawData.Tags, tag.SlugName) msg.NewQuestionTemplateRawData.TagIDs = append(msg.NewQuestionTemplateRawData.TagIDs, tag.ID) } return msg } ================================================ FILE: internal/schema/notification_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "encoding/json" "sort" "github.com/apache/answer/internal/entity" ) const ( NotificationTypeInbox = 1 NotificationTypeAchievement = 2 NotificationNotRead = 1 NotificationRead = 2 NotificationStatusNormal = 1 NotificationStatusDelete = 10 NotificationInboxTypeAll = 0 NotificationInboxTypePosts = 1 NotificationInboxTypeVotes = 2 NotificationInboxTypeInvites = 3 ) var NotificationType = map[string]int{ "inbox": NotificationTypeInbox, "achievement": NotificationTypeAchievement, } var NotificationInboxType = map[string]int{ "all": NotificationInboxTypeAll, "posts": NotificationInboxTypePosts, "invites": NotificationInboxTypeInvites, "votes": NotificationInboxTypeVotes, } type NotificationContent struct { ID string `json:"id"` TriggerUserID string `json:"-"` // show userid ReceiverUserID string `json:"-"` // receiver userid UserInfo *UserBasicInfo `json:"user_info,omitempty"` ObjectInfo ObjectInfo `json:"object_info"` Rank int `json:"rank"` NotificationAction string `json:"notification_action,omitempty"` Type int `json:"-"` // 1 inbox 2 achievement IsRead bool `json:"is_read"` UpdateTime int64 `json:"update_time"` } type GetRedDot struct { CanReviewQuestion bool `json:"-"` CanReviewAnswer bool `json:"-"` CanReviewTag bool `json:"-"` UserID string `json:"-"` IsAdmin bool `json:"-"` } // NotificationMsg notification message type NotificationMsg struct { // trigger notification user id TriggerUserID string // receive notification user id ReceiverUserID string // type 1 inbox 2 achievement Type int // notification title Title string // notification object ObjectID string // notification object type ObjectType string // notification action NotificationAction string // if true no need to send notification to all followers NoNeedPushAllFollow bool // extra info ExtraInfo map[string]string } type ObjectInfo struct { Title string `json:"title"` ObjectID string `json:"object_id"` ObjectMap map[string]string `json:"object_map"` ObjectType string `json:"object_type"` } type RedDot struct { Inbox int64 `json:"inbox"` Achievement int64 `json:"achievement"` Revision int64 `json:"revision"` CanRevision bool `json:"can_revision"` BadgeAward *RedDotBadgeAward `json:"badge_award"` } type RedDotBadgeAward struct { NotificationID string `json:"notification_id"` BadgeID string `json:"badge_id"` Name string `json:"name"` Icon string `json:"icon"` Level entity.BadgeLevel `json:"level"` } type RedDotBadgeAwardCache struct { BadgeAwardList map[string]*RedDotBadgeAward `json:"badge_award_list"` } // NewRedDotBadgeAwardCache new red dot badge award cache func NewRedDotBadgeAwardCache() *RedDotBadgeAwardCache { return &RedDotBadgeAwardCache{ BadgeAwardList: make(map[string]*RedDotBadgeAward), } } // GetBadgeAward get badge award func (r *RedDotBadgeAwardCache) GetBadgeAward() *RedDotBadgeAward { if len(r.BadgeAwardList) == 0 { return nil } var ids []string for _, v := range r.BadgeAwardList { ids = append(ids, v.NotificationID) } sort.Strings(ids) return r.BadgeAwardList[ids[0]] } // FromJSON from json func (r *RedDotBadgeAwardCache) FromJSON(data string) { _ = json.Unmarshal([]byte(data), r) } // ToJSON to json func (r *RedDotBadgeAwardCache) ToJSON() string { data, _ := json.Marshal(r) return string(data) } // AddBadgeAward add badge award func (r *RedDotBadgeAwardCache) AddBadgeAward(badgeAward *RedDotBadgeAward) { if r.BadgeAwardList == nil { r.BadgeAwardList = make(map[string]*RedDotBadgeAward) } r.BadgeAwardList[badgeAward.NotificationID] = badgeAward } // RemoveBadgeAward remove badge award func (r *RedDotBadgeAwardCache) RemoveBadgeAward(notificationID string) { if r.BadgeAwardList == nil { return } delete(r.BadgeAwardList, notificationID) } type NotificationSearch struct { Page int `json:"page" form:"page"` // Query number of pages PageSize int `json:"page_size" form:"page_size"` // Search page size Type int `json:"-" form:"-"` TypeStr string `json:"type" form:"type"` // inbox achievement InboxTypeStr string `json:"inbox_type" form:"inbox_type"` // inbox achievement InboxType int `json:"-" form:"-"` // inbox achievement UserID string `json:"-"` } type NotificationClearRequest struct { NotificationType string `validate:"required,oneof=inbox achievement" json:"type"` UserID string `json:"-"` CanReviewQuestion bool `json:"-"` CanReviewAnswer bool `json:"-"` CanReviewTag bool `json:"-"` } type NotificationClearIDRequest struct { UserID string `json:"-"` ID string `json:"id" form:"id"` } ================================================ FILE: internal/schema/permission.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "strings" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/segmentfault/pacman/i18n" ) // PermissionTrTplData template data as for translate permission message type PermissionTrTplData struct { Rank int } // PermissionMemberAction permission member action type PermissionMemberAction struct { Action string `json:"action"` Name string `json:"name"` Type string `json:"type"` } // GetPermissionReq get permission request type GetPermissionReq struct { Action string `form:"action"` Actions []string `validate:"omitempty" form:"actions"` } func (r *GetPermissionReq) Check() (errField []*validator.FormErrorField, err error) { if len(r.Action) > 0 { r.Actions = strings.Split(r.Action, ",") } return nil, nil } // GetPermissionResp get permission response type GetPermissionResp struct { HasPermission bool `json:"has_permission"` // only not allow, will return this tip NoPermissionTip string `json:"no_permission_tip"` } func (r *GetPermissionResp) TrTip(lang i18n.Language, requireRank int) { if r.HasPermission { return } if requireRank <= 0 { r.NoPermissionTip = translator.Tr(lang, reason.RankFailToMeetTheCondition) } else { r.NoPermissionTip = translator.TrWithData( lang, reason.NoEnoughRankToOperate, &PermissionTrTplData{Rank: requireRank}) } } ================================================ FILE: internal/schema/plugin_admin_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" ) const ( PluginStatusActive PluginStatus = "active" PluginStatusInactive PluginStatus = "inactive" ) type PluginStatus string type GetPluginListReq struct { Status PluginStatus `form:"status"` HaveConfig bool `form:"have_config"` } type GetPluginListResp struct { Name string `json:"name"` SlugName string `json:"slug_name"` Description string `json:"description"` Version string `json:"version"` Enabled bool `json:"enabled"` HaveConfig bool `json:"have_config"` Link string `json:"link"` } type GetAllPluginStatusResp struct { SlugName string `json:"slug_name"` Enabled bool `json:"enabled"` } type UpdatePluginStatusReq struct { PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"` Enabled bool `json:"enabled"` } type GetPluginConfigReq struct { PluginSlugName string `validate:"required,gt=1,lte=100" form:"plugin_slug_name"` } type GetPluginConfigResp struct { Name string `json:"name"` SlugName string `json:"slug_name"` Description string `json:"description"` Version string `json:"version"` ConfigFields []ConfigField `json:"config_fields"` } func (g *GetPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.ConfigField) { for _, field := range fields { configField := ConfigField{ Name: field.Name, Type: string(field.Type), Title: field.Title.Translate(ctx), Description: field.Description.Translate(ctx), Required: field.Required, Value: field.Value, UIOptions: ConfigFieldUIOptions{ Rows: field.UIOptions.Rows, InputType: string(field.UIOptions.InputType), Variant: field.UIOptions.Variant, ClassName: field.UIOptions.ClassName, FieldClassName: field.UIOptions.FieldClassName, }, } configField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx) configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx) configField.UIOptions.Text = field.UIOptions.Text.Translate(ctx) if field.UIOptions.Action != nil { uiOptionAction := &UIOptionAction{ Url: field.UIOptions.Action.Url, Method: field.UIOptions.Action.Method, } if field.UIOptions.Action.Loading != nil { uiOptionAction.Loading = &LoadingAction{ Text: field.UIOptions.Action.Loading.Text.Translate(ctx), State: string(field.UIOptions.Action.Loading.State), } } if field.UIOptions.Action.OnComplete != nil { uiOptionAction.OnCompleteAction = &OnCompleteAction{ ToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage, RefreshFormConfig: field.UIOptions.Action.OnComplete.RefreshFormConfig, } } configField.UIOptions.Action = uiOptionAction } for _, option := range field.Options { configField.Options = append(configField.Options, ConfigFieldOption{ Label: option.Label.Translate(ctx), Value: option.Value, }) } g.ConfigFields = append(g.ConfigFields, configField) } } type ConfigField struct { Name string `json:"name"` Type string `json:"type"` Title string `json:"title"` Description string `json:"description"` Required bool `json:"required"` Value any `json:"value"` UIOptions ConfigFieldUIOptions `json:"ui_options"` Options []ConfigFieldOption `json:"options,omitempty"` } type ConfigFieldUIOptions struct { Placeholder string `json:"placeholder,omitempty"` Rows string `json:"rows,omitempty"` InputType string `json:"input_type,omitempty"` Label string `json:"label,omitempty"` Action *UIOptionAction `json:"action,omitempty"` Variant string `json:"variant,omitempty"` Text string `json:"text,omitempty"` ClassName string `json:"class_name,omitempty"` FieldClassName string `json:"field_class_name,omitempty"` } type ConfigFieldOption struct { Label string `json:"label"` Value string `json:"value"` } type UIOptionAction struct { Url string `json:"url"` Method string `json:"method,omitempty"` Loading *LoadingAction `json:"loading,omitempty"` OnCompleteAction *OnCompleteAction `json:"on_complete,omitempty"` } type LoadingAction struct { Text string `json:"text"` State string `json:"state"` } type OnCompleteAction struct { ToastReturnMessage bool `json:"toast_return_message"` RefreshFormConfig bool `json:"refresh_form_config"` } type UpdatePluginConfigReq struct { PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"` ConfigFields map[string]any `json:"config_fields"` } ================================================ FILE: internal/schema/plugin_user_center.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema type UserCenterAgentResp struct { Enabled bool `json:"enabled"` AgentInfo *AgentInfo `json:"agent_info"` } type AgentInfo struct { Name string `json:"name"` DisplayName string `json:"display_name"` Icon string `json:"icon"` Url string `json:"url"` LoginRedirectURL string `json:"login_redirect_url"` SignUpRedirectURL string `json:"sign_up_redirect_url"` ControlCenterItems []*ControlCenter `json:"control_center"` EnabledOriginalUserSystem bool `json:"enabled_original_user_system"` } type ControlCenter struct { Name string `json:"name"` Label string `json:"label"` Url string `json:"url"` } type UserCenterPersonalBranding struct { Enabled bool `json:"enabled"` PersonalBranding []*PersonalBranding `json:"personal_branding"` } type PersonalBranding struct { Icon string `json:"icon"` Name string `json:"name"` Label string `json:"label"` Url string `json:"url"` } ================================================ FILE: internal/schema/plugin_user_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" ) type GetUserPluginListResp struct { Name string `json:"name"` SlugName string `json:"slug_name"` } type UpdateUserPluginReq struct { PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"` UserID string `json:"-"` } type GetUserPluginConfigReq struct { PluginSlugName string `validate:"required,gt=1,lte=100" form:"plugin_slug_name"` UserID string `json:"-"` } type GetUserPluginConfigResp struct { Name string `json:"name"` SlugName string `json:"slug_name"` ConfigFields []*ConfigField `json:"config_fields"` } func (g *GetUserPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.ConfigField) { for _, field := range fields { configField := &ConfigField{ Name: field.Name, Type: string(field.Type), Title: field.Title.Translate(ctx), Description: field.Description.Translate(ctx), Required: field.Required, Value: field.Value, UIOptions: ConfigFieldUIOptions{ Rows: field.UIOptions.Rows, InputType: string(field.UIOptions.InputType), Variant: field.UIOptions.Variant, ClassName: field.UIOptions.ClassName, FieldClassName: field.UIOptions.FieldClassName, }, } configField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx) configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx) configField.UIOptions.Text = field.UIOptions.Text.Translate(ctx) if field.UIOptions.Action != nil { uiOptionAction := &UIOptionAction{ Url: field.UIOptions.Action.Url, Method: field.UIOptions.Action.Method, } if field.UIOptions.Action.Loading != nil { uiOptionAction.Loading = &LoadingAction{ Text: field.UIOptions.Action.Loading.Text.Translate(ctx), State: string(field.UIOptions.Action.Loading.State), } } if field.UIOptions.Action.OnComplete != nil { uiOptionAction.OnCompleteAction = &OnCompleteAction{ ToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage, RefreshFormConfig: field.UIOptions.Action.OnComplete.RefreshFormConfig, } } configField.UIOptions.Action = uiOptionAction } for _, option := range field.Options { configField.Options = append(configField.Options, ConfigFieldOption{ Label: option.Label.Translate(ctx), Value: option.Value, }) } g.ConfigFields = append(g.ConfigFields, configField) } } type UpdateUserPluginConfigReq struct { PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"` ConfigFields map[string]any `json:"config_fields"` UserID string `json:"-"` } ================================================ FILE: internal/schema/question_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "strings" "time" "github.com/apache/answer/internal/base/reason" "github.com/segmentfault/pacman/errors" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/uid" ) const ( QuestionOperationPin = "pin" QuestionOperationUnPin = "unpin" QuestionOperationHide = "hide" QuestionOperationShow = "show" ) // RemoveQuestionReq delete question request type RemoveQuestionReq struct { // question id ID string `validate:"required" json:"id"` UserID string `json:"-" ` // user_id IsAdmin bool `json:"-"` CaptchaID string `json:"captcha_id"` // captcha_id CaptchaCode string `json:"captcha_code"` } type CloseQuestionReq struct { ID string `validate:"required" json:"id"` CloseType int `json:"close_type"` // close_type CloseMsg string `json:"close_msg"` // close_type UserID string `json:"-"` // user_id } type OperationQuestionReq struct { ID string `validate:"required" json:"id"` Operation string `json:"operation"` // operation [pin unpin hide show] UserID string `json:"-"` // user_id CanPin bool `json:"-"` CanList bool `json:"-"` } type CloseQuestionMeta struct { CloseType int `json:"close_type"` CloseMsg string `json:"close_msg"` } // ReopenQuestionReq reopen question request type ReopenQuestionReq struct { QuestionID string `json:"question_id"` UserID string `json:"-"` } type QuestionAdd struct { // question title Title string `validate:"required,notblank,gte=6,lte=150" json:"title"` // content Content string `validate:"gte=0,lte=65535" json:"content"` // html HTML string `json:"-"` // tags Tags []*TagItem `validate:"dive" json:"tags"` // user id UserID string `json:"-"` QuestionPermission CaptchaID string `json:"captcha_id"` // captcha_id CaptchaCode string `json:"captcha_code"` IP string `json:"-"` UserAgent string `json:"-"` } func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err error) { req.HTML = converter.Markdown2HTML(req.Content) for _, tag := range req.Tags { if len(tag.OriginalText) > 0 { tag.ParsedText = converter.Markdown2HTML(tag.OriginalText) } } return nil, nil } type QuestionAddByAnswer struct { // question title Title string `validate:"required,notblank,gte=6,lte=150" json:"title"` // content Content string `validate:"gte=0,lte=65535" json:"content"` // html HTML string `json:"-"` AnswerContent string `validate:"required,notblank,gte=6,lte=65535" json:"answer_content"` AnswerHTML string `json:"-"` // tags Tags []*TagItem `validate:"dive" json:"tags"` // user id UserID string `json:"-"` MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"` QuestionPermission CaptchaID string `json:"captcha_id"` // captcha_id CaptchaCode string `json:"captcha_code"` IP string `json:"-"` UserAgent string `json:"-"` } func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField, err error) { req.HTML = converter.Markdown2HTML(req.Content) req.AnswerHTML = converter.Markdown2HTML(req.AnswerContent) for _, tag := range req.Tags { if len(tag.OriginalText) > 0 { tag.ParsedText = converter.Markdown2HTML(tag.OriginalText) } } if req.AnswerHTML == "" { errFields = append(errFields, &validator.FormErrorField{ ErrorField: "answer_content", ErrorMsg: reason.AnswerContentCannotEmpty, }) return errFields, errors.BadRequest(reason.QuestionContentCannotEmpty) } return nil, nil } type QuestionPermission struct { // whether user can add it CanAdd bool `json:"-"` // whether user can edit it CanEdit bool `json:"-"` // whether user can delete it CanDelete bool `json:"-"` // whether user can close it CanClose bool `json:"-"` // whether user can reopen it CanReopen bool `json:"-"` // whether user can pin it CanPin bool `json:"-"` CanUnPin bool `json:"-"` // whether user can hide it CanHide bool `json:"-"` CanShow bool `json:"-"` // whether user can use reserved it CanUseReservedTag bool `json:"-"` // whether user can invite other user to answer this question CanInviteOtherToAnswer bool `json:"-"` CanAddTag bool `json:"-"` CanRecover bool `json:"-"` } type CheckCanQuestionUpdate struct { // question id ID string `validate:"required" form:"id"` // user id UserID string `json:"-"` IsAdmin bool `json:"-"` } type QuestionUpdate struct { // question id ID string `validate:"required" json:"id"` // question title Title string `validate:"required,notblank,gte=6,lte=150" json:"title"` // content Content string `validate:"gte=0,lte=65535" json:"content"` // html HTML string `json:"-"` InviteUser []string `validate:"omitempty" json:"invite_user"` // tags Tags []*TagItem `validate:"dive" json:"tags"` // edit summary EditSummary string `validate:"omitempty" json:"edit_summary"` // user id UserID string `json:"-"` NoNeedReview bool `json:"-"` QuestionPermission CaptchaID string `json:"captcha_id"` // captcha_id CaptchaCode string `json:"captcha_code"` } type QuestionRecoverReq struct { QuestionID string `validate:"required" json:"question_id"` UserID string `json:"-"` } type QuestionUpdateInviteUser struct { ID string `validate:"required" json:"id"` InviteUser []string `validate:"omitempty" json:"invite_user"` UserID string `json:"-"` QuestionPermission CaptchaID string `json:"captcha_id"` // captcha_id CaptchaCode string `json:"captcha_code"` } func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) { req.HTML = converter.Markdown2HTML(req.Content) return nil, nil } type QuestionBaseInfo struct { ID string `json:"id" ` Title string `json:"title"` UrlTitle string `json:"url_title"` ViewCount int `json:"view_count"` AnswerCount int `json:"answer_count"` CollectionCount int `json:"collection_count"` FollowCount int `json:"follow_count"` Status string `json:"status"` AcceptedAnswer bool `json:"accepted_answer"` } type QuestionInfoResp struct { ID string `json:"id" ` Title string `json:"title"` UrlTitle string `json:"url_title"` Content string `json:"content"` HTML string `json:"html"` Description string `json:"description"` Tags []*TagResp `json:"tags"` ViewCount int `json:"view_count"` UniqueViewCount int `json:"unique_view_count"` VoteCount int `json:"vote_count"` AnswerCount int `json:"answer_count"` CollectionCount int `json:"collection_count"` FollowCount int `json:"follow_count"` AcceptedAnswerID string `json:"accepted_answer_id"` LastAnswerID string `json:"last_answer_id"` CreateTime int64 `json:"create_time"` UpdateTime int64 `json:"-"` PostUpdateTime int64 `json:"update_time"` QuestionUpdateTime int64 `json:"edit_time"` Pin int `json:"pin"` Show int `json:"show"` Status int `json:"status"` Operation *Operation `json:"operation,omitempty"` UserID string `json:"-"` LastEditUserID string `json:"-"` LastAnsweredUserID string `json:"-"` UserInfo *UserBasicInfo `json:"user_info"` UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` LastAnsweredUserInfo *UserBasicInfo `json:"last_answered_user_info,omitempty"` Answered bool `json:"answered"` FirstAnswerId string `json:"first_answer_id"` Collected bool `json:"collected"` VoteStatus string `json:"vote_status"` IsFollowed bool `json:"is_followed"` // MemberActions MemberActions []*PermissionMemberAction `json:"member_actions"` ExtendsActions []*PermissionMemberAction `json:"extends_actions"` } // UpdateQuestionResp update question resp type UpdateQuestionResp struct { UrlTitle string `json:"url_title"` WaitForReview bool `json:"wait_for_review"` } type AdminQuestionInfo struct { ID string `json:"id"` Title string `json:"title"` VoteCount int `json:"vote_count"` Show int `json:"show"` Pin int `json:"pin"` AnswerCount int `json:"answer_count"` AcceptedAnswerID string `json:"accepted_answer_id"` CreateTime int64 `json:"create_time"` UpdateTime int64 `json:"update_time"` EditTime int64 `json:"edit_time"` UserID string `json:"-" ` UserInfo *UserBasicInfo `json:"user_info"` } type OperationLevel string const ( OperationLevelInfo OperationLevel = "info" OperationLevelDanger OperationLevel = "danger" OperationLevelWarning OperationLevel = "warning" OperationLevelSecondary OperationLevel = "secondary" ) type Operation struct { Type string `json:"type"` Description string `json:"description"` Msg string `json:"msg"` Time int64 `json:"time"` Level OperationLevel `json:"level"` } type GetCloseTypeResp struct { // report name Name string `json:"name"` // report description Description string `json:"description"` // report source Source string `json:"source"` // report type Type int `json:"type"` // is have content HaveContent bool `json:"have_content"` // content type ContentType string `json:"content_type"` } type UserAnswerInfo struct { AnswerID string `json:"answer_id"` QuestionID string `json:"question_id"` Accepted int `json:"accepted"` VoteCount int `json:"vote_count"` CreateTime int `json:"create_time"` UpdateTime int `json:"update_time"` QuestionInfo struct { Title string `json:"title"` UrlTitle string `json:"url_title"` Tags []any `json:"tags"` } `json:"question_info"` } type UserQuestionInfo struct { ID string `json:"question_id"` Title string `json:"title"` UrlTitle string `json:"url_title"` VoteCount int `json:"vote_count"` Tags []any `json:"tags"` ViewCount int `json:"view_count"` AnswerCount int `json:"answer_count"` CollectionCount int `json:"collection_count"` CreatedAt int64 `json:"created_at"` AcceptedAnswerID string `json:"accepted_answer_id"` Status string `json:"status"` } const ( QuestionOrderCondNewest = "newest" QuestionOrderCondActive = "active" QuestionOrderCondHot = "hot" QuestionOrderCondScore = "score" QuestionOrderCondUnanswered = "unanswered" QuestionOrderCondRecommend = "recommend" QuestionOrderCondFrequent = "frequent" // HotInDays limit max days of the hottest question HotInDays = 90 ) // QuestionPageReq query questions page type QuestionPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered recommend frequent" form:"order"` Tag string `validate:"omitempty,gt=0,lte=100" form:"tag"` Username string `validate:"omitempty,gt=0,lte=100" form:"username"` InDays int `validate:"omitempty,min=1" form:"in_days"` LoginUserID string `json:"-"` UserIDBeSearched string `json:"-"` TagID string `json:"-"` ShowPending bool `json:"-"` } const ( QuestionPageRespOperationTypeAsked = "asked" QuestionPageRespOperationTypeAnswered = "answered" QuestionPageRespOperationTypeModified = "modified" ) type QuestionPageResp struct { ID string `json:"id" ` CreatedAt int64 `json:"created_at"` Title string `json:"title"` UrlTitle string `json:"url_title"` Description string `json:"description"` Pin int `json:"pin"` // 1: unpin, 2: pin Show int `json:"show"` // 0: show, 1: hide Status int `json:"status"` Tags []*TagResp `json:"tags"` // question statistical information ViewCount int `json:"view_count"` UniqueViewCount int `json:"unique_view_count"` VoteCount int `json:"vote_count"` AnswerCount int `json:"answer_count"` CollectionCount int `json:"collection_count"` FollowCount int `json:"follow_count"` // answer information AcceptedAnswerID string `json:"accepted_answer_id"` LastAnswerID string `json:"last_answer_id"` LastAnsweredUserID string `json:"-"` LastAnsweredAt time.Time `json:"-"` // operator information OperatedAt int64 `json:"operated_at"` Operator *QuestionPageRespOperator `json:"operator"` OperationType string `json:"operation_type"` } type QuestionPageRespOperator struct { ID string `json:"id"` Username string `json:"username"` Rank int `json:"rank"` DisplayName string `json:"display_name"` Status string `json:"status"` Avatar string `json:"avatar"` } type AdminQuestionPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` StatusCond string `validate:"omitempty,oneof=normal closed deleted pending" form:"status"` Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` Status int `json:"-"` LoginUserID string `json:"-"` } func (req *AdminQuestionPageReq) Check() (errField []*validator.FormErrorField, err error) { status, ok := entity.AdminQuestionSearchStatus[req.StatusCond] if ok { req.Status = status } if req.Status == 0 { req.Status = 1 } return nil, nil } // AdminAnswerPageReq admin answer page req type AdminAnswerPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` StatusCond string `validate:"omitempty,oneof=normal deleted pending" form:"status"` Query string `validate:"omitempty,gt=0,lte=100" form:"query"` QuestionID string `validate:"omitempty,gt=0,lte=24" form:"question_id"` QuestionTitle string `json:"-"` AnswerID string `json:"-"` Status int `json:"-"` LoginUserID string `json:"-"` } func (req *AdminAnswerPageReq) Check() (errField []*validator.FormErrorField, err error) { req.QuestionID = uid.DeShortID(req.QuestionID) if req.QuestionID == "0" { req.QuestionID = "" } if status, ok := entity.AdminAnswerSearchStatus[req.StatusCond]; ok { req.Status = status } if req.Status == 0 { req.Status = 1 } // parse query condition if len(req.Query) > 0 { prefix := "answer:" if strings.Contains(req.Query, prefix) { req.AnswerID = uid.DeShortID(strings.TrimSpace(strings.TrimPrefix(req.Query, prefix))) } else { req.QuestionTitle = strings.TrimSpace(req.Query) } } return nil, nil } type AdminUpdateQuestionStatusReq struct { QuestionID string `validate:"required" json:"question_id"` Status string `validate:"required,oneof=available closed deleted" json:"status"` UserID string `json:"-"` } type PersonalQuestionPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered" form:"order"` Username string `validate:"omitempty,gt=0,lte=100" form:"username"` LoginUserID string `json:"-"` IsAdmin bool `json:"-"` } type PersonalAnswerPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered" form:"order"` Username string `validate:"omitempty,gt=0,lte=100" form:"username"` LoginUserID string `json:"-"` IsAdmin bool `json:"-"` } type PersonalCollectionPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` UserID string `json:"-"` } type GetQuestionLinkReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1,max=100" form:"page_size"` QuestionID string `validate:"required" form:"question_id"` OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered recommend frequent" form:"order"` InDays int `validate:"omitempty,min=1" form:"in_days"` LoginUserID string `json:"-"` } type GetQuestionLinkResp struct { QuestionPageResp } ================================================ FILE: internal/schema/rank_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // GetRankPersonalWithPageReq get rank list page request type GetRankPersonalWithPageReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // username Username string `validate:"omitempty,gt=0,lte=100" form:"username"` // user id UserID string `json:"-"` } // GetRankPersonalPageResp rank response type GetRankPersonalPageResp struct { // create time CreatedAt int64 `json:"created_at"` // object id ObjectID string `json:"object_id"` // question id QuestionID string `json:"question_id"` // answer id AnswerID string `json:"answer_id"` // object type ObjectType string `json:"object_type" enums:"question,answer,tag,comment"` // title Title string `json:"title"` // url title UrlTitle string `json:"url_title"` // content Content string `json:"content"` // reputation Reputation int `json:"reputation"` // rank type RankType string `json:"rank_type"` } ================================================ FILE: internal/schema/reason_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "github.com/apache/answer/internal/base/translator" "github.com/segmentfault/pacman/i18n" ) type ReasonItem struct { ReasonKey string `json:"reason_key"` ReasonType int `json:"reason_type"` Name string `json:"name"` Description string `json:"description"` ContentType string `json:"content_type"` Placeholder string `json:"placeholder"` } type ReasonReq struct { // ObjectType ObjectType string `validate:"required" form:"object_type" json:"object_type"` // Action Action string `validate:"required" form:"action" json:"action"` } func (r *ReasonItem) Translate(keyPrefix string, lang i18n.Language) { trField := func(fieldName, fieldData string) string { // If fieldData is empty, means no need to translate if len(fieldData) == 0 { return fieldData } key := keyPrefix + "." + fieldName fieldTr := translator.Tr(lang, key) if fieldTr != key { // If i18n key exists, return i18n value return fieldTr } // If i18n key not exists, return fieldData original value return fieldData } r.ReasonKey = keyPrefix r.Name = trField("name", r.Name) r.Description = trField("desc", r.Description) r.Placeholder = trField("placeholder", r.Placeholder) } ================================================ FILE: internal/schema/render_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // PostRenderReq post render request type PostRenderReq struct { Content string `json:"content"` } ================================================ FILE: internal/schema/report_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // AddReportReq add report request type AddReportReq struct { // object id ObjectID string `validate:"required,gt=0,lte=20" json:"object_id"` // report type ReportType int `validate:"required" json:"report_type"` // report content Content string `validate:"omitempty,gt=0,lte=500" json:"content"` // user id UserID string `json:"-"` CaptchaID string `json:"captcha_id"` // captcha_id CaptchaCode string `json:"captcha_code"` } // GetReportListReq get report list all request type GetReportListReq struct { // report source Source string `validate:"required,oneof=question answer comment" form:"source"` } // GetReportTypeResp get report response type GetReportTypeResp struct { // report name Name string `json:"name"` // report description Description string `json:"description"` // report source Source string `json:"source"` // report type Type int `json:"type"` // is have content HaveContent bool `json:"have_content"` // content type ContentType string `json:"content_type"` } // ReportHandleReq request handle request type ReportHandleReq struct { ID string `validate:"required" comment:"report id" form:"id" json:"id"` FlaggedType int `validate:"required" comment:"flagged type" form:"flagged_type" json:"flagged_type"` FlaggedContent string `validate:"omitempty" comment:"flagged content" form:"flagged_content" json:"flagged_content"` } // GetReportListPageDTO report list data transfer object type GetReportListPageDTO struct { Page int PageSize int Status int } // GetReportListPageResp get report list type GetReportListPageResp struct { FlagID string `json:"flag_id"` CreatedAt int64 `json:"created_at"` ObjectID string `json:"object_id"` QuestionID string `json:"question_id"` AnswerID string `json:"answer_id"` CommentID string `json:"comment_id"` ObjectType string `json:"object_type" enums:"question,answer,comment"` Title string `json:"title"` UrlTitle string `json:"url_title"` OriginalText string `json:"original_text"` ParsedText string `json:"parsed_text"` AnswerCount int `json:"answer_count"` AnswerAccepted bool `json:"answer_accepted"` Tags []*TagResp `json:"tags"` ObjectStatus int `json:"object_status"` ObjectShowStatus int `json:"object_show_status"` AuthorUserInfo UserBasicInfo `json:"author_user_info"` SubmitAt int64 `json:"submit_at"` SubmitterUser UserBasicInfo `json:"submitter_user"` Reason *ReasonItem `json:"reason"` ReasonContent string `json:"reason_content"` } // GetUnreviewedReportPostPageReq get unreviewed report post page request type GetUnreviewedReportPostPageReq struct { Page int `json:"page" form:"page"` UserID string `json:"-"` IsAdmin bool `json:"-"` } // ReviewReportReq review report request type ReviewReportReq struct { FlagID string `validate:"required" json:"flag_id"` OperationType string `validate:"required,oneof=edit_post close_post delete_post unlist_post ignore_report" json:"operation_type"` CloseType int `validate:"omitempty" json:"close_type"` CloseMsg string `validate:"omitempty" json:"close_msg"` Title string `validate:"omitempty,notblank,gte=6,lte=150" json:"title"` Content string `validate:"omitempty,notblank,gte=6,lte=65535" json:"content"` Tags []*TagItem `validate:"omitempty,dive" json:"tags"` UserID string `json:"-"` IsAdmin bool `json:"-"` } ================================================ FILE: internal/schema/review_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/pkg/uid" ) // UpdateReviewReq update review request type UpdateReviewReq struct { ReviewID int `validate:"required" json:"review_id"` Status string `validate:"required,oneof=approve reject" json:"status"` UserID string `json:"-"` IsAdmin bool `json:"-"` } func (r *UpdateReviewReq) IsApprove() bool { return r.Status == "approve" } func (r *UpdateReviewReq) IsReject() bool { return r.Status == "reject" } // GetUnreviewedPostPageReq get review page request type GetUnreviewedPostPageReq struct { ObjectID string `validate:"omitempty" form:"object_id"` Page int `validate:"omitempty" form:"page"` ReviewerMapping map[string]string `json:"-"` UserID string `json:"-"` IsAdmin bool `json:"-"` } func (r *GetUnreviewedPostPageReq) Check() (errField []*validator.FormErrorField, err error) { if len(r.ObjectID) > 0 { r.Page = 1 r.ObjectID = uid.DeShortID(r.ObjectID) } return } // GetUnreviewedPostPageResp get review page response type GetUnreviewedPostPageResp struct { ReviewID int `json:"review_id"` CreatedAt int64 `json:"created_at"` ObjectID string `json:"object_id"` QuestionID string `json:"question_id"` AnswerID string `json:"answer_id"` CommentID string `json:"comment_id"` ObjectType string `json:"object_type" enums:"question,answer,comment"` Title string `json:"title"` UrlTitle string `json:"url_title"` OriginalText string `json:"original_text"` ParsedText string `json:"parsed_text"` Tags []*TagResp `json:"tags"` ObjectStatus int `json:"object_status"` ObjectShowStatus int `json:"object_show_status"` AuthorUserInfo UserBasicInfo `json:"author_user_info"` SubmitAt int64 `json:"submit_at"` SubmitterDisplayName string `json:"submitter_display_name"` Reason string `json:"reason"` } ================================================ FILE: internal/schema/revision_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "time" "github.com/apache/answer/internal/base/constant" ) // AddRevisionDTO add revision request type AddRevisionDTO struct { // user id UserID string // object id ObjectID string // title Title string // content Content string // log Log string // status Status int } // GetRevisionListReq get revision list all request type GetRevisionListReq struct { // object id ObjectID string `validate:"required" comment:"object_id" form:"object_id"` IsAdmin bool `json:"-"` UserID string `json:"-"` } const RevisionAuditApprove = "approve" const RevisionAuditReject = "reject" type RevisionAuditReq struct { // object id ID string `validate:"required" comment:"id" form:"id"` Operation string `validate:"required" comment:"operation" form:"operation"` // approve or reject UserID string `json:"-"` CanReviewQuestion bool `json:"-"` CanReviewAnswer bool `json:"-"` CanReviewTag bool `json:"-"` } type RevisionSearch struct { Page int `json:"page" form:"page"` // Query number of pages CanReviewQuestion bool `json:"-"` CanReviewAnswer bool `json:"-"` CanReviewTag bool `json:"-"` UserID string `json:"-"` } func (r RevisionSearch) GetCanReviewObjectTypes() []int { objectType := make([]int, 0) if r.CanReviewAnswer { objectType = append(objectType, constant.ObjectTypeStrMapping[constant.AnswerObjectType]) } if r.CanReviewQuestion { objectType = append(objectType, constant.ObjectTypeStrMapping[constant.QuestionObjectType]) } if r.CanReviewTag { objectType = append(objectType, constant.ObjectTypeStrMapping[constant.TagObjectType]) } return objectType } type GetUnreviewedRevisionResp struct { Type string `json:"type"` Info *UnreviewedRevisionInfoInfo `json:"info"` UnreviewedInfo *GetRevisionResp `json:"unreviewed_info"` } // GetRevisionResp get revision response type GetRevisionResp struct { ID string `json:"id"` UserID string `json:"use_id"` ObjectID string `json:"object_id"` ObjectType int `json:"-"` Title string `json:"title"` UrlTitle string `json:"url_title"` Content string `json:"-"` ContentParsed any `json:"content"` Status int `json:"status"` CreatedAt time.Time `json:"-"` CreatedAtParsed int64 `json:"create_at"` UserInfo UserBasicInfo `json:"user_info"` Log string `json:"reason"` } // GetReviewingTypeReq get reviewing type request type GetReviewingTypeReq struct { CanReviewQuestion bool `json:"-"` CanReviewAnswer bool `json:"-"` CanReviewTag bool `json:"-"` IsAdmin bool `json:"-"` UserID string `json:"-"` } func (r *GetReviewingTypeReq) GetCanReviewObjectTypes() []int { objectType := make([]int, 0) if r.CanReviewAnswer { objectType = append(objectType, constant.ObjectTypeStrMapping[constant.AnswerObjectType]) } if r.CanReviewQuestion { objectType = append(objectType, constant.ObjectTypeStrMapping[constant.QuestionObjectType]) } if r.CanReviewTag { objectType = append(objectType, constant.ObjectTypeStrMapping[constant.TagObjectType]) } return objectType } // GetReviewingTypeResp get reviewing type response type GetReviewingTypeResp struct { Name string `json:"name"` Label string `json:"label"` TodoAmount int64 `json:"todo_amount"` } ================================================ FILE: internal/schema/role_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // GetRoleResp get role response type GetRoleResp struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` } ================================================ FILE: internal/schema/search_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "regexp" "strings" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/plugin" ) type SearchDTO struct { Query string `validate:"required,gte=1,lte=60" form:"q"` Page int `validate:"omitempty,min=1" form:"page,default=1"` Size int `validate:"omitempty,min=1,max=50" form:"size,default=30"` Order string `validate:"required,oneof=newest active score relevance" form:"order,default=relevance" enums:"newest,active,score,relevance"` CaptchaID string `form:"captcha_id"` CaptchaCode string `form:"captcha_code"` UserID string `json:"-"` } func (s *SearchDTO) Check() (errField []*validator.FormErrorField, err error) { // Replace special characters. // Special characters will cause the search abnormal, such as search for "#" will get nearly all the content that Markdown format. replacedContent, patterns := ReplaceSearchContent(s.Query) s.Query = strings.Join(append(patterns, replacedContent), " ") return nil, nil } func ReplaceSearchContent(content string) (string, []string) { // Define the regular expressions for key:value pairs and [tag] keyValueRegex := regexp.MustCompile(`\w+:\S+`) tagRegex := regexp.MustCompile(`\[\w+\]`) // Define the pattern for characters to replace replaceCharsPattern := regexp.MustCompile(`[+#.<>\-_()*]`) // Extract key:value pairs keyValues := keyValueRegex.FindAllString(content, -1) // Extract [tag] tags := tagRegex.FindAllString(content, -1) // Replace key:value pairs and [tag] with empty string contentWithoutPatterns := keyValueRegex.ReplaceAllString(content, "") contentWithoutPatterns = tagRegex.ReplaceAllString(contentWithoutPatterns, "") // Replace characters with pattern [+#.<>_()*] with space replacedContent := replaceCharsPattern.ReplaceAllString(contentWithoutPatterns, " ") return strings.TrimSpace(replacedContent), append(keyValues, tags...) } type SearchCondition struct { // search target type: all/question/answer TargetType string // search query user id UserID string // vote amount VoteAmount int // only show not accepted answer's question NotAccepted bool // view amount Views int // answer count AnswerAmount int // only show accepted answer Accepted bool // only show this question's answer QuestionID string // search query tags Tags [][]string // search query keywords Words []string } // SearchAll check if search all func (s *SearchCondition) SearchAll() bool { return len(s.TargetType) == 0 } // SearchQuestion check if search only need question func (s *SearchCondition) SearchQuestion() bool { return s.TargetType == constant.QuestionObjectType } // SearchAnswer check if search only need answer func (s *SearchCondition) SearchAnswer() bool { return s.TargetType == constant.AnswerObjectType } // Convert2PluginSearchCond convert to plugin search condition func (s *SearchCondition) Convert2PluginSearchCond(page, pageSize int, order string) *plugin.SearchBasicCond { basic := &plugin.SearchBasicCond{ Page: page, PageSize: pageSize, Words: s.Words, TagIDs: s.Tags, UserID: s.UserID, Order: plugin.SearchOrderCond(order), QuestionID: s.QuestionID, VoteAmount: s.VoteAmount, ViewAmount: s.Views, AnswerAmount: s.AnswerAmount, } if s.Accepted { basic.AnswerAccepted = plugin.AcceptedCondTrue } else { basic.AnswerAccepted = plugin.AcceptedCondAll } if s.NotAccepted { basic.QuestionAccepted = plugin.AcceptedCondFalse } else { basic.QuestionAccepted = plugin.AcceptedCondAll } return basic } type SearchObject struct { ID string `json:"id"` QuestionID string `json:"question_id"` Title string `json:"title"` UrlTitle string `json:"url_title"` Excerpt string `json:"excerpt"` CreatedAtParsed int64 `json:"created_at"` VoteCount int `json:"vote_count"` Accepted bool `json:"accepted"` AnswerCount int `json:"answer_count"` // user info UserInfo *SearchObjectUser `json:"user_info"` // tags Tags []*TagResp `json:"tags"` // Status StatusStr string `json:"status"` } type SearchObjectUser struct { ID string `json:"id"` Username string `json:"username"` DisplayName string `json:"display_name"` Rank int `json:"rank"` Status string `json:"status"` } type TagResp struct { ID string `json:"-"` SlugName string `json:"slug_name"` DisplayName string `json:"display_name"` // if main tag slug name is not empty, this tag is synonymous with the main tag MainTagSlugName string `json:"main_tag_slug_name"` Recommend bool `json:"recommend"` Reserved bool `json:"reserved"` } type SearchResult struct { // object_type ObjectType string `json:"object_type"` // this object Object *SearchObject `json:"object"` } type SearchResp struct { Total int64 `json:"count"` // search response SearchResults []*SearchResult `json:"list"` } type SearchDescResp struct { Name string `json:"name"` Icon string `json:"icon"` Link string `json:"link"` } ================================================ FILE: internal/schema/search_schema_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "strings" "testing" "github.com/stretchr/testify/assert" ) func TestReplaceSearchContent(t *testing.T) { content := "user:aaa [tag] ssssfdfdf-as#fsadf" replacedContent, patterns := ReplaceSearchContent(content) ret := strings.Join(append(patterns, replacedContent), " ") assert.Equal(t, "user:aaa [tag] ssssfdfdf as fsadf", ret) content = "user:aaa-sss [tag1] ssssfdfdf-as#fsadf [tag2] score:3" replacedContent, patterns = ReplaceSearchContent(content) ret = strings.Join(append(patterns, replacedContent), " ") assert.Equal(t, "user:aaa-sss score:3 [tag1] [tag2] ssssfdfdf as fsadf", ret) } ================================================ FILE: internal/schema/simple_obj_info_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" ) // SimpleObjectInfo simple object info type SimpleObjectInfo struct { ObjectID string `json:"object_id"` ObjectCreatorUserID string `json:"object_creator_user_id"` QuestionID string `json:"question_id"` QuestionStatus int `json:"question_status"` AnswerID string `json:"answer_id"` AnswerStatus int `json:"answer_status"` CommentID string `json:"comment_id"` CommentStatus int `json:"comment_status"` TagID string `json:"tag_id"` TagStatus int `json:"tag_status"` ObjectType string `json:"object_type"` Title string `json:"title"` Content string `json:"content"` } // IsDeleted is deleted func (s *SimpleObjectInfo) IsDeleted() bool { switch s.ObjectType { case constant.QuestionObjectType: return s.QuestionStatus == entity.QuestionStatusDeleted case constant.AnswerObjectType: return s.AnswerStatus == entity.AnswerStatusDeleted case constant.CommentObjectType: return s.CommentStatus == entity.CommentStatusDeleted case constant.TagObjectType: return s.TagStatus == entity.TagStatusDeleted } return false } type UnreviewedRevisionInfoInfo struct { CreatedAt int64 `json:"created_at"` ObjectID string `json:"object_id"` QuestionID string `json:"question_id"` AnswerID string `json:"answer_id"` CommentID string `json:"comment_id"` ObjectType string `json:"object_type"` ObjectCreatorUserID string `json:"object_creator_user_id"` Title string `json:"title"` UrlTitle string `json:"url_title"` Content string `json:"content"` Html string `json:"html"` AnswerCount int `json:"answer_count"` AnswerAccepted bool `json:"answer_accepted"` Tags []*TagResp `json:"tags"` Status int `json:"status"` ShowStatus int `json:"show_status"` } ================================================ FILE: internal/schema/siteinfo_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "context" "fmt" "net/mail" "net/url" "path/filepath" "strings" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/segmentfault/pacman/errors" ) // SiteGeneralReq site general request type SiteGeneralReq struct { Name string `validate:"required,sanitizer,gt=1,lte=128" form:"name" json:"name"` ShortDescription string `validate:"omitempty,sanitizer,gt=3,lte=255" form:"short_description" json:"short_description"` Description string `validate:"omitempty,sanitizer,gt=3,lte=2000" form:"description" json:"description"` SiteUrl string `validate:"required,sanitizer,gt=1,lte=512,url" form:"site_url" json:"site_url"` ContactEmail string `validate:"required,sanitizer,gt=1,lte=512,email" form:"contact_email" json:"contact_email"` } func (r *SiteGeneralReq) FormatSiteUrl() { parsedUrl, err := url.Parse(r.SiteUrl) if err != nil { return } r.SiteUrl = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host) if len(parsedUrl.Path) > 0 { r.SiteUrl += parsedUrl.Path r.SiteUrl = strings.TrimSuffix(r.SiteUrl, "/") } } // SiteInterfaceReq site interface request type SiteInterfaceReq struct { Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"` TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"` // Deperecated: use SiteUsersSettingsReq instead DefaultAvatar string `validate:"omitempty" json:"-"` // Deperecated: use SiteUsersSettingsReq instead GravatarBaseURL string `validate:"omitempty" json:"-"` } // SiteInterfaceSettingsReq site interface settings request type SiteInterfaceSettingsReq struct { Language string `validate:"required,gt=1,lte=128" json:"language"` TimeZone string `validate:"required,gt=1,lte=128" json:"time_zone"` } type SiteInterfaceSettingsResp SiteInterfaceSettingsReq type SiteUsersSettingsReq struct { DefaultAvatar string `validate:"required,oneof=system gravatar" json:"default_avatar"` GravatarBaseURL string `validate:"omitempty" json:"gravatar_base_url"` } type SiteUsersSettingsResp SiteUsersSettingsReq // SiteBrandingReq site branding request type SiteBrandingReq struct { Logo string `validate:"omitempty,gt=0,lte=512" form:"logo" json:"logo"` MobileLogo string `validate:"omitempty,gt=0,lte=512" form:"mobile_logo" json:"mobile_logo"` SquareIcon string `validate:"omitempty,gt=0,lte=512" form:"square_icon" json:"square_icon"` Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"` } // SiteWriteReq site write request use SiteQuestionsReq, SiteAdvancedReq and SiteTagsReq instead type SiteWriteReq struct { MinimumContent int `validate:"omitempty,gte=0,lte=65535" json:"min_content"` RestrictAnswer bool `validate:"omitempty" json:"restrict_answer"` MinimumTags int `validate:"omitempty,gte=0,lte=5" json:"min_tags"` RequiredTag bool `validate:"omitempty" json:"required_tag"` RecommendTags []*SiteWriteTag `validate:"omitempty,dive" json:"recommend_tags"` ReservedTags []*SiteWriteTag `validate:"omitempty,dive" json:"reserved_tags"` MaxImageSize int `validate:"omitempty,gt=0" json:"max_image_size"` MaxAttachmentSize int `validate:"omitempty,gt=0" json:"max_attachment_size"` MaxImageMegapixel int `validate:"omitempty,gt=0" json:"max_image_megapixel"` AuthorizedImageExtensions []string `validate:"omitempty" json:"authorized_image_extensions"` AuthorizedAttachmentExtensions []string `validate:"omitempty" json:"authorized_attachment_extensions"` UserID string `json:"-"` } type SiteWriteResp SiteWriteReq // SiteQuestionsReq site questions settings request type SiteQuestionsReq struct { MinimumTags int `validate:"omitempty,gte=0,lte=5" json:"min_tags"` MinimumContent int `validate:"omitempty,gte=0,lte=65535" json:"min_content"` RestrictAnswer bool `validate:"omitempty" json:"restrict_answer"` } // SiteAdvancedReq site advanced settings request type SiteAdvancedReq struct { MaxImageSize int `validate:"omitempty,gt=0" json:"max_image_size"` MaxAttachmentSize int `validate:"omitempty,gt=0" json:"max_attachment_size"` MaxImageMegapixel int `validate:"omitempty,gt=0" json:"max_image_megapixel"` AuthorizedImageExtensions []string `validate:"omitempty" json:"authorized_image_extensions"` AuthorizedAttachmentExtensions []string `validate:"omitempty" json:"authorized_attachment_extensions"` } // SiteTagsReq site tags settings request type SiteTagsReq struct { ReservedTags []*SiteWriteTag `validate:"omitempty,dive" json:"reserved_tags"` RecommendTags []*SiteWriteTag `validate:"omitempty,dive" json:"recommend_tags"` RequiredTag bool `validate:"omitempty" json:"required_tag"` UserID string `json:"-"` } func (s *SiteAdvancedResp) GetMaxImageSize() int64 { if s.MaxImageSize <= 0 { return constant.DefaultMaxImageSize } return int64(s.MaxImageSize) * 1024 * 1024 } func (s *SiteAdvancedResp) GetMaxAttachmentSize() int64 { if s.MaxAttachmentSize <= 0 { return constant.DefaultMaxAttachmentSize } return int64(s.MaxAttachmentSize) * 1024 * 1024 } func (s *SiteAdvancedResp) GetMaxImageMegapixel() int { if s.MaxImageMegapixel <= 0 { return constant.DefaultMaxImageMegapixel } return s.MaxImageMegapixel * 1000 * 1000 } // SiteWriteTag site write response tag type SiteWriteTag struct { SlugName string `validate:"required" json:"slug_name"` DisplayName string `json:"display_name"` } // SiteLegalReq site branding request use SitePoliciesReq and SiteSecurityReq instead type SiteLegalReq struct { TermsOfServiceOriginalText string `json:"terms_of_service_original_text"` TermsOfServiceParsedText string `json:"terms_of_service_parsed_text"` PrivacyPolicyOriginalText string `json:"privacy_policy_original_text"` PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text"` ExternalContentDisplay string `validate:"required,oneof=always_display ask_before_display" json:"external_content_display"` } type SitePoliciesReq struct { TermsOfServiceOriginalText string `json:"terms_of_service_original_text"` TermsOfServiceParsedText string `json:"terms_of_service_parsed_text"` PrivacyPolicyOriginalText string `json:"privacy_policy_original_text"` PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text"` } type SiteSecurityReq struct { LoginRequired bool `json:"login_required"` ExternalContentDisplay string `validate:"required,oneof=always_display ask_before_display" json:"external_content_display"` CheckUpdate bool `validate:"omitempty,sanitizer" form:"check_update" json:"check_update"` } type SitePoliciesResp SitePoliciesReq type SiteSecurityResp SiteSecurityReq // GetSiteLegalInfoReq site site legal request type GetSiteLegalInfoReq struct { InfoType string `validate:"required,oneof=tos privacy" form:"info_type"` } func (r *GetSiteLegalInfoReq) IsTOS() bool { return r.InfoType == "tos" } func (r *GetSiteLegalInfoReq) IsPrivacy() bool { return r.InfoType == "privacy" } // GetSiteLegalInfoResp get site legal info response type GetSiteLegalInfoResp struct { TermsOfServiceOriginalText string `json:"terms_of_service_original_text,omitempty"` TermsOfServiceParsedText string `json:"terms_of_service_parsed_text,omitempty"` PrivacyPolicyOriginalText string `json:"privacy_policy_original_text,omitempty"` PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text,omitempty"` } // SiteUsersReq site users config request type SiteUsersReq struct { DefaultAvatar string `validate:"required,oneof=system gravatar" json:"default_avatar"` GravatarBaseURL string `json:"gravatar_base_url"` AllowUpdateDisplayName bool `json:"allow_update_display_name"` AllowUpdateUsername bool `json:"allow_update_username"` AllowUpdateAvatar bool `json:"allow_update_avatar"` AllowUpdateBio bool `json:"allow_update_bio"` AllowUpdateWebsite bool `json:"allow_update_website"` AllowUpdateLocation bool `json:"allow_update_location"` } // SiteLoginReq site login request type SiteLoginReq struct { AllowNewRegistrations bool `json:"allow_new_registrations"` AllowEmailRegistrations bool `json:"allow_email_registrations"` AllowPasswordLogin bool `json:"allow_password_login"` AllowEmailDomains []string `json:"allow_email_domains"` } // SiteCustomCssHTMLReq site custom css html type SiteCustomCssHTMLReq struct { CustomHead string `validate:"omitempty,gt=0,lte=65536" json:"custom_head"` CustomCss string `validate:"omitempty,gt=0,lte=65536" json:"custom_css"` CustomHeader string `validate:"omitempty,gt=0,lte=65536" json:"custom_header"` CustomFooter string `validate:"omitempty,gt=0,lte=65536" json:"custom_footer"` CustomSideBar string `validate:"omitempty,gt=0,lte=65536" json:"custom_sidebar"` } // SiteThemeReq site theme config type SiteThemeReq struct { Theme string `validate:"required,gt=0,lte=255" json:"theme"` ThemeConfig map[string]any `validate:"omitempty" json:"theme_config"` ColorScheme string `validate:"omitempty,gt=0,lte=100" json:"color_scheme"` Layout string `validate:"omitempty,oneof=Full-width Fixed-width" json:"layout"` } type SiteSeoReq struct { Permalink int `validate:"required,lte=4,gte=0" form:"permalink" json:"permalink"` Robots string `validate:"required" form:"robots" json:"robots"` } func (s *SiteSeoResp) IsShortLink() bool { return s.Permalink == constant.PermalinkQuestionIDAndTitleByShortID || s.Permalink == constant.PermalinkQuestionIDByShortID } // AIPromptConfig AI prompt configuration for different languages type AIPromptConfig struct { ZhCN string `json:"zh_cn"` EnUS string `json:"en_us"` } // SiteAIReq AI configuration request type SiteAIReq struct { Enabled bool `validate:"omitempty" form:"enabled" json:"enabled"` ChosenProvider string `validate:"omitempty,lte=50" form:"chosen_provider" json:"chosen_provider"` SiteAIProviders []*SiteAIProvider `validate:"omitempty,dive" form:"ai_providers" json:"ai_providers"` PromptConfig *AIPromptConfig `validate:"omitempty" form:"prompt_config" json:"prompt_config,omitempty"` } func (s *SiteAIResp) GetProvider() *SiteAIProvider { if !s.Enabled || s.ChosenProvider == "" { return &SiteAIProvider{} } if len(s.SiteAIProviders) == 0 { return &SiteAIProvider{} } for _, provider := range s.SiteAIProviders { if provider.Provider == s.ChosenProvider { return provider } } return &SiteAIProvider{} } type SiteAIProvider struct { Provider string `validate:"omitempty,lte=50" form:"provider" json:"provider"` APIHost string `validate:"omitempty,lte=512" form:"api_host" json:"api_host"` APIKey string `validate:"omitempty,lte=256" form:"api_key" json:"api_key"` Model string `validate:"omitempty,lte=100" form:"model" json:"model"` } // SiteAIResp AI configuration response type SiteAIResp SiteAIReq type SiteMCPReq struct { Enabled bool `validate:"omitempty" form:"enabled" json:"enabled"` } type SiteMCPResp struct { Enabled bool `json:"enabled"` Type string `json:"type"` URL string `json:"url"` HTTPHeader string `json:"http_header"` } // SiteGeneralResp site general response type SiteGeneralResp SiteGeneralReq // SiteInterfaceResp site interface response type SiteInterfaceResp SiteInterfaceReq // SiteBrandingResp site branding response type SiteBrandingResp SiteBrandingReq // SiteLoginResp site login response type SiteLoginResp SiteLoginReq // SiteCustomCssHTMLResp site custom css html response type SiteCustomCssHTMLResp SiteCustomCssHTMLReq // SiteUsersResp site users response type SiteUsersResp SiteUsersReq // SiteThemeResp site theme response type SiteThemeResp struct { ThemeOptions []*ThemeOption `json:"theme_options"` Theme string `json:"theme"` ThemeConfig map[string]any `json:"theme_config"` ColorScheme string `json:"color_scheme"` Layout string `json:"layout"` } func (s *SiteThemeResp) TrTheme(ctx context.Context) { la := handler.GetLangByCtx(ctx) for _, option := range s.ThemeOptions { tr := translator.Tr(la, option.Value) // if tr is equal the option value means not found translation, so use the original label if tr != option.Value { option.Label = tr } } } // ThemeOption get label option type ThemeOption struct { Label string `json:"label"` Value string `json:"value"` } type SiteQuestionsResp SiteQuestionsReq type SiteAdvancedResp SiteAdvancedReq type SiteTagsResp SiteTagsReq // SiteLegalResp site write response use SitePoliciesResp and SiteSecurityResp instead type SiteLegalResp SiteLegalReq // SiteLegalSimpleResp site write response type SiteLegalSimpleResp struct { ExternalContentDisplay string `validate:"required,oneof=always_display ask_before_display" json:"external_content_display"` } // SiteSeoResp site write response type SiteSeoResp SiteSeoReq // SiteInfoResp get site info response type SiteInfoResp struct { General *SiteGeneralResp `json:"general"` Interface *SiteInterfaceSettingsResp `json:"interface"` UsersSettings *SiteUsersSettingsResp `json:"users_settings"` Branding *SiteBrandingResp `json:"branding"` Login *SiteLoginResp `json:"login"` Theme *SiteThemeResp `json:"theme"` CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"` SiteSeo *SiteSeoResp `json:"site_seo"` SiteUsers *SiteUsersResp `json:"site_users"` Advanced *SiteAdvancedResp `json:"site_advanced"` Questions *SiteQuestionsResp `json:"site_questions"` Tags *SiteTagsResp `json:"site_tags"` Legal *SiteLegalSimpleResp `json:"site_legal"` Security *SiteSecurityResp `json:"site_security"` Version string `json:"version"` Revision string `json:"revision"` AIEnabled bool `json:"ai_enabled"` MCPEnabled bool `json:"mcp_enabled"` } type TemplateSiteInfoResp struct { General *SiteGeneralResp `json:"general"` Interface *SiteInterfaceSettingsResp `json:"interface"` Branding *SiteBrandingResp `json:"branding"` SiteSeo *SiteSeoResp `json:"site_seo"` CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"` Title string Year string Canonical string JsonLD string Keywords string Description string } // UpdateSMTPConfigReq get smtp config request type UpdateSMTPConfigReq struct { FromEmail string `validate:"omitempty,gt=0,lte=256" json:"from_email"` FromName string `validate:"omitempty,gt=0,lte=256" json:"from_name"` SMTPHost string `validate:"omitempty,gt=0,lte=256" json:"smtp_host"` SMTPPort int `validate:"omitempty,min=1,max=65535" json:"smtp_port"` Encryption string `validate:"omitempty,oneof=SSL TLS" json:"encryption"` // "" SSL TLS SMTPUsername string `validate:"omitempty,gt=0,lte=256" json:"smtp_username"` SMTPPassword string `validate:"omitempty,gt=0,lte=256" json:"smtp_password"` SMTPAuthentication bool `validate:"omitempty" json:"smtp_authentication"` TestEmailRecipient string `validate:"omitempty,email" json:"test_email_recipient"` } func (r *UpdateSMTPConfigReq) Check() (errField []*validator.FormErrorField, err error) { _, err = mail.ParseAddress(r.FromName) if err == nil { return append(errField, &validator.FormErrorField{ ErrorField: "from_name", ErrorMsg: reason.SMTPConfigFromNameCannotBeEmail, }), errors.BadRequest(reason.SMTPConfigFromNameCannotBeEmail) } return nil, nil } // GetSMTPConfigResp get smtp config response type GetSMTPConfigResp struct { FromEmail string `json:"from_email"` FromName string `json:"from_name"` SMTPHost string `json:"smtp_host"` SMTPPort int `json:"smtp_port"` Encryption string `json:"encryption"` // "" SSL TLS SMTPUsername string `json:"smtp_username"` SMTPPassword string `json:"smtp_password"` SMTPAuthentication bool `json:"smtp_authentication"` } // GetManifestJsonResp get manifest json response type GetManifestJsonResp struct { ManifestVersion int `json:"manifest_version"` Version string `json:"version"` Revision string `json:"revision"` ShortName string `json:"short_name"` Name string `json:"name"` Icons []ManifestJsonIcon `json:"icons"` StartUrl string `json:"start_url"` Display string `json:"display"` ThemeColor string `json:"theme_color"` BackgroundColor string `json:"background_color"` } type ManifestJsonIcon struct { Src string `json:"src"` Sizes string `json:"sizes"` Type string `json:"type"` } func CreateManifestJsonIcons(icon string) []ManifestJsonIcon { ext := filepath.Ext(icon) if ext == "" { ext = "png" } else { ext = strings.ToLower(ext[1:]) } iconType := fmt.Sprintf("image/%s", ext) return []ManifestJsonIcon{ { Src: icon, Sizes: "16x16", Type: iconType, }, { Src: icon, Sizes: "32x32", Type: iconType, }, { Src: icon, Sizes: "48x48", Type: iconType, }, { Src: icon, Sizes: "128x128", Type: iconType, }, } } const ( // PrivilegeLevel1 low PrivilegeLevel1 PrivilegeLevel = 1 // PrivilegeLevel2 medium PrivilegeLevel2 PrivilegeLevel = 2 // PrivilegeLevel3 high PrivilegeLevel3 PrivilegeLevel = 3 // PrivilegeLevelCustom custom PrivilegeLevelCustom PrivilegeLevel = 99 ) type PrivilegeLevel int type PrivilegeOptions []*PrivilegeOption func (p PrivilegeOptions) Choose(level PrivilegeLevel) (option *PrivilegeOption) { for _, op := range p { if op.Level == level { return op } } return nil } // GetPrivilegesConfigResp get privileges config response type GetPrivilegesConfigResp struct { Options []*PrivilegeOption `json:"options"` SelectedLevel PrivilegeLevel `json:"selected_level"` } // PrivilegeOption privilege option type PrivilegeOption struct { Level PrivilegeLevel `json:"level"` LevelDesc string `json:"level_desc"` Privileges []*constant.Privilege `validate:"dive" json:"privileges"` } // UpdatePrivilegesConfigReq update privileges config request type UpdatePrivilegesConfigReq struct { Level PrivilegeLevel `validate:"required,min=1,max=3|eq=99" json:"level"` CustomPrivileges []*constant.Privilege `validate:"dive" json:"custom_privileges"` } var ( DefaultPrivilegeOptions PrivilegeOptions DefaultCustomPrivilegeOption *PrivilegeOption privilegeOptionsLevelMapping = map[string][]int{ constant.RankQuestionAddKey: {1, 1, 1}, constant.RankAnswerAddKey: {1, 1, 1}, constant.RankCommentAddKey: {1, 1, 1}, constant.RankReportAddKey: {1, 1, 1}, constant.RankCommentVoteUpKey: {1, 1, 1}, constant.RankLinkUrlLimitKey: {1, 10, 10}, constant.RankQuestionVoteUpKey: {1, 8, 15}, constant.RankAnswerVoteUpKey: {1, 8, 15}, constant.RankQuestionVoteDownKey: {125, 125, 125}, constant.RankAnswerVoteDownKey: {125, 125, 125}, constant.RankInviteSomeoneToAnswerKey: {1, 500, 1000}, constant.RankTagAddKey: {1, 750, 1500}, constant.RankTagEditKey: {1, 50, 100}, constant.RankQuestionEditKey: {1, 100, 200}, constant.RankAnswerEditKey: {1, 100, 200}, constant.RankQuestionEditWithoutReviewKey: {1, 1000, 2000}, constant.RankAnswerEditWithoutReviewKey: {1, 1000, 2000}, constant.RankQuestionAuditKey: {1, 1000, 2000}, constant.RankAnswerAuditKey: {1, 1000, 2000}, constant.RankTagAuditKey: {1, 2500, 5000}, constant.RankTagEditWithoutReviewKey: {1, 10000, 20000}, constant.RankTagSynonymKey: {1, 10000, 20000}, } ) func init() { DefaultPrivilegeOptions = append(DefaultPrivilegeOptions, &PrivilegeOption{ Level: PrivilegeLevel1, LevelDesc: reason.PrivilegeLevel1Desc, }, &PrivilegeOption{ Level: PrivilegeLevel2, LevelDesc: reason.PrivilegeLevel2Desc, }, &PrivilegeOption{ Level: PrivilegeLevel3, LevelDesc: reason.PrivilegeLevel3Desc, }) for _, option := range DefaultPrivilegeOptions { for _, privilege := range constant.RankAllPrivileges { if len(privilegeOptionsLevelMapping[privilege.Key]) == 0 { continue } option.Privileges = append(option.Privileges, &constant.Privilege{ Label: privilege.Label, Value: privilegeOptionsLevelMapping[privilege.Key][option.Level-1], Key: privilege.Key, }) } } // set up default custom privilege option DefaultCustomPrivilegeOption = &PrivilegeOption{ Level: PrivilegeLevelCustom, LevelDesc: reason.PrivilegeLevelCustomDesc, Privileges: DefaultPrivilegeOptions[0].Privileges, } } ================================================ FILE: internal/schema/sitemap_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema type SiteMapList struct { QuestionIDs []*SiteMapQuestionInfo `json:"question_ids"` MaxPageNum []int `json:"max_page_num"` } type SiteMapPageList struct { PageData []*SiteMapQuestionInfo `json:"page_data"` } type SiteMapQuestionInfo struct { ID string `json:"id"` Title string `json:"title"` UpdateTime string `json:"time"` } ================================================ FILE: internal/schema/tag_list_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // AddTagListReq add tag list request type AddTagListReq struct { // tag_id TagID int64 `validate:"required" comment:"tag_id" json:"tag_id"` // object_id ObjectID int64 `validate:"required" comment:"object_id" json:"object_id"` // tag_list_status(available: 1; deleted: 10) Status int `validate:"required" comment:"tag_list_status(available: 1; deleted: 10)" json:"status"` } // RemoveTagListReq delete tag list request type RemoveTagListReq struct { // tag_list_id ID int64 `validate:"required" comment:"tag_list_id" json:"id"` } // UpdateTagListReq update tag list request type UpdateTagListReq struct { // tag_list_id ID int64 `validate:"required" comment:"tag_list_id" json:"id"` // tag_id TagID int64 `validate:"omitempty" comment:"tag_id" json:"tag_id"` // object_id ObjectID int64 `validate:"omitempty" comment:"object_id" json:"object_id"` // tag_list_status(available: 1; deleted: 10) Status int `validate:"omitempty" comment:"tag_list_status(available: 1; deleted: 10)" json:"status"` } // GetTagListListReq get tag list list all request type GetTagListListReq struct { // tag_id TagID int64 `validate:"omitempty" comment:"tag_id" form:"tag_id"` // object_id ObjectID int64 `validate:"omitempty" comment:"object_id" form:"object_id"` // tag_list_status(available: 1; deleted: 10) Status int `validate:"omitempty" comment:"tag_list_status(available: 1; deleted: 10)" form:"status"` } // GetTagListWithPageReq get tag list list page request type GetTagListWithPageReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // tag_id TagID int64 `validate:"omitempty" comment:"tag_id" form:"tag_id"` // object_id ObjectID int64 `validate:"omitempty" comment:"object_id" form:"object_id"` // tag_list_status(available: 1; deleted: 10) Status int `validate:"omitempty" comment:"tag_list_status(available: 1; deleted: 10)" form:"status"` } // GetTagListResp get tag list response type GetTagListResp struct { // tag_list_id ID int64 `json:"id"` // tag_id TagID int64 `json:"tag_id"` // object_id ObjectID int64 `json:"object_id"` // tag_list_status(available: 1; deleted: 10) Status int `json:"status"` } ================================================ FILE: internal/schema/tag_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "strings" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/pkg/converter" ) // SearchTagLikeReq get tag list all request type SearchTagLikeReq struct { // tag Tag string `validate:"omitempty" form:"tag"` IsAdmin bool `json:"-"` } // SearchTagsBySlugName search tags by slug name type SearchTagsBySlugName struct { // slug name list split by ',' Tags string `form:"tags"` } // GetTagInfoReq get tag info request type GetTagInfoReq struct { // tag id ID string `validate:"omitempty" form:"id"` // tag slug name Name string `validate:"omitempty,gt=0,lte=35" form:"name"` UserID string `json:"-"` CanEdit bool `json:"-"` CanDelete bool `json:"-"` CanMerge bool `json:"-"` CanRecover bool `json:"-"` } type GetTamplateTagInfoReq struct { // tag id ID string `validate:"omitempty" form:"id"` // tag slug name Name string `validate:"omitempty" form:"name"` // user id UserID string `json:"-"` Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` } func (r *GetTagInfoReq) Check() (errFields []*validator.FormErrorField, err error) { r.Name = strings.ToLower(r.Name) return nil, nil } // GetTagResp get tag response type GetTagResp struct { TagID string `json:"tag_id"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` SlugName string `json:"slug_name"` DisplayName string `json:"display_name"` Excerpt string `json:"excerpt"` OriginalText string `json:"original_text"` ParsedText string `json:"parsed_text"` Description string `json:"description"` FollowCount int `json:"follow_count"` QuestionCount int `json:"question_count"` IsFollower bool `json:"is_follower"` Status string `json:"status"` MemberActions []*PermissionMemberAction `json:"member_actions"` // if main tag slug name is not empty, this tag is synonymous with the main tag MainTagSlugName string `json:"main_tag_slug_name"` Recommend bool `json:"recommend"` Reserved bool `json:"reserved"` } func (tr *GetTagResp) GetExcerpt() { excerpt := strings.TrimSpace(tr.ParsedText) idx := strings.Index(excerpt, "\n") if idx >= 0 { excerpt = excerpt[0:idx] } tr.Excerpt = excerpt } // GetTagPageResp get tag response type GetTagPageResp struct { // tag_id TagID string `json:"tag_id"` // slug_name SlugName string `json:"slug_name"` // display_name DisplayName string `json:"display_name"` // excerpt Excerpt string `json:"excerpt"` // description Description string `json:"description"` // original text OriginalText string `json:"original_text"` // parsed_text ParsedText string `json:"parsed_text"` // follower amount FollowCount int `json:"follow_count"` // question amount QuestionCount int `json:"question_count"` // is follower IsFollower bool `json:"is_follower"` // created time CreatedAt int64 `json:"created_at"` // updated time UpdatedAt int64 `json:"updated_at"` Recommend bool `json:"recommend"` Reserved bool `json:"reserved"` } func (tr *GetTagPageResp) GetExcerpt() { excerpt := strings.TrimSpace(tr.ParsedText) idx := strings.Index(excerpt, "\n") if idx >= 0 { excerpt = excerpt[0:idx] } tr.Excerpt = excerpt } type TagChange struct { ObjectID string `json:"object_id"` // object_id Tags []*TagItem `json:"tags"` // tags name // user id UserID string `json:"-"` } type TagItem struct { // slug_name SlugName string `validate:"omitempty,gt=0,lte=35" json:"slug_name"` // display_name DisplayName string `validate:"omitempty,gt=0,lte=35" json:"display_name"` // original text OriginalText string `validate:"omitempty" json:"original_text"` // parsed text ParsedText string `json:"-"` } // RemoveTagReq delete tag request type RemoveTagReq struct { // tag_id TagID string `validate:"required" json:"tag_id"` // user id UserID string `json:"-"` } // AddTagReq add tag request type AddTagReq struct { // slug_name SlugName string `validate:"required,gt=0,lte=35" json:"slug_name"` // display_name DisplayName string `validate:"required,gt=0,lte=35" json:"display_name"` // original text OriginalText string `validate:"required,gt=0,lte=65536" json:"original_text"` // parsed text ParsedText string `json:"-"` // user id UserID string `json:"-"` } func (req *AddTagReq) Check() (errFields []*validator.FormErrorField, err error) { req.ParsedText = converter.Markdown2HTML(req.OriginalText) req.SlugName = strings.ToLower(req.SlugName) return nil, nil } // AddTagResp add tag response type AddTagResp struct { SlugName string `json:"slug_name"` } // UpdateTagReq update tag request type UpdateTagReq struct { // tag_id TagID string `validate:"required" json:"tag_id"` // slug_name SlugName string `validate:"omitempty,gt=0,lte=35" json:"slug_name"` // display_name DisplayName string `validate:"omitempty,gt=0,lte=35" json:"display_name"` // original text OriginalText string `validate:"omitempty" json:"original_text"` // parsed text ParsedText string `json:"-"` // edit summary EditSummary string `validate:"omitempty" json:"edit_summary"` // user id UserID string `json:"-"` NoNeedReview bool `json:"-"` } func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error) { r.ParsedText = converter.Markdown2HTML(r.OriginalText) return nil, nil } // RecoverTagReq update tag request type RecoverTagReq struct { TagID string `validate:"required" json:"tag_id"` UserID string `json:"-"` } // UpdateTagResp update tag response type UpdateTagResp struct { WaitForReview bool `json:"wait_for_review"` } // GetTagWithPageReq get tag list page request type GetTagWithPageReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // slug_name SlugName string `validate:"omitempty,gt=0,lte=35" form:"slug_name"` // display_name DisplayName string `validate:"omitempty,gt=0,lte=35" form:"display_name"` // query condition QueryCond string `validate:"omitempty,oneof=popular name newest" form:"query_cond"` // user id UserID string `json:"-"` } // GetTagSynonymsReq get tag synonyms request type GetTagSynonymsReq struct { // tag_id TagID string `validate:"required" form:"tag_id"` // user id UserID string `json:"-"` // whether user can edit it CanEdit bool `json:"-"` } // GetTagSynonymsResp get tag synonyms response type GetTagSynonymsResp struct { // synonyms Synonyms []*TagSynonym `json:"synonyms"` // MemberActions MemberActions []*PermissionMemberAction `json:"member_actions"` } type TagSynonym struct { // tag id TagID string `json:"tag_id"` // slug name SlugName string `json:"slug_name"` // display name DisplayName string `json:"display_name"` // if main tag slug name is not empty, this tag is synonymous with the main tag MainTagSlugName string `json:"main_tag_slug_name"` } // UpdateTagSynonymReq update tag request type UpdateTagSynonymReq struct { // tag_id TagID string `validate:"required" json:"tag_id"` // synonym tag list SynonymTagList []*TagItem `validate:"required,dive" json:"synonym_tag_list"` // user id UserID string `json:"-"` } func (req *UpdateTagSynonymReq) Format() { for _, item := range req.SynonymTagList { item.SlugName = strings.ToLower(item.SlugName) } } // GetFollowingTagsResp get following tags response type GetFollowingTagsResp struct { // tag id TagID string `json:"tag_id"` // slug name SlugName string `json:"slug_name"` // display name DisplayName string `json:"display_name"` // if main tag slug name is not empty, this tag is synonymous with the main tag MainTagSlugName string `json:"main_tag_slug_name"` Recommend bool `json:"recommend"` Reserved bool `json:"reserved"` } // GetTagBasicResp get tag basic response type GetTagBasicResp struct { TagID string `json:"tag_id"` SlugName string `json:"slug_name"` DisplayName string `json:"display_name"` Recommend bool `json:"recommend"` Reserved bool `json:"reserved"` } // MergeTagReq merge tag request type MergeTagReq struct { // source tag id SourceTagID string `validate:"required" json:"source_tag_id"` // target tag id TargetTagID string `validate:"required" json:"target_tag_id"` // user id UserID string `json:"-"` } // MergeTagResp merge tag response type MergeTagResp struct { } ================================================ FILE: internal/schema/template_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import "time" type Paginator struct { Pages []int Totalpages int Prevpage int Nextpage int Currpage int } type QAPageJsonLD struct { Context string `json:"@context"` Type string `json:"@type"` MainEntity struct { Type string `json:"@type"` Name string `json:"name"` Text string `json:"text"` AnswerCount int `json:"answerCount"` UpvoteCount int `json:"upvoteCount"` DateCreated time.Time `json:"dateCreated"` Author struct { URL string `json:"url"` Type string `json:"@type"` Name string `json:"name"` } `json:"author"` AcceptedAnswer *AcceptedAnswerItem `json:"acceptedAnswer,omitempty"` SuggestedAnswer []*SuggestedAnswerItem `json:"suggestedAnswer"` } `json:"mainEntity"` } type AcceptedAnswerItem struct { Type string `json:"@type"` Text string `json:"text"` DateCreated time.Time `json:"dateCreated"` UpvoteCount int `json:"upvoteCount"` URL string `json:"url"` Author struct { URL string `json:"url"` Type string `json:"@type"` Name string `json:"name"` } `json:"author"` } type SuggestedAnswerItem struct { Type string `json:"@type"` Text string `json:"text"` DateCreated time.Time `json:"dateCreated"` UpvoteCount int `json:"upvoteCount"` URL string `json:"url"` Author struct { URL string `json:"url"` Type string `json:"@type"` Name string `json:"name"` } `json:"author"` } ================================================ FILE: internal/schema/theme_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema var GetThemeOptions = []*ThemeOption{ { Label: "Default", Value: "default", }, } ================================================ FILE: internal/schema/user_external_login_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema // UserExternalLoginResp user external login resp type UserExternalLoginResp struct { BindingKey string `json:"binding_key"` AccessToken string `json:"access_token"` // ErrMsg error message, if not empty, means login failed and this message should be displayed. ErrMsg string `json:"-"` ErrTitle string `json:"-"` } // ExternalLoginBindingUserSendEmailReq external login binding user request type ExternalLoginBindingUserSendEmailReq struct { BindingKey string `validate:"required,gt=1,lte=100" json:"binding_key"` Email string `validate:"required,gt=1,lte=512,email" json:"email"` // If must is true, whatever email if exists, try to bind user. // If must is false, when email exist, will only be prompted with a warning. Must bool `json:"must"` } // ExternalLoginBindingUserSendEmailResp external login binding user response type ExternalLoginBindingUserSendEmailResp struct { EmailExistAndMustBeConfirmed bool `json:"email_exist_and_must_be_confirmed"` AccessToken string `json:"access_token"` } // ExternalLoginBindingUserReq external login binding user request type ExternalLoginBindingUserReq struct { Code string `validate:"required,gt=0,lte=500" json:"code"` Content string `json:"-"` } // ExternalLoginBindingUserResp external login binding user response type ExternalLoginBindingUserResp struct { AccessToken string `json:"access_token"` } // ExternalLoginUserInfoCache external login user info type ExternalLoginUserInfoCache struct { // Third party identification // e.g. facebook, twitter, instagram Provider string // required. The unique user ID provided by the third-party login ExternalID string // optional. This name is used preferentially during registration DisplayName string // optional. This username is used preferentially during registration Username string // optional. If email exist will bind the existing user Email string // optional. The avatar URL provided by the third-party login platform Avatar string // optional. The original user information provided by the third-party login platform MetaInfo string // optional. The bio provided by the third-party login platform Bio string } // ExternalLoginUnbindingReq external login unbinding user type ExternalLoginUnbindingReq struct { ExternalID string `validate:"required,gt=0,lte=128" json:"external_id"` UserID string `json:"-"` } // UserCenterUserSettingsResp user center user info response type UserCenterUserSettingsResp struct { ProfileSettingAgent UserSettingAgent `json:"profile_setting_agent"` AccountSettingAgent UserSettingAgent `json:"account_setting_agent"` } type UserCenterAdminFunctionAgentResp struct { AllowCreateUser bool `json:"allow_create_user"` AllowUpdateUserStatus bool `json:"allow_update_user_status"` AllowUpdateUserPassword bool `json:"allow_update_user_password"` AllowUpdateUserRole bool `json:"allow_update_user_role"` } type UserSettingAgent struct { Enabled bool `json:"enabled"` RedirectURL string `json:"redirect_url"` } ================================================ FILE: internal/schema/user_notification_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "encoding/json" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" ) type NotificationChannelConfig struct { Key constant.NotificationChannelKey `json:"key"` Enable bool `json:"enable"` } type NotificationChannels []*NotificationChannelConfig func NewNotificationChannelsFormJson(jsonStr string) NotificationChannels { var list NotificationChannels _ = json.Unmarshal([]byte(jsonStr), &list) return list } func NewNotificationChannelConfigFormJson(jsonStr string) NotificationChannelConfig { var list NotificationChannels _ = json.Unmarshal([]byte(jsonStr), &list) if len(list) > 0 { return *list[0] } return NotificationChannelConfig{} } func (n *NotificationChannels) ToJsonString() string { data, _ := json.Marshal(n) return string(data) } type NotificationConfig struct { Inbox NotificationChannelConfig `json:"inbox"` AllNewQuestion NotificationChannelConfig `json:"all_new_question"` AllNewQuestionForFollowingTags NotificationChannelConfig `json:"all_new_question_for_following_tags"` } func NewNotificationConfig(configs []*entity.UserNotificationConfig) NotificationConfig { nc := NotificationConfig{} for _, item := range configs { switch item.Source { case string(constant.InboxSource): nc.Inbox = NewNotificationChannelConfigFormJson(item.Channels) case string(constant.AllNewQuestionSource): nc.AllNewQuestion = NewNotificationChannelConfigFormJson(item.Channels) case string(constant.AllNewQuestionForFollowingTagsSource): nc.AllNewQuestionForFollowingTags = NewNotificationChannelConfigFormJson(item.Channels) } } return nc } func (n *NotificationConfig) Format() { if n.Inbox.Key == "" { n.Inbox.Key = constant.EmailChannel n.Inbox.Enable = false } if n.AllNewQuestion.Key == "" { n.AllNewQuestion.Key = constant.EmailChannel n.AllNewQuestion.Enable = false } if n.AllNewQuestionForFollowingTags.Key == "" { n.AllNewQuestionForFollowingTags.Key = constant.EmailChannel n.AllNewQuestionForFollowingTags.Enable = false } } // UpdateUserNotificationConfigReq update user notification config request type UpdateUserNotificationConfigReq struct { NotificationConfig UserID string `json:"-"` } // GetUserNotificationConfigResp get user notification config response type GetUserNotificationConfigResp struct { NotificationConfig } ================================================ FILE: internal/schema/user_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema import ( "context" "encoding/json" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/pkg/day" "github.com/segmentfault/pacman/errors" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/converter" "github.com/jinzhu/copier" ) // UserVerifyEmailReq user verify email request type UserVerifyEmailReq struct { // code Code string `validate:"required,gt=0,lte=500" form:"code"` // content Content string `json:"-"` } // UserLoginResp get user response type UserLoginResp struct { // user id ID string `json:"id"` // create time CreatedAt int64 `json:"created_at"` // last login date LastLoginDate int64 `json:"last_login_date"` // username Username string `json:"username"` // email EMail string `json:"e_mail"` // mail status(1 pass 2 to be verified) MailStatus int `json:"mail_status"` // notice status(1 on 2off) NoticeStatus int `json:"notice_status"` // follow count FollowCount int `json:"follow_count"` // answer count AnswerCount int `json:"answer_count"` // question count QuestionCount int `json:"question_count"` // rank Rank int `json:"rank"` // authority group AuthorityGroup int `json:"authority_group"` // display name DisplayName string `json:"display_name"` // avatar Avatar string `json:"avatar"` // mobile Mobile string `json:"mobile"` // bio markdown Bio string `json:"bio"` // bio html BioHTML string `json:"bio_html"` // website Website string `json:"website"` // location Location string `json:"location"` // language Language string `json:"language"` // Color scheme ColorScheme string `json:"color_scheme"` // access token AccessToken string `json:"access_token"` // role id RoleID int `json:"role_id"` // user status Status string `json:"status"` // user have password HavePassword bool `json:"have_password"` // visit token VisitToken string `json:"visit_token"` // suspended until timestamp SuspendedUntil int64 `json:"suspended_until"` } func (r *UserLoginResp) ConvertFromUserEntity(userInfo *entity.User) { _ = copier.Copy(r, userInfo) r.CreatedAt = userInfo.CreatedAt.Unix() r.LastLoginDate = userInfo.LastLoginDate.Unix() r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus) r.HavePassword = len(userInfo.Pass) > 0 if !userInfo.SuspendedUntil.IsZero() { r.SuspendedUntil = userInfo.SuspendedUntil.Unix() } } type GetCurrentLoginUserInfoResp struct { *UserLoginResp Avatar *AvatarInfo `json:"avatar"` } func (r *GetCurrentLoginUserInfoResp) ConvertFromUserEntity(userInfo *entity.User) { _ = copier.Copy(r, userInfo) r.CreatedAt = userInfo.CreatedAt.Unix() r.LastLoginDate = userInfo.LastLoginDate.Unix() r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus) if len(r.ColorScheme) == 0 { r.ColorScheme = constant.ColorSchemeDefault } if !userInfo.SuspendedUntil.IsZero() { r.SuspendedUntil = userInfo.SuspendedUntil.Unix() } } // GetOtherUserInfoByUsernameResp get user response type GetOtherUserInfoByUsernameResp struct { // user id ID string `json:"id"` // create time CreatedAt int64 `json:"created_at"` // last login date LastLoginDate int64 `json:"last_login_date"` // username Username string `json:"username"` // email // follow count FollowCount int `json:"follow_count"` // answer count AnswerCount int `json:"answer_count"` // question count QuestionCount int `json:"question_count"` // rank Rank int `json:"rank"` // display name DisplayName string `json:"display_name"` // avatar Avatar string `json:"avatar"` // mobile Mobile string `json:"mobile"` // bio markdown Bio string `json:"bio"` // bio html BioHTML string `json:"bio_html"` // website Website string `json:"website"` // location Location string `json:"location"` Status string `json:"status"` StatusMsg string `json:"status_msg,omitempty"` // suspended until timestamp SuspendedUntil int64 `json:"suspended_until"` } func (r *GetOtherUserInfoByUsernameResp) ConvertFromUserEntity(userInfo *entity.User) { _ = copier.Copy(r, userInfo) r.CreatedAt = userInfo.CreatedAt.Unix() r.LastLoginDate = userInfo.LastLoginDate.Unix() r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus) if !userInfo.SuspendedUntil.IsZero() { r.SuspendedUntil = userInfo.SuspendedUntil.Unix() } r.StatusMsg = "" } func (r *GetOtherUserInfoByUsernameResp) ConvertFromUserEntityWithLang(ctx context.Context, userInfo *entity.User) { _ = copier.Copy(r, userInfo) r.CreatedAt = userInfo.CreatedAt.Unix() r.LastLoginDate = userInfo.LastLoginDate.Unix() r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus) lang := handler.GetLangByCtx(ctx) if userInfo.MailStatus == entity.EmailStatusToBeVerified { r.StatusMsg = translator.Tr(lang, reason.UserStatusInactive) } switch userInfo.Status { case entity.UserStatusSuspended: if userInfo.SuspendedUntil.IsZero() || userInfo.SuspendedUntil.Year() >= 2099 { r.StatusMsg = translator.Tr(lang, reason.UserStatusSuspendedForever) } else { r.SuspendedUntil = userInfo.SuspendedUntil.Unix() trans := translator.GlobalTrans.Tr(lang, "ui.dates.long_date_with_time") suspendedUntilFormatted := day.Format(userInfo.SuspendedUntil.Unix(), trans, "UTC") r.StatusMsg = translator.TrWithData(lang, reason.UserStatusSuspendedUntil, map[string]any{ "SuspendedUntil": suspendedUntilFormatted, }) } case entity.UserStatusDeleted: r.StatusMsg = translator.Tr(lang, reason.UserStatusDeleted) } } // UserEmailLoginReq user email login request type UserEmailLoginReq struct { Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"` Pass string `validate:"required,gte=8,lte=32" json:"pass"` CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` } // UserRegisterReq user register request type UserRegisterReq struct { Name string `validate:"required,gte=2,lte=30" json:"name"` Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` Pass string `validate:"required,gte=8,lte=32" json:"pass"` CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` IP string `json:"-" ` } func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err error) { if err = checker.CheckPassword(u.Pass); err != nil { errFields = append(errFields, &validator.FormErrorField{ ErrorField: "pass", ErrorMsg: err.Error(), }) return errFields, err } return nil, nil } type UserModifyPasswordReq struct { OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"` Pass string `validate:"required,gte=8,lte=32" json:"pass"` CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` UserID string `json:"-"` AccessToken string `json:"-"` } func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) { if err = checker.CheckPassword(u.Pass); err != nil { errFields = append(errFields, &validator.FormErrorField{ ErrorField: "pass", ErrorMsg: err.Error(), }) return errFields, err } return nil, nil } type UpdateInfoRequest struct { DisplayName string `validate:"omitempty,gte=2,lte=30" json:"display_name"` Username string `validate:"omitempty,gte=2,lte=30" json:"username"` Avatar AvatarInfo `json:"avatar"` Bio string `validate:"omitempty,gt=0,lte=4096" json:"bio"` BioHTML string `json:"-"` Website string `validate:"omitempty,gt=0,lte=500" json:"website"` Location string `validate:"omitempty,gt=0,lte=100" json:"location"` UserID string `json:"-"` IsAdmin bool `json:"-"` } type AvatarInfo struct { Type string `validate:"omitempty,gt=0,lte=100" json:"type"` Gravatar string `validate:"omitempty,gt=0,lte=200" json:"gravatar"` Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"` } func (a *AvatarInfo) ToJsonString() string { data, _ := json.Marshal(a) return string(data) } func (a *AvatarInfo) GetURL() string { switch a.Type { case constant.AvatarTypeGravatar: return a.Gravatar case constant.AvatarTypeCustom: return a.Custom default: return "" } } func CustomAvatar(url string) *AvatarInfo { return &AvatarInfo{ Type: constant.AvatarTypeCustom, Custom: url, } } func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) { req.BioHTML = converter.Markdown2BasicHTML(req.Bio) if len(req.Website) > 0 && !checker.IsURL(req.Website) { return append(errFields, &validator.FormErrorField{ ErrorField: "website", ErrorMsg: reason.InvalidURLError, }), errors.BadRequest(reason.InvalidURLError) } return nil, nil } // UpdateUserInterfaceRequest update user interface request type UpdateUserInterfaceRequest struct { // language Language string `validate:"required,gt=1,lte=100" json:"language"` // Color scheme ColorScheme string `validate:"required,gt=1,lte=100" json:"color_scheme"` // user id UserId string `json:"-"` } func (req *UpdateUserInterfaceRequest) Check() (errFields []*validator.FormErrorField, err error) { if !translator.CheckLanguageIsValid(req.Language) { return nil, errors.BadRequest(reason.LangNotFound) } if req.ColorScheme != constant.ColorSchemeDefault && req.ColorScheme != constant.ColorSchemeLight && req.ColorScheme != constant.ColorSchemeDark && req.ColorScheme != constant.ColorSchemeSystem { req.ColorScheme = constant.ColorSchemeDefault } return nil, nil } type UserRetrievePassWordRequest struct { Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"` CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` } type UserRePassWordRequest struct { Code string `validate:"required,gt=0,lte=100" json:"code"` Pass string `validate:"required,gt=0,lte=32" json:"pass"` Content string `json:"-"` } func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField, err error) { if err = checker.CheckPassword(u.Pass); err != nil { errFields = append(errFields, &validator.FormErrorField{ ErrorField: "pass", ErrorMsg: err.Error(), }) return errFields, err } return nil, nil } type ActionRecordReq struct { Action string `validate:"required,oneof=email password edit_userinfo question answer comment edit invitation_answer search report delete vote" form:"action"` IP string `json:"-"` UserID string `json:"-"` } type ActionRecordResp struct { CaptchaID string `json:"captcha_id"` CaptchaImg string `json:"captcha_img"` Verify bool `json:"verify"` } type UserBasicInfo struct { ID string `json:"id"` Username string `json:"username"` Rank int `json:"rank"` DisplayName string `json:"display_name"` Avatar string `json:"avatar"` Website string `json:"website"` Location string `json:"location"` Language string `json:"language"` Status string `json:"status"` SuspendedUntil int64 `json:"suspended_until"` } type GetOtherUserInfoByUsernameReq struct { Username string `validate:"required,gt=0,lte=500" form:"username"` UserID string `json:"-"` IsAdmin bool `json:"-"` } type GetOtherUserInfoResp struct { Info *GetOtherUserInfoByUsernameResp `json:"info"` } type UserChangeEmailSendCodeReq struct { UserVerifyEmailSendReq Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"` Pass string `validate:"omitempty,gte=8,lte=32" json:"pass"` UserID string `json:"-"` } type UserChangeEmailVerifyReq struct { Code string `validate:"required,gt=0,lte=500" json:"code"` Content string `json:"-"` } type UserVerifyEmailSendReq struct { CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` } // UserRankingResp user ranking response type UserRankingResp struct { UsersWithTheMostReputation []*UserRankingSimpleInfo `json:"users_with_the_most_reputation"` UsersWithTheMostVote []*UserRankingSimpleInfo `json:"users_with_the_most_vote"` Staffs []*UserRankingSimpleInfo `json:"staffs"` } // UserRankingSimpleInfo user ranking simple info type UserRankingSimpleInfo struct { // username Username string `json:"username"` // rank Rank int `json:"rank"` // vote VoteCount int `json:"vote_count"` // display name DisplayName string `json:"display_name"` // avatar Avatar string `json:"avatar"` } // UserUnsubscribeNotificationReq user unsubscribe email notification request type UserUnsubscribeNotificationReq struct { Code string `validate:"required,gt=0,lte=500" json:"code"` Content string `json:"-"` } // GetUserStaffReq get user staff request type GetUserStaffReq struct { Username string `validate:"omitempty,gt=0,lte=500" form:"username"` PageSize int `validate:"omitempty,min=1" form:"page_size"` } // GetUserStaffResp get user staff response type GetUserStaffResp struct { // username Username string `json:"username"` // display name DisplayName string `json:"display_name"` // avatar Avatar string `json:"avatar"` } ================================================ FILE: internal/schema/vote_schema.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package schema type VoteReq struct { ObjectID string `validate:"required" json:"object_id"` IsCancel bool `validate:"omitempty" json:"is_cancel"` CaptchaID string `json:"captcha_id"` CaptchaCode string `json:"captcha_code"` UserID string `json:"-"` } type VoteResp struct { UpVotes int64 `json:"up_votes"` DownVotes int64 `json:"down_votes"` Votes int64 `json:"votes"` VoteStatus string `json:"vote_status"` } // VoteOperationInfo vote operation info type VoteOperationInfo struct { // operation object id ObjectID string // question answer comment ObjectType string // object owner user id ObjectCreatorUserID string // operation user id OperatingUserID string // vote up VoteUp bool // vote down VoteDown bool // vote activity info Activities []*VoteActivity } // VoteActivity vote activity type VoteActivity struct { ActivityType int ActivityUserID string TriggerUserID string Rank int } func (v *VoteActivity) HasRank() int { if v.Rank != 0 { return 1 } return 0 } type GetVoteWithPageReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // user id UserID string `json:"-"` } type GetVoteWithPageResp struct { // create time CreatedAt int64 `json:"created_at"` // object id ObjectID string `json:"object_id"` // question id QuestionID string `json:"question_id"` // answer id AnswerID string `json:"answer_id"` // object type ObjectType string `json:"object_type" enums:"question,answer,tag,comment"` // title Title string `json:"title"` // url title UrlTitle string `json:"url_title"` // content Content string `json:"content"` // vote type VoteType string `json:"vote_type"` } ================================================ FILE: internal/service/action/captcha_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package action import ( "context" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/pkg/token" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/log" ) // CaptchaRepo captcha repository type CaptchaRepo interface { SetCaptcha(ctx context.Context, key, captcha string) (err error) GetCaptcha(ctx context.Context, key string) (captcha string, err error) DelCaptcha(ctx context.Context, key string) (err error) SetActionType(ctx context.Context, unit, actionType, config string, amount int) (err error) GetActionType(ctx context.Context, unit, actionType string) (actioninfo *entity.ActionRecordInfo, err error) DelActionType(ctx context.Context, unit, actionType string) (err error) } // CaptchaService kit service type CaptchaService struct { captchaRepo CaptchaRepo } // NewCaptchaService captcha service func NewCaptchaService(captchaRepo CaptchaRepo) *CaptchaService { return &CaptchaService{ captchaRepo: captchaRepo, } } // ActionRecord action record func (cs *CaptchaService) ActionRecord(ctx context.Context, req *schema.ActionRecordReq) (resp *schema.ActionRecordResp, err error) { resp = &schema.ActionRecordResp{} unit := req.IP switch req.Action { case entity.CaptchaActionEditUserinfo: unit = req.UserID case entity.CaptchaActionQuestion: unit = req.UserID case entity.CaptchaActionAnswer: unit = req.UserID case entity.CaptchaActionComment: unit = req.UserID case entity.CaptchaActionEdit: unit = req.UserID case entity.CaptchaActionInvitationAnswer: unit = req.UserID case entity.CaptchaActionSearch: if req.UserID != "" { unit = req.UserID } case entity.CaptchaActionReport: unit = req.UserID case entity.CaptchaActionDelete: unit = req.UserID case entity.CaptchaActionVote: unit = req.UserID } verificationResult := cs.ValidationStrategy(ctx, unit, req.Action) if !verificationResult { resp.Verify = true resp.CaptchaID, resp.CaptchaImg, err = cs.GenerateCaptcha(ctx) if err != nil { log.Errorf("GenerateCaptcha error: %v", err) } } return } // ActionRecordVerifyCaptcha // Verify that you need to enter a CAPTCHA, and that the CAPTCHA is correct func (cs *CaptchaService) ActionRecordVerifyCaptcha( ctx context.Context, actionType string, unit string, captchaID string, captchaCode string, ) bool { verificationResult := cs.ValidationStrategy(ctx, unit, actionType) if verificationResult { return true } pass, err := cs.VerifyCaptcha(ctx, captchaID, captchaCode) if err != nil { return false } return pass } func (cs *CaptchaService) ActionRecordAdd(ctx context.Context, actionType string, unit string) { info, err := cs.captchaRepo.GetActionType(ctx, unit, actionType) if err != nil { log.Error(err) return } amount := 1 if info != nil { amount = info.Num + 1 } err = cs.captchaRepo.SetActionType(ctx, unit, actionType, "", amount) if err != nil { log.Error(err) } } func (cs *CaptchaService) ActionRecordDel(ctx context.Context, actionType string, unit string) { err := cs.captchaRepo.DelActionType(ctx, unit, actionType) if err != nil { log.Error(err) } } // GenerateCaptcha generate captcha func (cs *CaptchaService) GenerateCaptcha(ctx context.Context) (key, captchaBase64 string, err error) { realCaptcha := "" key = token.GenerateToken() _ = plugin.CallCaptcha(func(fn plugin.Captcha) error { if captcha, code := fn.Create(); len(code) > 0 { captchaBase64 = captcha realCaptcha = code } return nil }) if len(realCaptcha) == 0 { return key, captchaBase64, nil } err = cs.captchaRepo.SetCaptcha(ctx, key, realCaptcha) return key, captchaBase64, err } // VerifyCaptcha generate captcha func (cs *CaptchaService) VerifyCaptcha(ctx context.Context, key, captcha string) (isCorrect bool, err error) { realCaptcha, _ := cs.captchaRepo.GetCaptcha(ctx, key) _ = plugin.CallCaptcha(func(fn plugin.Captcha) error { isCorrect = fn.Verify(realCaptcha, captcha) return nil }) _ = cs.captchaRepo.DelCaptcha(ctx, key) return isCorrect, nil } ================================================ FILE: internal/service/action/captcha_strategy.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package action import ( "context" "time" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/log" "github.com/apache/answer/internal/entity" ) // ValidationStrategy // true pass // false need captcha func (cs *CaptchaService) ValidationStrategy(ctx context.Context, unit, actionType string) bool { // If the captcha is not enabled, the verification is passed directly if !plugin.CaptchaEnabled() { return true } info, err := cs.captchaRepo.GetActionType(ctx, unit, actionType) if err != nil { log.Error(err) return false } switch actionType { case entity.CaptchaActionEmail: return cs.CaptchaActionEmail(ctx, unit, info) case entity.CaptchaActionPassword: return cs.CaptchaActionPassword(ctx, unit, info) case entity.CaptchaActionEditUserinfo: return cs.CaptchaActionEditUserinfo(ctx, unit, info) case entity.CaptchaActionQuestion: return cs.CaptchaActionQuestion(ctx, unit, info) case entity.CaptchaActionAnswer: return cs.CaptchaActionAnswer(ctx, unit, info) case entity.CaptchaActionComment: return cs.CaptchaActionComment(ctx, unit, info) case entity.CaptchaActionEdit: return cs.CaptchaActionEdit(ctx, unit, info) case entity.CaptchaActionInvitationAnswer: return cs.CaptchaActionInvitationAnswer(ctx, unit, info) case entity.CaptchaActionSearch: return cs.CaptchaActionSearch(ctx, unit, info) case entity.CaptchaActionReport: return cs.CaptchaActionReport(ctx, unit, info) case entity.CaptchaActionDelete: return cs.CaptchaActionDelete(ctx, unit, info) case entity.CaptchaActionVote: return cs.CaptchaActionVote(ctx, unit, info) } // actionType not found return false } func (cs *CaptchaService) CaptchaActionEmail(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { // You need a verification code every time return false } func (cs *CaptchaService) CaptchaActionPassword(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } setNum := 3 setTime := int64(60 * 30) // seconds now := time.Now().Unix() if now-actionInfo.LastTime <= setTime && actionInfo.Num >= setNum { return false } if now-actionInfo.LastTime != 0 && now-actionInfo.LastTime > setTime { if err := cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionPassword, "", 0); err != nil { log.Error(err) } } return true } func (cs *CaptchaService) CaptchaActionEditUserinfo(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } setNum := 3 setTime := int64(60 * 30) // seconds now := time.Now().Unix() if now-actionInfo.LastTime <= setTime && actionInfo.Num >= setNum { return false } if now-actionInfo.LastTime != 0 && now-actionInfo.LastTime > setTime { if err := cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionEditUserinfo, "", 0); err != nil { log.Error(err) } } return true } func (cs *CaptchaService) CaptchaActionQuestion(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } setNum := 10 setTime := int64(5) // seconds now := time.Now().Unix() if now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum { return false } return true } func (cs *CaptchaService) CaptchaActionAnswer(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } setNum := 10 setTime := int64(5) // seconds now := time.Now().Unix() if now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum { return false } return true } func (cs *CaptchaService) CaptchaActionComment(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } setNum := 30 setTime := int64(1) // seconds now := time.Now().Unix() if now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum { return false } return true } func (cs *CaptchaService) CaptchaActionEdit(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } setNum := 10 return actionInfo.Num < setNum } func (cs *CaptchaService) CaptchaActionInvitationAnswer(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } setNum := 30 return actionInfo.Num < setNum } func (cs *CaptchaService) CaptchaActionSearch(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } now := time.Now().Unix() setNum := 20 setTime := int64(60) // seconds if now-actionInfo.LastTime <= setTime && actionInfo.Num >= setNum { return false } if now-actionInfo.LastTime > setTime { if err := cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionSearch, "", 0); err != nil { log.Error(err) } } return true } func (cs *CaptchaService) CaptchaActionReport(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } setNum := 30 setTime := int64(1) // seconds now := time.Now().Unix() if now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum { return false } return true } func (cs *CaptchaService) CaptchaActionDelete(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } setNum := 5 setTime := int64(5) // seconds now := time.Now().Unix() if now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum { return false } return true } func (cs *CaptchaService) CaptchaActionVote(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { if actionInfo == nil { return true } setNum := 40 return actionInfo.Num < setNum } ================================================ FILE: internal/service/activity/activity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity import ( "context" "encoding/json" "fmt" "strings" "github.com/apache/answer/internal/service/activity_common" metacommon "github.com/apache/answer/internal/service/meta_common" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/object_info" "github.com/apache/answer/internal/service/revision_common" "github.com/apache/answer/internal/service/tag_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/obj" "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/log" ) // ActivityRepo activity repository type ActivityRepo interface { GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (activityList []*entity.Activity, err error) } // ActivityService activity service type ActivityService struct { activityRepo ActivityRepo userCommon *usercommon.UserCommon activityCommonService *activity_common.ActivityCommon tagCommonService *tag_common.TagCommonService objectInfoService *object_info.ObjService commentCommonService *comment_common.CommentCommonService revisionService *revision_common.RevisionService metaService *metacommon.MetaCommonService configService *config.ConfigService } // NewActivityService new activity service func NewActivityService( activityRepo ActivityRepo, userCommon *usercommon.UserCommon, activityCommonService *activity_common.ActivityCommon, tagCommonService *tag_common.TagCommonService, objectInfoService *object_info.ObjService, commentCommonService *comment_common.CommentCommonService, revisionService *revision_common.RevisionService, metaService *metacommon.MetaCommonService, configService *config.ConfigService, ) *ActivityService { return &ActivityService{ objectInfoService: objectInfoService, activityRepo: activityRepo, userCommon: userCommon, activityCommonService: activityCommonService, tagCommonService: tagCommonService, commentCommonService: commentCommonService, revisionService: revisionService, metaService: metaService, configService: configService, } } // GetObjectTimeline get object timeline func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.GetObjectTimelineReq) ( resp *schema.GetObjectTimelineResp, err error) { resp = &schema.GetObjectTimelineResp{ ObjectInfo: &schema.ActObjectInfo{}, Timeline: make([]*schema.ActObjectTimeline, 0), } resp.ObjectInfo, err = as.getTimelineMainObjInfo(ctx, req.ObjectID) if err != nil { return nil, err } activityList, err := as.activityRepo.GetObjectAllActivity(ctx, req.ObjectID, req.ShowVote) if err != nil { return nil, err } for _, act := range activityList { item := &schema.ActObjectTimeline{ ActivityID: act.ID, RevisionID: converter.IntToString(act.RevisionID), CreatedAt: act.CreatedAt.Unix(), Cancelled: act.Cancelled == entity.ActivityCancelled, ObjectID: act.ObjectID, UserInfo: &schema.UserBasicInfo{}, } item.ObjectType, _ = obj.GetObjectTypeStrByObjectID(act.ObjectID) if item.Cancelled { item.CancelledAt = act.CancelledAt.Unix() } if item.ObjectType == constant.QuestionObjectType || item.ObjectType == constant.AnswerObjectType { if handler.GetEnableShortID(ctx) { item.ObjectID = uid.EnShortID(act.ObjectID) } } cfg, err := as.configService.GetConfigByID(ctx, act.ActivityType) if err != nil { log.Errorf("fail to get config by id: %d, err: %v, act id is: %s", act.ActivityType, err, act.ID) } else { // database save activity type is number, change to activity type string is like "question.asked". // so we need to cut the front part of '.', only need string like 'asked' _, item.ActivityType, _ = strings.Cut(cfg.Key, ".") // format activity type string to show if isHidden, formattedActivityType := formatActivity(item.ActivityType); isHidden { continue } else { item.ActivityType = formattedActivityType } } // if activity is down vote, only admin can see who does it. if item.ActivityType == constant.ActDownVote && !req.IsAdmin { item.UserInfo.Username = "N/A" item.UserInfo.DisplayName = "N/A" } else { if act.TriggerUserID > 0 { item.UserInfo.ID = fmt.Sprintf("%d", act.TriggerUserID) } else { item.UserInfo.ID = act.UserID } } item.Comment = as.getTimelineActivityComment(ctx, item.ObjectID, item.ObjectType, item.ActivityType, item.RevisionID) resp.Timeline = append(resp.Timeline, item) } as.formatTimelineUserInfo(ctx, resp.Timeline) return } func (as *ActivityService) getTimelineMainObjInfo(ctx context.Context, objectID string) ( resp *schema.ActObjectInfo, err error) { resp = &schema.ActObjectInfo{} objInfo, err := as.objectInfoService.GetInfo(ctx, objectID) if err != nil { return nil, err } resp.Title = objInfo.Title if objInfo.ObjectType == constant.TagObjectType { tag, exist, _ := as.tagCommonService.GetTagByID(ctx, objInfo.TagID) if exist { resp.Title = tag.SlugName resp.MainTagSlugName = tag.MainTagSlugName } } resp.ObjectType = objInfo.ObjectType resp.QuestionID = objInfo.QuestionID resp.AnswerID = objInfo.AnswerID if len(objInfo.ObjectCreatorUserID) > 0 { // get object creator user info userBasicInfo, exist, err := as.userCommon.GetUserBasicInfoByID(ctx, objInfo.ObjectCreatorUserID) if err != nil { return nil, err } if exist { resp.Username = userBasicInfo.Username resp.DisplayName = userBasicInfo.DisplayName } } return resp, nil } func (as *ActivityService) getTimelineActivityComment(ctx context.Context, objectID, objectType, activityType, revisionID string) (comment string) { if objectType == constant.CommentObjectType { commentInfo, err := as.commentCommonService.GetComment(ctx, objectID) if err != nil { log.Error(err) } else { return commentInfo.ParsedText } return } if activityType == constant.ActEdited { revision, err := as.revisionService.GetRevision(ctx, revisionID) if err != nil { log.Error(err) } else { return converter.Markdown2HTML(revision.Log) } return } if activityType == constant.ActClosed { // only question can be closed metaInfo, err := as.metaService.GetMetaByObjectIdAndKey(ctx, objectID, entity.QuestionCloseReasonKey) if err != nil { log.Error(err) } else { closeMsg := &schema.CloseQuestionMeta{} if err := json.Unmarshal([]byte(metaInfo.Value), closeMsg); err == nil { return converter.Markdown2HTML(closeMsg.CloseMsg) } } } return "" } func (as *ActivityService) formatTimelineUserInfo(ctx context.Context, timeline []*schema.ActObjectTimeline) { userExist := make(map[string]bool) userIDs := make([]string, 0) for _, info := range timeline { if len(info.UserInfo.ID) == 0 || userExist[info.UserInfo.ID] { continue } userIDs = append(userIDs, info.UserInfo.ID) } if len(userIDs) == 0 { return } userInfoMapping, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs) if err != nil { log.Error(err) return } for _, info := range timeline { if len(info.UserInfo.ID) == 0 { continue } info.UserInfo = userInfoMapping[info.UserInfo.ID] } } // GetObjectTimelineDetail get object timeline func (as *ActivityService) GetObjectTimelineDetail(ctx context.Context, req *schema.GetObjectTimelineDetailReq) ( resp *schema.GetObjectTimelineDetailResp, err error) { resp = &schema.GetObjectTimelineDetailResp{} resp.OldRevision, _ = as.getOneObjectDetail(ctx, req.OldRevisionID) resp.NewRevision, _ = as.getOneObjectDetail(ctx, req.NewRevisionID) return resp, nil } // getOneObjectDetail get object detail func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID string) ( resp *schema.ObjectTimelineDetail, err error) { resp = &schema.ObjectTimelineDetail{Tags: make([]*schema.ObjectTimelineTag, 0)} // if request revision is 0, return null object detail. if revisionID == "0" { return nil, nil } revision, err := as.revisionService.GetRevision(ctx, revisionID) if err != nil { log.Warn(err) return nil, nil } objInfo, err := as.objectInfoService.GetInfo(ctx, revision.ObjectID) if err != nil { return nil, err } switch objInfo.ObjectType { case constant.QuestionObjectType: data := &entity.QuestionWithTagsRevision{} if err = json.Unmarshal([]byte(revision.Content), data); err != nil { log.Errorf("revision parsing error %s", err) return resp, nil } for _, tag := range data.Tags { resp.Tags = append(resp.Tags, &schema.ObjectTimelineTag{ SlugName: tag.SlugName, DisplayName: tag.DisplayName, MainTagSlugName: tag.MainTagSlugName, Recommend: tag.Recommend, Reserved: tag.Reserved, }) } resp.Title = data.Title resp.OriginalText = data.OriginalText case constant.AnswerObjectType: data := &entity.Answer{} if err = json.Unmarshal([]byte(revision.Content), data); err != nil { log.Errorf("revision parsing error %s", err) return resp, nil } resp.Title = objInfo.Title // answer show question title resp.OriginalText = data.OriginalText case constant.TagObjectType: data := &entity.Tag{} if err = json.Unmarshal([]byte(revision.Content), data); err != nil { log.Errorf("revision parsing error %s", err) return resp, nil } resp.Title = data.DisplayName resp.OriginalText = data.OriginalText resp.SlugName = data.SlugName resp.MainTagSlugName = data.MainTagSlugName default: log.Errorf("unknown object type %s", objInfo.ObjectType) } return resp, nil } func formatActivity(activityType string) (isHidden bool, formattedActivityType string) { if activityType == constant.ActVotedUp || activityType == constant.ActVotedDown || activityType == constant.ActFollow { return true, "" } if activityType == constant.ActVoteUp { return false, constant.ActUpVote } if activityType == constant.ActVoteDown { return false, constant.ActDownVote } if activityType == constant.ActAccepted { return false, constant.ActAccept } return false, activityType } ================================================ FILE: internal/service/activity/answer_activity_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity import ( "context" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_type" "github.com/apache/answer/internal/service/config" "github.com/segmentfault/pacman/log" ) // AnswerActivityRepo answer activity type AnswerActivityRepo interface { SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (err error) SaveCancelAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (err error) } // AnswerActivityService answer activity service type AnswerActivityService struct { answerActivityRepo AnswerActivityRepo configService *config.ConfigService } // NewAnswerActivityService new comment service func NewAnswerActivityService( answerActivityRepo AnswerActivityRepo, configService *config.ConfigService, ) *AnswerActivityService { return &AnswerActivityService{ answerActivityRepo: answerActivityRepo, configService: configService, } } // AcceptAnswer accept answer change activity func (as *AnswerActivityService) AcceptAnswer(ctx context.Context, loginUserID, answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) { log.Debugf("user %s want to accept answer %s[%s] for question %s[%s]", loginUserID, answerObjID, answerUserID, questionObjID, questionUserID) operationInfo := as.createAcceptAnswerOperationInfo(ctx, loginUserID, answerObjID, questionObjID, questionUserID, answerUserID, isSelf) return as.answerActivityRepo.SaveAcceptAnswerActivity(ctx, operationInfo) } // CancelAcceptAnswer cancel accept answer change activity func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context, loginUserID, answerObjID, questionObjID, questionUserID, answerUserID string) (err error) { operationInfo := as.createAcceptAnswerOperationInfo(ctx, loginUserID, answerObjID, questionObjID, questionUserID, answerUserID, false) return as.answerActivityRepo.SaveCancelAcceptAnswerActivity(ctx, operationInfo) } func (as *AnswerActivityService) createAcceptAnswerOperationInfo(ctx context.Context, loginUserID, answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) *schema.AcceptAnswerOperationInfo { operationInfo := &schema.AcceptAnswerOperationInfo{ TriggerUserID: loginUserID, QuestionObjectID: questionObjID, QuestionUserID: questionUserID, AnswerObjectID: answerObjID, AnswerUserID: answerUserID, } operationInfo.Activities = as.getActivities(ctx, operationInfo) if isSelf { for _, activity := range operationInfo.Activities { activity.Rank = 0 } } return operationInfo } func (as *AnswerActivityService) getActivities(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ( activities []*schema.AcceptAnswerActivity) { activities = make([]*schema.AcceptAnswerActivity, 0) for _, action := range []string{activity_type.AnswerAccept, activity_type.AnswerAccepted} { t := &schema.AcceptAnswerActivity{} cfg, err := as.configService.GetConfigByKey(ctx, action) if err != nil { log.Warnf("get config by key error: %v", err) continue } t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue() if action == activity_type.AnswerAccept { t.ActivityUserID = op.QuestionUserID t.TriggerUserID = op.TriggerUserID t.OriginalObjectID = op.QuestionObjectID // if activity is 'accept' means this question is accept the answer. } else { t.ActivityUserID = op.AnswerUserID t.TriggerUserID = op.TriggerUserID t.OriginalObjectID = op.AnswerObjectID // if activity is 'accepted' means this answer was accepted. } activities = append(activities, t) } return activities } ================================================ FILE: internal/service/activity/review_active.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity import ( "context" "github.com/apache/answer/internal/schema" ) // ReviewActivityRepo interface type ReviewActivityRepo interface { Review(ctx context.Context, sct *schema.PassReviewActivity) (err error) } ================================================ FILE: internal/service/activity/user_active.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity import "context" // UserActiveActivityRepo interface type UserActiveActivityRepo interface { UserActive(ctx context.Context, userID string) (err error) } ================================================ FILE: internal/service/activity_common/activity.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity_common import ( "context" "time" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activityqueue" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) type ActivityRepo interface { GetActivityTypeByObjID(ctx context.Context, objectId string, action string) (activityType, rank int, hasRank int, err error) GetActivityTypeByObjectType(ctx context.Context, objectKey, action string) (activityType int, err error) GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) ( existsActivity *entity.Activity, exist bool, err error) GetUserActivitiesByActivityType(ctx context.Context, userID string, activityType int) (activityList []*entity.Activity, err error) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) AddActivity(ctx context.Context, activity *entity.Activity) (err error) GetUsersWhoHasGainedTheMostReputation( ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) GetUsersWhoHasVoteMost( ctx context.Context, startTime, endTime time.Time, limit int) (voteStat []*entity.ActivityUserVoteStat, err error) } type ActivityCommon struct { activityRepo ActivityRepo activityQueueService activityqueue.Service } // NewActivityCommon new activity common func NewActivityCommon( activityRepo ActivityRepo, activityQueueService activityqueue.Service, ) *ActivityCommon { activity := &ActivityCommon{ activityRepo: activityRepo, activityQueueService: activityQueueService, } activity.activityQueueService.RegisterHandler(activity.HandleActivity) return activity } // HandleActivity handle activity message func (ac *ActivityCommon) HandleActivity(ctx context.Context, msg *schema.ActivityMsg) error { activityType, err := ac.activityRepo.GetActivityTypeByConfigKey(ctx, string(msg.ActivityTypeKey)) if err != nil { log.Errorf("error getting activity type %s, activity type is %d", err, activityType) return err } act := &entity.Activity{ UserID: msg.UserID, TriggerUserID: msg.TriggerUserID, ObjectID: uid.DeShortID(msg.ObjectID), OriginalObjectID: uid.DeShortID(msg.OriginalObjectID), ActivityType: activityType, Cancelled: entity.ActivityAvailable, } if len(msg.RevisionID) > 0 { act.RevisionID = converter.StringToInt64(msg.RevisionID) } if err := ac.activityRepo.AddActivity(ctx, act); err != nil { return err } return nil } ================================================ FILE: internal/service/activity_common/follow.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity_common import "context" type FollowRepo interface { GetFollowIDs(ctx context.Context, userID, objectType string) (followIDs []string, err error) GetFollowAmount(ctx context.Context, objectID string) (followAmount int, err error) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) IsFollowed(ctx context.Context, userId, objectId string) (bool, error) MigrateFollowers(ctx context.Context, sourceObjectID, targetObjectID, action string) error } ================================================ FILE: internal/service/activity_common/vote.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity_common import ( "context" ) // VoteRepo activity repository type VoteRepo interface { GetVoteStatus(ctx context.Context, objectId, userId string) (status string) GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) } ================================================ FILE: internal/service/activity_type/activity_type.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activity_type const ( QuestionVoteUp = "question.vote_up" QuestionVoteDown = "question.vote_down" QuestionVotedUp = "question.voted_up" QuestionVotedDown = "question.voted_down" AnswerVoteUp = "answer.vote_up" AnswerVoteDown = "answer.vote_down" AnswerVotedUp = "answer.voted_up" AnswerVotedDown = "answer.voted_down" AnswerAccepted = "answer.accepted" AnswerAccept = "answer.accept" CommentVoteUp = "comment.vote_up" EditAccepted = "edit.accepted" ) var ( ActivityTypeList = []string{ QuestionVoteUp, QuestionVoteDown, QuestionVotedUp, QuestionVotedDown, AnswerVoteUp, AnswerVoteDown, AnswerVotedUp, AnswerVotedDown, AnswerAccepted, AnswerAccept, CommentVoteUp, } VoteActivityTypeList = []string{ QuestionVoteUp, QuestionVoteDown, QuestionVotedUp, QuestionVotedDown, AnswerVoteUp, AnswerVoteDown, AnswerVotedUp, AnswerVotedDown, CommentVoteUp, } ActivityTypeFlagMapping = map[string]string{ QuestionVoteUp: "action_activity_type.upvote", QuestionVoteDown: "action_activity_type.downvote", QuestionVotedUp: "action_activity_type.upvoted", QuestionVotedDown: "action_activity_type.downvoted", AnswerVoteUp: "action_activity_type.upvote", AnswerVoteDown: "action_activity_type.downvote", AnswerVotedUp: "action_activity_type.upvoted", AnswerVotedDown: "action_activity_type.downvoted", AnswerAccepted: "action_activity_type.accepted", AnswerAccept: "action_activity_type.accept", CommentVoteUp: "action_activity_type.upvote", EditAccepted: "action_activity_type.edit", } ) ================================================ FILE: internal/service/activityqueue/activity_queue.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package activityqueue import ( "github.com/apache/answer/internal/base/queue" "github.com/apache/answer/internal/schema" ) type Service queue.Service[*schema.ActivityMsg] func NewService() Service { return queue.New[*schema.ActivityMsg]("activity", 128) } ================================================ FILE: internal/service/ai_conversation/ai_conversation_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package ai_conversation import ( "context" "strings" "time" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/repo/ai_conversation" "github.com/apache/answer/internal/schema" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // AIConversationService type AIConversationService interface { CreateConversation(ctx context.Context, userID, conversationID, topic string) error SaveConversationRecords(ctx context.Context, conversationID, chatcmplID string, records []*ConversationMessage) error GetConversationList(ctx context.Context, req *schema.AIConversationListReq) (*pager.PageModel, error) GetConversationDetail(ctx context.Context, req *schema.AIConversationDetailReq) (resp *schema.AIConversationDetailResp, exist bool, err error) VoteRecord(ctx context.Context, req *schema.AIConversationVoteReq) error GetConversationListForAdmin(ctx context.Context, req *schema.AIConversationAdminListReq) (*pager.PageModel, error) GetConversationDetailForAdmin(ctx context.Context, req *schema.AIConversationAdminDetailReq) (*schema.AIConversationAdminDetailResp, error) DeleteConversationForAdmin(ctx context.Context, req *schema.AIConversationAdminDeleteReq) error } // ConversationMessage type ConversationMessage struct { ChatCompletionID string `json:"chat_completion_id"` Role string `json:"role"` Content string `json:"content"` } // aiConversationService type aiConversationService struct { aiConversationRepo ai_conversation.AIConversationRepo userCommon *usercommon.UserCommon } // NewAIConversationService func NewAIConversationService( aiConversationRepo ai_conversation.AIConversationRepo, userCommon *usercommon.UserCommon, ) AIConversationService { return &aiConversationService{ aiConversationRepo: aiConversationRepo, userCommon: userCommon, } } // CreateConversation func (s *aiConversationService) CreateConversation(ctx context.Context, userID, conversationID, topic string) error { conversation := &entity.AIConversation{ ConversationID: conversationID, Topic: topic, UserID: userID, } err := s.aiConversationRepo.CreateConversation(ctx, conversation) if err != nil { log.Errorf("create conversation failed: %v", err) return err } return nil } // SaveConversationRecords func (s *aiConversationService) SaveConversationRecords(ctx context.Context, conversationID, chatcmplID string, records []*ConversationMessage) error { conversation, exist, err := s.aiConversationRepo.GetConversation(ctx, conversationID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err) } if !exist { return errors.BadRequest(reason.ObjectNotFound) } content := strings.Builder{} for _, record := range records { if len(record.ChatCompletionID) > 0 { continue } if record.Role == "user" { aiRecord := &entity.AIConversationRecord{ ConversationID: conversationID, ChatCompletionID: chatcmplID, Role: "user", Content: record.Content, } err = s.aiConversationRepo.CreateRecord(ctx, aiRecord) if err != nil { log.Errorf("create conversation record failed: %v", err) return errors.InternalServer(reason.DatabaseError).WithError(err) } continue } content.WriteString(record.Content) content.WriteString("\n") } aiRecord := &entity.AIConversationRecord{ ConversationID: conversationID, ChatCompletionID: chatcmplID, Role: "assistant", Content: content.String(), Helpful: 0, Unhelpful: 0, } err = s.aiConversationRepo.CreateRecord(ctx, aiRecord) if err != nil { log.Errorf("create conversation record failed: %v", err) return errors.InternalServer(reason.DatabaseError).WithError(err) } conversation.UpdatedAt = time.Now() err = s.aiConversationRepo.UpdateConversation(ctx, conversation) if err != nil { log.Errorf("update conversation failed: %v", err) return errors.InternalServer(reason.DatabaseError).WithError(err) } return nil } // GetConversationList func (s *aiConversationService) GetConversationList(ctx context.Context, req *schema.AIConversationListReq) (*pager.PageModel, error) { conversations, total, err := s.aiConversationRepo.GetConversationsPage(ctx, req.Page, req.PageSize, &entity.AIConversation{UserID: req.UserID}) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err) } list := make([]schema.AIConversationListItem, 0, len(conversations)) for _, conversation := range conversations { list = append(list, schema.AIConversationListItem{ ConversationID: conversation.ConversationID, CreatedAt: conversation.CreatedAt.Unix(), Topic: conversation.Topic, }) } return pager.NewPageModel(total, list), nil } // GetConversationDetail func (s *aiConversationService) GetConversationDetail(ctx context.Context, req *schema.AIConversationDetailReq) ( resp *schema.AIConversationDetailResp, exist bool, err error) { conversation, exist, err := s.aiConversationRepo.GetConversation(ctx, req.ConversationID) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err) } if !exist || conversation.UserID != req.UserID { return nil, false, nil } records, err := s.aiConversationRepo.GetRecordsByConversationID(ctx, req.ConversationID) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err) } recordList := make([]*schema.AIConversationRecord, 0, len(records)) for i, record := range records { if i == 0 { record.Content = conversation.Topic } recordList = append(recordList, &schema.AIConversationRecord{ ChatCompletionID: record.ChatCompletionID, Role: record.Role, Content: record.Content, Helpful: record.Helpful, Unhelpful: record.Unhelpful, CreatedAt: record.CreatedAt.Unix(), }) } return &schema.AIConversationDetailResp{ ConversationID: conversation.ConversationID, Topic: conversation.Topic, Records: recordList, CreatedAt: conversation.CreatedAt.Unix(), UpdatedAt: conversation.UpdatedAt.Unix(), }, true, nil } // VoteRecord func (s *aiConversationService) VoteRecord(ctx context.Context, req *schema.AIConversationVoteReq) error { record, exist, err := s.aiConversationRepo.GetRecordByChatCompletionID(ctx, "assistant", req.ChatCompletionID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err) } if !exist { return errors.BadRequest(reason.ObjectNotFound) } conversation, exist, err := s.aiConversationRepo.GetConversation(ctx, record.ConversationID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err) } if !exist { return errors.BadRequest(reason.ObjectNotFound) } if conversation.UserID != req.UserID { return errors.Forbidden(reason.UnauthorizedError) } if record.Role != "assistant" { return errors.BadRequest("Only AI responses can be voted") } if req.VoteType == "helpful" { if req.Cancel { record.Helpful = 0 } else { record.Helpful = 1 record.Unhelpful = 0 } } else { if req.Cancel { record.Unhelpful = 0 } else { record.Unhelpful = 1 record.Helpful = 0 } } err = s.aiConversationRepo.UpdateRecordVote(ctx, record) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err) } return nil } // GetConversationListForAdmin func (s *aiConversationService) GetConversationListForAdmin( ctx context.Context, req *schema.AIConversationAdminListReq) (*pager.PageModel, error) { conversations, total, err := s.aiConversationRepo.GetConversationsForAdmin(ctx, req.Page, req.PageSize, &entity.AIConversation{}) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err) } list := make([]*schema.AIConversationAdminListItem, 0, len(conversations)) for _, conversation := range conversations { userInfo, err := s.getUserInfo(ctx, conversation.UserID) if err != nil { log.Errorf("get user info failed for user %s: %v", conversation.UserID, err) continue } helpful, unhelpful, err := s.aiConversationRepo.GetConversationWithVoteStats(ctx, conversation.ConversationID) if err != nil { log.Errorf("get conversation vote stats failed for conversation %s: %v", conversation.ConversationID, err) continue } list = append(list, &schema.AIConversationAdminListItem{ ID: conversation.ConversationID, Topic: conversation.Topic, UserInfo: userInfo, HelpfulCount: helpful, UnhelpfulCount: unhelpful, CreatedAt: conversation.CreatedAt.Unix(), }) } return pager.NewPageModel(total, list), nil } // GetConversationDetailForAdmin func (s *aiConversationService) GetConversationDetailForAdmin(ctx context.Context, req *schema.AIConversationAdminDetailReq) (*schema.AIConversationAdminDetailResp, error) { conversation, exist, err := s.aiConversationRepo.GetConversation(ctx, req.ConversationID) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err) } if !exist { return nil, errors.BadRequest(reason.ObjectNotFound) } userInfo, err := s.getUserInfo(ctx, conversation.UserID) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err) } records, err := s.aiConversationRepo.GetRecordsByConversationID(ctx, req.ConversationID) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err) } recordList := make([]schema.AIConversationRecord, 0, len(records)) for i, record := range records { if i == 0 { record.Content = conversation.Topic } recordList = append(recordList, schema.AIConversationRecord{ ChatCompletionID: record.ChatCompletionID, Role: record.Role, Content: record.Content, Helpful: record.Helpful, Unhelpful: record.Unhelpful, CreatedAt: record.CreatedAt.Unix(), }) } return &schema.AIConversationAdminDetailResp{ ConversationID: conversation.ConversationID, Topic: conversation.Topic, UserInfo: userInfo, Records: recordList, CreatedAt: conversation.CreatedAt.Unix(), }, nil } // getUserInfo func (s *aiConversationService) getUserInfo(ctx context.Context, userID string) (schema.AIConversationUserInfo, error) { userInfo := schema.AIConversationUserInfo{} user, exist, err := s.userCommon.GetUserBasicInfoByID(ctx, userID) if err != nil { return userInfo, err } if !exist { return userInfo, errors.BadRequest(reason.ObjectNotFound) } userInfo.ID = user.ID userInfo.Username = user.Username userInfo.DisplayName = user.DisplayName userInfo.Avatar = user.Avatar userInfo.Rank = user.Rank return userInfo, nil } // DeleteConversationForAdmin func (s *aiConversationService) DeleteConversationForAdmin(ctx context.Context, req *schema.AIConversationAdminDeleteReq) error { _, exist, err := s.aiConversationRepo.GetConversation(ctx, req.ConversationID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err) } if !exist { return errors.BadRequest(reason.ObjectNotFound) } if err := s.aiConversationRepo.DeleteConversation(ctx, req.ConversationID); err != nil { return err } return nil } ================================================ FILE: internal/service/answer_common/answer.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package answercommon import ( "context" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/uid" ) type AnswerRepo interface { AddAnswer(ctx context.Context, answer *entity.Answer) (err error) RemoveAnswer(ctx context.Context, id string) (err error) RecoverAnswer(ctx context.Context, answerID string) (err error) UpdateAnswer(ctx context.Context, answer *entity.Answer, cols []string) (err error) GetAnswer(ctx context.Context, id string) (answer *entity.Answer, exist bool, err error) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error) UpdateAcceptedStatus(ctx context.Context, acceptedAnswerID string, questionID string) error GetByID(ctx context.Context, answerID string) (*entity.Answer, bool, error) GetByIDs(ctx context.Context, answerIDs ...string) ([]*entity.Answer, error) GetCountByQuestionID(ctx context.Context, questionID string) (int64, error) GetCountByUserID(ctx context.Context, userID string) (int64, error) GetIDsByUserIDAndQuestionID(ctx context.Context, userID string, questionID string) ([]string, error) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) GetPersonalAnswerPage(ctx context.Context, cond *entity.PersonalAnswerPageQueryCond) ( resp []*entity.Answer, total int64, err error) AdminSearchList(ctx context.Context, search *schema.AdminAnswerPageReq) ([]*entity.Answer, int64, error) UpdateAnswerStatus(ctx context.Context, answerID string, status int) (err error) GetAnswerCount(ctx context.Context) (count int64, err error) RemoveAllUserAnswer(ctx context.Context, userID string) (err error) SumVotesByQuestionID(ctx context.Context, questionID string) (float64, error) DeletePermanentlyAnswers(ctx context.Context) (err error) } // AnswerCommon user service type AnswerCommon struct { answerRepo AnswerRepo } func NewAnswerCommon(answerRepo AnswerRepo) *AnswerCommon { return &AnswerCommon{ answerRepo: answerRepo, } } func (as *AnswerCommon) SearchAnswerIDs(ctx context.Context, userID, questionID string) ([]string, error) { ids, err := as.answerRepo.GetIDsByUserIDAndQuestionID(ctx, userID, questionID) if err != nil { return nil, err } return ids, nil } func (as *AnswerCommon) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) ( resp []*entity.Answer, count int64, err error) { resp, count, err = as.answerRepo.AdminSearchList(ctx, req) if handler.GetEnableShortID(ctx) { for _, item := range resp { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return resp, count, err } func (as *AnswerCommon) Search(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) { list, count, err := as.answerRepo.SearchList(ctx, search) if err != nil { return list, count, err } return list, count, err } func (as *AnswerCommon) PersonalAnswerPage(ctx context.Context, cond *entity.PersonalAnswerPageQueryCond) ([]*entity.Answer, int64, error) { return as.answerRepo.GetPersonalAnswerPage(ctx, cond) } func (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo { info := schema.AnswerInfo{} info.ID = data.ID info.QuestionID = data.QuestionID info.Content = data.OriginalText info.HTML = data.ParsedText info.Accepted = data.Accepted info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() if data.UpdatedAt.Unix() < 1 { info.UpdateTime = 0 } info.UserID = data.UserID info.UpdateUserID = data.LastEditUserID info.Status = data.Status info.MemberActions = make([]*schema.PermissionMemberAction, 0) return &info } func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer) *schema.AdminAnswerInfo { info := schema.AdminAnswerInfo{} info.ID = data.ID info.QuestionID = data.QuestionID info.Accepted = data.Accepted info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() if data.UpdatedAt.Unix() < 1 { info.UpdateTime = 0 } info.UserID = data.UserID info.UpdateUserID = data.LastEditUserID info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240) return &info } ================================================ FILE: internal/service/apikey/apikey_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package apikey import ( "context" "strings" "time" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/pkg/token" ) type APIKeyRepo interface { GetAPIKeyList(ctx context.Context) (keys []*entity.APIKey, err error) GetAPIKey(ctx context.Context, apiKey string) (key *entity.APIKey, exist bool, err error) UpdateAPIKey(ctx context.Context, apiKey entity.APIKey) (err error) AddAPIKey(ctx context.Context, apiKey entity.APIKey) (err error) DeleteAPIKey(ctx context.Context, id int) (err error) } type APIKeyService struct { apiKeyRepo APIKeyRepo } func NewAPIKeyService( apiKeyRepo APIKeyRepo, ) *APIKeyService { return &APIKeyService{ apiKeyRepo: apiKeyRepo, } } func (s *APIKeyService) GetAPIKeyList(ctx context.Context, req *schema.GetAPIKeyReq) (resp []*schema.GetAPIKeyResp, err error) { keys, err := s.apiKeyRepo.GetAPIKeyList(ctx) if err != nil { return nil, err } resp = make([]*schema.GetAPIKeyResp, 0) for _, key := range keys { // hide access key middle part, replace with * if len(key.AccessKey) < 10 { // If the access key is too short, do not mask it key.AccessKey = strings.Repeat("*", len(key.AccessKey)) } else { key.AccessKey = key.AccessKey[:7] + strings.Repeat("*", 8) + key.AccessKey[len(key.AccessKey)-4:] } resp = append(resp, &schema.GetAPIKeyResp{ ID: key.ID, AccessKey: key.AccessKey, Description: key.Description, Scope: key.Scope, CreatedAt: key.CreatedAt.Unix(), LastUsedAt: key.LastUsedAt.Unix(), }) } return resp, nil } func (s *APIKeyService) UpdateAPIKey(ctx context.Context, req *schema.UpdateAPIKeyReq) (err error) { apiKey := entity.APIKey{ ID: req.ID, Description: req.Description, } err = s.apiKeyRepo.UpdateAPIKey(ctx, apiKey) if err != nil { return err } return nil } func (s *APIKeyService) AddAPIKey(ctx context.Context, req *schema.AddAPIKeyReq) (resp *schema.AddAPIKeyResp, err error) { ak := "sk_" + strings.ReplaceAll(token.GenerateToken(), "-", "") apiKey := entity.APIKey{ Description: req.Description, AccessKey: ak, Scope: req.Scope, LastUsedAt: time.Now(), UserID: req.UserID, } err = s.apiKeyRepo.AddAPIKey(ctx, apiKey) if err != nil { return nil, err } resp = &schema.AddAPIKeyResp{ AccessKey: apiKey.AccessKey, } return resp, nil } func (s *APIKeyService) DeleteAPIKey(ctx context.Context, req *schema.DeleteAPIKeyReq) (err error) { err = s.apiKeyRepo.DeleteAPIKey(ctx, req.ID) if err != nil { return err } return nil } ================================================ FILE: internal/service/auth/auth.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package auth import ( "context" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/apikey" "github.com/apache/answer/pkg/token" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/log" ) // AuthRepo auth repository type AuthRepo interface { GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) SetUserCacheInfo(ctx context.Context, accessToken, visitToken string, userInfo *entity.UserCacheInfo) error GetUserVisitCacheInfo(ctx context.Context, visitToken string) (accessToken string, err error) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) RemoveUserVisitCacheInfo(ctx context.Context, visitToken string) (err error) SetUserStatus(ctx context.Context, userID string, userInfo *entity.UserCacheInfo) (err error) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) RemoveUserStatus(ctx context.Context, userID string) (err error) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) RemoveUserTokens(ctx context.Context, userID string, remainToken string) } // AuthService kit service type AuthService struct { authRepo AuthRepo apiKeyRepo apikey.APIKeyRepo } // NewAuthService email service func NewAuthService(authRepo AuthRepo, apiKeyRepo apikey.APIKeyRepo) *AuthService { return &AuthService{ authRepo: authRepo, apiKeyRepo: apiKeyRepo, } } func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { userCacheInfo, err := as.authRepo.GetUserCacheInfo(ctx, accessToken) if err != nil { return nil, err } if userCacheInfo == nil { return nil, nil } cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID) if cacheInfo != nil { userCacheInfo.UserStatus = cacheInfo.UserStatus userCacheInfo.EmailStatus = cacheInfo.EmailStatus userCacheInfo.RoleID = cacheInfo.RoleID // update current user cache info err := as.authRepo.SetUserCacheInfo(ctx, accessToken, userCacheInfo.VisitToken, userCacheInfo) if err != nil { return nil, err } } // try to get user status from user center uc, ok := plugin.GetUserCenter() if ok && len(userCacheInfo.ExternalID) > 0 { if userStatus := uc.UserStatus(userCacheInfo.ExternalID); userStatus != plugin.UserStatusAvailable { userCacheInfo.UserStatus = int(userStatus) } } return userCacheInfo, nil } func (as *AuthService) SetUserCacheInfo(ctx context.Context, userInfo *entity.UserCacheInfo) ( accessToken string, visitToken string, err error) { accessToken = token.GenerateToken() visitToken = token.GenerateToken() err = as.authRepo.SetUserCacheInfo(ctx, accessToken, visitToken, userInfo) if err != nil { return "", "", err } return accessToken, visitToken, err } func (as *AuthService) CheckUserVisitToken(ctx context.Context, visitToken string) bool { accessToken, err := as.authRepo.GetUserVisitCacheInfo(ctx, visitToken) if err != nil { return false } if len(accessToken) == 0 { return false } return true } func (as *AuthService) SetUserStatus(ctx context.Context, userInfo *entity.UserCacheInfo) (err error) { return as.authRepo.SetUserStatus(ctx, userInfo.UserID, userInfo) } func (as *AuthService) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) { return as.authRepo.RemoveUserCacheInfo(ctx, accessToken) } func (as *AuthService) RemoveUserVisitCacheInfo(ctx context.Context, visitToken string) (err error) { if len(visitToken) > 0 { return as.authRepo.RemoveUserVisitCacheInfo(ctx, visitToken) } return nil } // AddUserTokenMapping add user token mapping func (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) { return as.authRepo.AddUserTokenMapping(ctx, userID, accessToken) } // RemoveUserAllTokens Log out all users under this user id func (as *AuthService) RemoveUserAllTokens(ctx context.Context, userID string) { as.authRepo.RemoveUserTokens(ctx, userID, "") } // RemoveTokensExceptCurrentUser remove all tokens except the current user func (as *AuthService) RemoveTokensExceptCurrentUser(ctx context.Context, userID string, accessToken string) { as.authRepo.RemoveUserTokens(ctx, userID, accessToken) } // Admin func (as *AuthService) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { return as.authRepo.GetAdminUserCacheInfo(ctx, accessToken) } func (as *AuthService) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { err = as.authRepo.SetAdminUserCacheInfo(ctx, accessToken, userInfo) return err } func (as *AuthService) RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) { return as.authRepo.RemoveAdminUserCacheInfo(ctx, accessToken) } func (as *AuthService) AuthAPIKey(ctx context.Context, read bool, apiKey string) (pass bool, err error) { apiKeyInfo, exist, err := as.apiKeyRepo.GetAPIKey(ctx, apiKey) if err != nil { return false, err } if !exist { return false, nil } // If the request is not read-only, check if the API key has write permissions if !read && apiKeyInfo.Scope == "read-only" { log.Warnf("API key %s does not have write permissions", apiKeyInfo.AccessKey) return false, nil } log.Infof("API key %s is valid, scope: %s", apiKeyInfo.AccessKey, apiKeyInfo.Scope) return true, nil } ================================================ FILE: internal/service/badge/badge_award_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package badge import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/object_info" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) type BadgeAwardRepo interface { CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string, singleOrMulti int8) (isAward bool, err error) AwardBadgeForUser(ctx context.Context, badgeAward *entity.BadgeAward) (err error) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) CountByBadgeID(ctx context.Context, badgeID string) (awardCount int64, err error) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) ListNewestEarned(ctx context.Context, userID string, limit int) (badgeAwards []*entity.BadgeAwardRecent, err error) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward, exists bool, err error) GetByUserIdAndBadgeIdAndAwardKey(ctx context.Context, userID string, badgeID string, awardKey string) (badgeAward *entity.BadgeAward, exists bool, err error) DeleteUserBadgeAward(ctx context.Context, userID string) (err error) } type BadgeAwardService struct { badgeAwardRepo BadgeAwardRepo badgeRepo BadgeRepo userCommon *usercommon.UserCommon objectInfoService *object_info.ObjService notificationQueueService noticequeue.Service } func NewBadgeAwardService( badgeAwardRepo BadgeAwardRepo, badgeRepo BadgeRepo, userCommon *usercommon.UserCommon, objectInfoService *object_info.ObjService, notificationQueueService noticequeue.Service, ) *BadgeAwardService { return &BadgeAwardService{ badgeAwardRepo: badgeAwardRepo, badgeRepo: badgeRepo, userCommon: userCommon, objectInfoService: objectInfoService, notificationQueueService: notificationQueueService, } } // GetBadgeAwardList get badge award list func (bs *BadgeAwardService) GetBadgeAwardList( ctx context.Context, req *schema.GetBadgeAwardWithPageReq, ) (resp []*schema.GetBadgeAwardWithPageResp, total int64, err error) { var ( badgeAwardList []*entity.BadgeAward ) req.UserID, err = bs.validateUserByUsername(ctx, req.Username) if err != nil { badgeAwardList, total, err = bs.badgeAwardRepo.ListPagedByBadgeId(ctx, req.BadgeID, req.Page, req.PageSize) } else { badgeAwardList, total, err = bs.badgeAwardRepo.ListPagedByBadgeIdAndUserId(ctx, req.BadgeID, req.UserID, req.Page, req.PageSize) } if err != nil { return } resp = make([]*schema.GetBadgeAwardWithPageResp, len(badgeAwardList)) for i, badgeAward := range badgeAwardList { var ( objectID, questionID, answerID, commentID, objectType, urlTitle string ) // if exist object info objInfo, e := bs.objectInfoService.GetInfo(ctx, badgeAward.AwardKey) if e == nil && !objInfo.IsDeleted() { objectID = objInfo.ObjectID questionID = objInfo.QuestionID answerID = objInfo.AnswerID commentID = objInfo.CommentID objectType = objInfo.ObjectType urlTitle = objInfo.Title } row := &schema.GetBadgeAwardWithPageResp{ CreatedAt: badgeAward.CreatedAt.Unix(), ObjectID: objectID, QuestionID: questionID, AnswerID: answerID, CommentID: commentID, ObjectType: objectType, UrlTitle: urlTitle, AuthorUserInfo: schema.UserBasicInfo{}, } // get user info userInfo, exists, e := bs.userCommon.GetUserBasicInfoByID(ctx, badgeAward.UserID) if e != nil { log.Errorf("user not found by id: %s, err: %v", badgeAward.UserID, e) } if exists { _ = copier.Copy(&row.AuthorUserInfo, userInfo) } resp[i] = row } return } // Award award badge func (bs *BadgeAwardService) Award(ctx context.Context, badgeID string, userID string, awardKey string) (err error) { badgeData, exists, err := bs.badgeRepo.GetByID(ctx, badgeID) if err != nil { return err } if !exists || badgeData.Status == entity.BadgeStatusInactive { return errors.BadRequest(reason.BadgeObjectNotFound) } alreadyAwarded, err := bs.badgeAwardRepo.CheckIsAward(ctx, badgeID, userID, awardKey, badgeData.Single) if err != nil { return err } if alreadyAwarded { return nil } badgeAward := &entity.BadgeAward{ UserID: userID, BadgeID: badgeID, AwardKey: awardKey, BadgeGroupID: badgeData.BadgeGroupID, IsBadgeDeleted: entity.IsBadgeNotDeleted, } err = bs.badgeAwardRepo.AwardBadgeForUser(ctx, badgeAward) if err != nil { return err } msg := &schema.NotificationMsg{ TriggerUserID: badgeAward.UserID, ReceiverUserID: badgeAward.UserID, Type: schema.NotificationTypeAchievement, ObjectID: badgeAward.ID, ObjectType: constant.BadgeAwardObjectType, Title: badgeData.Name, ExtraInfo: map[string]string{"badge_id": badgeData.ID}, NotificationAction: constant.NotificationEarnedBadge, } bs.notificationQueueService.Send(ctx, msg) return nil } // GetUserBadgeAwardList get user badge award list func (bs *BadgeAwardService) GetUserBadgeAwardList( ctx *gin.Context, req *schema.GetUserBadgeAwardListReq, ) ( resp []*schema.GetUserBadgeAwardListResp, total int64, err error, ) { var ( earnedCounts []*entity.BadgeEarnedCount ) req.UserID, err = bs.validateUserByUsername(ctx, req.Username) if err != nil { return } earnedCounts, err = bs.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, req.UserID) if err != nil { return } total = int64(len(earnedCounts)) resp = make([]*schema.GetUserBadgeAwardListResp, total) for i, earnedCount := range earnedCounts { badge, exists, e := bs.badgeRepo.GetByID(ctx, earnedCount.BadgeID) if e != nil { err = e return } if !exists { continue } resp[i] = &schema.GetUserBadgeAwardListResp{ ID: uid.EnShortID(badge.ID), Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), Icon: badge.Icon, EarnedCount: earnedCount.EarnedCount, Level: badge.Level, } } return } // GetUserRecentBadgeAwardList get user badge award list func (bs *BadgeAwardService) GetUserRecentBadgeAwardList(ctx *gin.Context, req *schema.GetUserBadgeAwardListReq) ( resp []*schema.GetUserBadgeAwardListResp, total int64, err error) { var ( earnedCounts []*entity.BadgeAwardRecent ) req.UserID, err = bs.validateUserByUsername(ctx, req.Username) if err != nil { return } earnedCounts, err = bs.badgeAwardRepo.ListNewestEarned(ctx, req.UserID, req.Limit) if err != nil { return } total = int64(len(earnedCounts)) resp = make([]*schema.GetUserBadgeAwardListResp, total) for i, earnedCount := range earnedCounts { badge, exists, e := bs.badgeRepo.GetByID(ctx, earnedCount.BadgeID) if e != nil { err = e return } if !exists { continue } resp[i] = &schema.GetUserBadgeAwardListResp{ ID: uid.EnShortID(badge.ID), Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), Icon: badge.Icon, EarnedCount: earnedCount.EarnedCount, Level: badge.Level, } } return } func (bs *BadgeAwardService) validateUserByUsername(ctx context.Context, userName string) (userID string, err error) { var ( userInfo *schema.UserBasicInfo exist bool ) // validate user exists or not if len(userName) > 0 { userInfo, exist, err = bs.userCommon.GetUserBasicInfoByUserName(ctx, userName) if err != nil { return } if !exist { err = errors.BadRequest(reason.UserNotFound) return } userID = userInfo.ID } if len(userID) == 0 { err = errors.BadRequest(reason.UserNotFound) return } return } ================================================ FILE: internal/service/badge/badge_event_handler.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package badge import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/eventqueue" "github.com/segmentfault/pacman/log" ) type BadgeEventService struct { data *data.Data eventQueueService eventqueue.Service badgeRepo BadgeRepo eventRuleRepo EventRuleRepo badgeAwardService *BadgeAwardService } type EventRuleHandler func(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) type EventRuleRepo interface { HandleEventWithRule(ctx context.Context, msg *schema.EventMsg) (awards []*entity.BadgeAward) } func NewBadgeEventService( data *data.Data, eventQueueService eventqueue.Service, badgeRepo BadgeRepo, eventRuleRepo EventRuleRepo, badgeAwardService *BadgeAwardService, ) *BadgeEventService { n := &BadgeEventService{ data: data, eventQueueService: eventQueueService, badgeRepo: badgeRepo, eventRuleRepo: eventRuleRepo, badgeAwardService: badgeAwardService, } eventQueueService.RegisterHandler(n.Handler) return n } func (ns *BadgeEventService) Handler(ctx context.Context, msg *schema.EventMsg) error { awards := ns.eventRuleRepo.HandleEventWithRule(ctx, msg) if len(awards) == 0 { return nil } for _, award := range awards { err := ns.badgeAwardService.Award(ctx, award.BadgeID, award.UserID, award.AwardKey) if err != nil { log.Debugf("error awarding badge %s: %v", award.BadgeID, err) } } return nil } ================================================ FILE: internal/service/badge/badge_group_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package badge import ( "context" "github.com/apache/answer/internal/entity" ) type BadgeGroupRepo interface { ListGroups(ctx context.Context) (groups []*entity.BadgeGroup, err error) AddGroup(ctx context.Context, group *entity.BadgeGroup) (err error) } type BadgeGroupService struct { badgeGroupRepo BadgeGroupRepo } func NewBadgeGroupService(badgeGroupRepo BadgeGroupRepo) *BadgeGroupService { return &BadgeGroupService{ badgeGroupRepo: badgeGroupRepo, } } ================================================ FILE: internal/service/badge/badge_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package badge import ( "context" "strings" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) type BadgeRepo interface { GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) GetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error) ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) ListActivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) ListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) UpdateStatus(ctx context.Context, id string, status int8) (err error) UpdateAwardCount(ctx context.Context, badgeID string, awardCount int) (err error) } type BadgeService struct { badgeRepo BadgeRepo badgeGroupRepo BadgeGroupRepo badgeAwardRepo BadgeAwardRepo badgeEventService *BadgeEventService siteInfoCommonService siteinfo_common.SiteInfoCommonService } func NewBadgeService( badgeRepo BadgeRepo, badgeGroupRepo BadgeGroupRepo, badgeAwardRepo BadgeAwardRepo, badgeEventService *BadgeEventService, siteInfoCommonService siteinfo_common.SiteInfoCommonService, ) *BadgeService { return &BadgeService{ badgeRepo: badgeRepo, badgeGroupRepo: badgeGroupRepo, badgeAwardRepo: badgeAwardRepo, badgeEventService: badgeEventService, siteInfoCommonService: siteInfoCommonService, } } // ListByGroup list all badges group by group func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []*schema.GetBadgeListResp, err error) { var ( groups []*entity.BadgeGroup badges []*entity.Badge earnedCounts []*entity.BadgeEarnedCount groupMap = make(map[int64]string, 0) badgesMap = make(map[int64][]*schema.BadgeListInfo, 0) ) resp = make([]*schema.GetBadgeListResp, 0) groups, err = b.badgeGroupRepo.ListGroups(ctx) if err != nil { return } badges, _, err = b.badgeRepo.ListActivated(ctx, 0, 0) if err != nil { return } if len(userID) > 0 { earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, userID) if err != nil { return } } for _, group := range groups { groupMap[converter.StringToInt64(group.ID)] = translator.Tr(handler.GetLangByCtx(ctx), group.Name) } for _, badge := range badges { // check is earned var earned int64 = 0 if len(earnedCounts) > 0 { for _, earnedCount := range earnedCounts { if badge.ID == earnedCount.BadgeID && earnedCount.EarnedCount > 0 { earned = earnedCount.EarnedCount break } } } badgesMap[badge.BadgeGroupID] = append(badgesMap[badge.BadgeGroupID], &schema.BadgeListInfo{ ID: uid.EnShortID(badge.ID), Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), Icon: badge.Icon, AwardCount: badge.AwardCount, EarnedCount: earned, Level: badge.Level, }) } for _, group := range groups { resp = append(resp, &schema.GetBadgeListResp{ GroupName: translator.Tr(handler.GetLangByCtx(ctx), group.Name), Badges: badgesMap[converter.StringToInt64(group.ID)], }) } return } // ListPaged list all badges by page func (b *BadgeService) ListPaged(ctx context.Context, req *schema.GetBadgeListPagedReq) (resp []*schema.GetBadgeListPagedResp, total int64, err error) { var ( groups []*entity.BadgeGroup badges []*entity.Badge badge *entity.Badge exists bool groupMap = make(map[int64]string, 0) ) total = 0 if len(req.Query) > 0 { isID := strings.Index(req.Query, "badge:") if isID != 0 { badges, err = b.searchByName(ctx, req.Query) if err != nil { return } // paged result count := len(badges) total = int64(count) start := (req.Page - 1) * req.PageSize end := req.Page * req.PageSize if start >= count { start = count end = count } if end > count { end = count } badges = badges[start:end] } else { req.Query = strings.TrimSpace(strings.TrimLeft(req.Query, "badge:")) id := uid.DeShortID(req.Query) if len(id) == 0 { return } badge, exists, err = b.badgeRepo.GetByID(ctx, id) if err != nil || !exists { return } badges = append(badges, badge) } } else { switch req.Status { case schema.BadgeStatusActive: badges, total, err = b.badgeRepo.ListActivated(ctx, req.Page, req.PageSize) case schema.BadgeStatusInactive: badges, total, err = b.badgeRepo.ListInactivated(ctx, req.Page, req.PageSize) default: badges, total, err = b.badgeRepo.ListPaged(ctx, req.Page, req.PageSize) } if err != nil { return } } // find all group and build group map groups, err = b.badgeGroupRepo.ListGroups(ctx) if err != nil { return } for _, group := range groups { groupMap[converter.StringToInt64(group.ID)] = translator.Tr(handler.GetLangByCtx(ctx), group.Name) } resp = make([]*schema.GetBadgeListPagedResp, len(badges)) general, siteErr := b.siteInfoCommonService.GetSiteGeneral(ctx) baseURL := "" if siteErr == nil { baseURL = general.SiteUrl } for i, badge := range badges { resp[i] = &schema.GetBadgeListPagedResp{ ID: uid.EnShortID(badge.ID), Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), Description: translator.TrWithData(handler.GetLangByCtx(ctx), badge.Description, &schema.BadgeTplData{ProfileURL: baseURL + "/users/settings/profile"}), Icon: badge.Icon, AwardCount: badge.AwardCount, Level: badge.Level, GroupName: groupMap[badge.BadgeGroupID], Status: schema.BadgeStatusMap[badge.Status], } } return } // searchByName func (b *BadgeService) searchByName(ctx context.Context, name string) (result []*entity.Badge, err error) { var badges []*entity.Badge name = strings.ToLower(name) result = make([]*entity.Badge, 0) badges, _, err = b.badgeRepo.ListPaged(ctx, 0, 0) for _, badge := range badges { tn := strings.ToLower(translator.Tr(handler.GetLangByCtx(ctx), badge.Name)) if strings.Contains(tn, name) { result = append(result, badge) } } return } // GetBadgeInfo get badge info func (b *BadgeService) GetBadgeInfo(ctx *gin.Context, id string, userID string) (info *schema.GetBadgeInfoResp, err error) { badge, exists, err := b.badgeRepo.GetByID(ctx, id) if err != nil { return nil, err } if !exists || badge.Status == entity.BadgeStatusInactive { return nil, errors.BadRequest(reason.BadgeObjectNotFound) } var earnedTotal int64 if len(userID) > 0 { earnedTotal = b.badgeAwardRepo.CountByUserIdAndBadgeId(ctx, userID, badge.ID) } baseURL := "" general, siteErr := b.siteInfoCommonService.GetSiteGeneral(ctx) if siteErr == nil { baseURL = general.SiteUrl } info = &schema.GetBadgeInfoResp{ ID: uid.EnShortID(badge.ID), Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), Description: translator.TrWithData(handler.GetLangByCtx(ctx), badge.Description, &schema.BadgeTplData{ProfileURL: baseURL + "/users/settings/profile"}), Icon: badge.Icon, AwardCount: badge.AwardCount, EarnedCount: earnedTotal, IsSingle: badge.Single == entity.BadgeSingleAward, Level: badge.Level, } return } // UpdateStatus update badge status func (b *BadgeService) UpdateStatus(ctx *gin.Context, req *schema.UpdateBadgeStatusReq) (err error) { req.ID = uid.DeShortID(req.ID) badge, exists, err := b.badgeRepo.GetByID(ctx, req.ID) if err != nil { return err } if !exists { return errors.BadRequest(reason.BadgeObjectNotFound) } // check duplicate action status, ok := schema.BadgeStatusEMap[req.Status] if !ok { err = errors.BadRequest(reason.StatusInvalid) return } if badge.Status == status { return } err = b.badgeRepo.UpdateStatus(ctx, req.ID, status) if err != nil { return err } if status == entity.BadgeStatusActive { count, err := b.badgeAwardRepo.CountByBadgeID(ctx, badge.ID) if err != nil { log.Errorf("count badge award failed: %v", err) return nil } err = b.badgeRepo.UpdateAwardCount(ctx, badge.ID, int(count)) if err != nil { log.Errorf("update badge award count failed: %v", err) return nil } } return nil } ================================================ FILE: internal/service/collection/collection_group_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package collection import ( "context" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" ) // CollectionGroupRepo collectionGroup repository type CollectionGroupRepo interface { AddCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup) (err error) AddCollectionDefaultGroup(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error) CreateDefaultGroupIfNotExist(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error) UpdateCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup, cols []string) (err error) GetCollectionGroup(ctx context.Context, id string) (collectionGroup *entity.CollectionGroup, exist bool, err error) GetCollectionGroupPage(ctx context.Context, page, pageSize int, collectionGroup *entity.CollectionGroup) (collectionGroupList []*entity.CollectionGroup, total int64, err error) GetDefaultID(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, has bool, err error) } // CollectionGroupService user service type CollectionGroupService struct { collectionGroupRepo CollectionGroupRepo } func NewCollectionGroupService(collectionGroupRepo CollectionGroupRepo) *CollectionGroupService { return &CollectionGroupService{ collectionGroupRepo: collectionGroupRepo, } } // AddCollectionGroup add collection group func (cs *CollectionGroupService) AddCollectionGroup(ctx context.Context, req *schema.AddCollectionGroupReq) (err error) { collectionGroup := &entity.CollectionGroup{} _ = copier.Copy(collectionGroup, req) return cs.collectionGroupRepo.AddCollectionGroup(ctx, collectionGroup) } // UpdateCollectionGroup update collection group func (cs *CollectionGroupService) UpdateCollectionGroup(ctx context.Context, req *schema.UpdateCollectionGroupReq, cols []string) (err error) { collectionGroup := &entity.CollectionGroup{} _ = copier.Copy(collectionGroup, req) return cs.collectionGroupRepo.UpdateCollectionGroup(ctx, collectionGroup, cols) } // GetCollectionGroup get collection group one func (cs *CollectionGroupService) GetCollectionGroup(ctx context.Context, id string) (resp *schema.GetCollectionGroupResp, err error) { collectionGroup, exist, err := cs.collectionGroupRepo.GetCollectionGroup(ctx, id) if err != nil { return } if !exist { return nil, errors.BadRequest(reason.UnknownError) } resp = &schema.GetCollectionGroupResp{} _ = copier.Copy(resp, collectionGroup) return resp, nil } ================================================ FILE: internal/service/collection/collection_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package collection import ( "context" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" collectioncommon "github.com/apache/answer/internal/service/collection_common" questioncommon "github.com/apache/answer/internal/service/question_common" ) // CollectionService user service type CollectionService struct { collectionRepo collectioncommon.CollectionRepo collectionGroupRepo CollectionGroupRepo questionCommon *questioncommon.QuestionCommon } func NewCollectionService( collectionRepo collectioncommon.CollectionRepo, collectionGroupRepo CollectionGroupRepo, questionCommon *questioncommon.QuestionCommon, ) *CollectionService { return &CollectionService{ collectionRepo: collectionRepo, collectionGroupRepo: collectionGroupRepo, questionCommon: questionCommon, } } func (cs *CollectionService) CollectionSwitch(ctx context.Context, req *schema.CollectionSwitchReq) ( resp *schema.CollectionSwitchResp, err error) { collectionGroup, err := cs.collectionGroupRepo.CreateDefaultGroupIfNotExist(ctx, req.UserID) if err != nil { return nil, err } collection, exist, err := cs.collectionRepo.GetOneByObjectIDAndUser(ctx, req.UserID, req.ObjectID) if err != nil { return nil, err } if (!req.Bookmark && !exist) || (req.Bookmark && exist) { return nil, nil } if req.Bookmark { collection = &entity.Collection{ UserID: req.UserID, ObjectID: req.ObjectID, UserCollectionGroupID: collectionGroup.ID, } err = cs.collectionRepo.AddCollection(ctx, collection) } else { err = cs.collectionRepo.RemoveCollection(ctx, collection.ID) } if err != nil { return nil, err } // For now, we only support bookmark for question, so we just update question collection count resp = &schema.CollectionSwitchResp{} resp.ObjectCollectionCount, err = cs.questionCommon.UpdateCollectionCount(ctx, req.ObjectID) if err != nil { return nil, err } return resp, nil } ================================================ FILE: internal/service/collection_common/collection.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package collectioncommon import ( "context" "github.com/apache/answer/internal/entity" ) // CollectionRepo collection repository type CollectionRepo interface { AddCollection(ctx context.Context, collection *entity.Collection) (err error) RemoveCollection(ctx context.Context, id string) (err error) UpdateCollection(ctx context.Context, collection *entity.Collection, cols []string) (err error) GetCollection(ctx context.Context, id int) (collection *entity.Collection, exist bool, err error) GetCollectionList(ctx context.Context, collection *entity.Collection) (collectionList []*entity.Collection, err error) GetOneByObjectIDAndUser(ctx context.Context, userId string, objectId string) (collection *entity.Collection, exist bool, err error) SearchByObjectIDsAndUser(ctx context.Context, userId string, objectIds []string) (collectionList []*entity.Collection, err error) CountByObjectID(ctx context.Context, objectId string) (total int64, err error) GetCollectionPage(ctx context.Context, page, pageSize int, collection *entity.Collection) (collectionList []*entity.Collection, total int64, err error) SearchObjectCollected(ctx context.Context, userId string, objectIds []string) (collectedMap map[string]bool, err error) SearchList(ctx context.Context, search *entity.CollectionSearch) ([]*entity.Collection, int64, error) } // CollectionCommon user service type CollectionCommon struct { collectionRepo CollectionRepo } func NewCollectionCommon(collectionRepo CollectionRepo) *CollectionCommon { return &CollectionCommon{ collectionRepo: collectionRepo, } } // SearchObjectCollected search object is collected func (ccs *CollectionCommon) SearchObjectCollected(ctx context.Context, userId string, objectIds []string) (collectedMap map[string]bool, err error) { return ccs.collectionRepo.SearchObjectCollected(ctx, userId, objectIds) } func (ccs *CollectionCommon) SearchList(ctx context.Context, search *entity.CollectionSearch) ([]*entity.Collection, int64, error) { return ccs.collectionRepo.SearchList(ctx, search) } ================================================ FILE: internal/service/comment/comment_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package comment import ( "context" "github.com/apache/answer/internal/service/eventqueue" "github.com/apache/answer/internal/service/review" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/activityqueue" "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/object_info" "github.com/apache/answer/internal/service/permission" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/token" "github.com/apache/answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // CommentRepo comment repository type CommentRepo interface { AddComment(ctx context.Context, comment *entity.Comment) (err error) RemoveComment(ctx context.Context, commentID string) (err error) UpdateCommentContent(ctx context.Context, commentID string, original string, parsedText string) (err error) UpdateCommentStatus(ctx context.Context, commentID string, status int) (err error) GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) GetCommentPage(ctx context.Context, commentQuery *CommentQuery) ( comments []*entity.Comment, total int64, err error) } type CommentQuery struct { pager.PageCond // object id ObjectID string // query condition QueryCond string // user id UserID string } func (c *CommentQuery) GetOrderBy() string { if c.QueryCond == "vote" { return "vote_count DESC,created_at ASC" } if c.QueryCond == "created_at" { return "created_at DESC" } return "created_at ASC" } // CommentService user service type CommentService struct { commentRepo CommentRepo commentCommonRepo comment_common.CommentCommonRepo userCommon *usercommon.UserCommon voteCommon activity_common.VoteRepo objectInfoService *object_info.ObjService emailService *export.EmailService userRepo usercommon.UserRepo notificationQueueService noticequeue.Service externalNotificationQueueService noticequeue.ExternalService activityQueueService activityqueue.Service eventQueueService eventqueue.Service reviewService *review.ReviewService } // NewCommentService new comment service func NewCommentService( commentRepo CommentRepo, commentCommonRepo comment_common.CommentCommonRepo, userCommon *usercommon.UserCommon, objectInfoService *object_info.ObjService, voteCommon activity_common.VoteRepo, emailService *export.EmailService, userRepo usercommon.UserRepo, notificationQueueService noticequeue.Service, externalNotificationQueueService noticequeue.ExternalService, activityQueueService activityqueue.Service, eventQueueService eventqueue.Service, reviewService *review.ReviewService, ) *CommentService { return &CommentService{ commentRepo: commentRepo, commentCommonRepo: commentCommonRepo, userCommon: userCommon, voteCommon: voteCommon, objectInfoService: objectInfoService, emailService: emailService, userRepo: userRepo, notificationQueueService: notificationQueueService, externalNotificationQueueService: externalNotificationQueueService, activityQueueService: activityQueueService, eventQueueService: eventQueueService, reviewService: reviewService, } } // AddComment add comment func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddCommentReq) ( resp *schema.GetCommentResp, err error) { comment := &entity.Comment{} _ = copier.Copy(comment, req) comment.Status = entity.CommentStatusAvailable objInfo, err := cs.objectInfoService.GetInfo(ctx, req.ObjectID) if err != nil { return nil, err } if objInfo.IsDeleted() { return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted) } objInfo.ObjectID = uid.DeShortID(objInfo.ObjectID) objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID) objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID) if objInfo.ObjectType == constant.QuestionObjectType || objInfo.ObjectType == constant.AnswerObjectType { comment.QuestionID = objInfo.QuestionID } if len(req.ReplyCommentID) > 0 { replyComment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.ReplyCommentID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.CommentNotFound) } comment.SetReplyUserID(replyComment.UserID) comment.SetReplyCommentID(replyComment.ID) } else { comment.SetReplyUserID("") comment.SetReplyCommentID("") } err = cs.commentRepo.AddComment(ctx, comment) if err != nil { return nil, err } comment.Status = cs.reviewService.AddCommentReview(ctx, comment, req.IP, req.UserAgent) if err := cs.commentRepo.UpdateCommentStatus(ctx, comment.ID, comment.Status); err != nil { return nil, err } resp = &schema.GetCommentResp{} resp.SetFromComment(comment) resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, time.Now(), req.CanEdit, req.CanDelete) if comment.Status == entity.CommentStatusAvailable { if err := cs.addCommentNotification(ctx, req, resp, comment, objInfo); err != nil { return nil, err } } // get user info userInfo, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID) if err != nil { return nil, err } if exist { resp.Username = userInfo.Username resp.UserDisplayName = userInfo.DisplayName resp.UserAvatar = userInfo.Avatar resp.UserStatus = userInfo.Status } activityMsg := &schema.ActivityMsg{ UserID: comment.UserID, ObjectID: comment.ID, OriginalObjectID: req.ObjectID, ActivityTypeKey: constant.ActQuestionCommented, } var event *schema.EventMsg switch objInfo.ObjectType { case constant.QuestionObjectType: activityMsg.ActivityTypeKey = constant.ActQuestionCommented event = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID). CID(comment.ID, comment.UserID).QID(objInfo.QuestionID, objInfo.ObjectCreatorUserID) case constant.AnswerObjectType: activityMsg.ActivityTypeKey = constant.ActAnswerCommented event = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID). CID(comment.ID, comment.UserID).AID(objInfo.AnswerID, objInfo.ObjectCreatorUserID) } cs.activityQueueService.Send(ctx, activityMsg) cs.eventQueueService.Send(ctx, event) return resp, nil } func (cs *CommentService) addCommentNotification( ctx context.Context, req *schema.AddCommentReq, resp *schema.GetCommentResp, comment *entity.Comment, objInfo *schema.SimpleObjectInfo) error { // The priority of the notification // 1. reply to user // 2. comment mention to user // 3. answer or question was commented alreadyNotifiedUserID := make(map[string]bool) // get reply user info if len(resp.ReplyUserID) > 0 && resp.ReplyUserID != req.UserID { replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID) if err != nil { return err } if exist { resp.ReplyUsername = replyUser.Username resp.ReplyUserDisplayName = replyUser.DisplayName resp.ReplyUserStatus = replyUser.Status } cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, req.UserID, objInfo.QuestionID, objInfo.Title, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) alreadyNotifiedUserID[replyUser.ID] = true return nil } if len(req.MentionUsernameList) > 0 { alreadyNotifiedUserIDs := cs.notificationMention( ctx, req.MentionUsernameList, comment.ID, req.UserID, alreadyNotifiedUserID) for _, userID := range alreadyNotifiedUserIDs { alreadyNotifiedUserID[userID] = true } return nil } if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] { cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID, objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) } else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] { cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID, objInfo.ObjectCreatorUserID, comment.ID, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) } return nil } // RemoveComment delete comment func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) { err = cs.commentRepo.RemoveComment(ctx, req.CommentID) if err != nil { return err } cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentDelete, req.UserID). TID(req.CommentID).CID(req.CommentID, req.UserID)) return nil } // UpdateComment update comment func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) ( resp *schema.UpdateCommentResp, err error) { old, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.CommentNotFound) } // user can't edit the comment that was posted by others except admin if !req.IsAdmin && req.UserID != old.UserID { return nil, errors.BadRequest(reason.CommentNotFound) } // user can edit the comment that was posted by himself before deadline. // admin can edit it at any time if !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) { return nil, errors.BadRequest(reason.CommentCannotEditAfterDeadline) } if err = cs.commentRepo.UpdateCommentContent(ctx, old.ID, req.OriginalText, req.ParsedText); err != nil { return nil, err } resp = &schema.UpdateCommentResp{ CommentID: old.ID, OriginalText: req.OriginalText, ParsedText: req.ParsedText, } cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentUpdate, req.UserID).TID(old.ID). CID(old.ID, old.UserID)) return resp, nil } // GetComment get comment one func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetCommentReq) (resp *schema.GetCommentResp, err error) { comment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.ID) if err != nil { return } if !exist { return nil, errors.BadRequest(reason.CommentNotFound) } resp = &schema.GetCommentResp{ CommentID: comment.ID, CreatedAt: comment.CreatedAt.Unix(), UserID: comment.UserID, ReplyUserID: comment.GetReplyUserID(), ReplyCommentID: comment.GetReplyCommentID(), ObjectID: comment.ObjectID, VoteCount: comment.VoteCount, OriginalText: comment.OriginalText, ParsedText: comment.ParsedText, } // get comment user info if len(resp.UserID) > 0 { commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID) if err != nil { return nil, err } if exist { resp.Username = commentUser.Username resp.UserDisplayName = commentUser.DisplayName resp.UserAvatar = commentUser.Avatar resp.UserStatus = commentUser.Status } } // get reply user info if len(resp.ReplyUserID) > 0 { replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID) if err != nil { return nil, err } if exist { resp.ReplyUsername = replyUser.Username resp.ReplyUserDisplayName = replyUser.DisplayName resp.ReplyUserStatus = replyUser.Status } } // check if current user vote this comment resp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID) resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, comment.CreatedAt, req.CanEdit, req.CanDelete) return resp, nil } // GetCommentWithPage get comment list page func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.GetCommentWithPageReq) ( pageModel *pager.PageModel, err error) { dto := &CommentQuery{ PageCond: pager.PageCond{Page: req.Page, PageSize: req.PageSize}, ObjectID: req.ObjectID, QueryCond: req.QueryCond, } commentList, total, err := cs.commentRepo.GetCommentPage(ctx, dto) if err != nil { return nil, err } resp := make([]*schema.GetCommentResp, 0) for _, comment := range commentList { commentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment) if err != nil { return nil, err } resp = append(resp, commentResp) } // if user request the specific comment, add it if not exist. if len(req.CommentID) > 0 { commentExist := false for _, t := range resp { if t.CommentID == req.CommentID { commentExist = true break } } if !commentExist { comment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID) if err != nil { return nil, err } if exist && comment.ObjectID == req.ObjectID { commentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment) if err != nil { return nil, err } resp = append(resp, commentResp) } } } return pager.NewPageModel(total, resp), nil } func (cs *CommentService) convertCommentEntity2Resp(ctx context.Context, req *schema.GetCommentWithPageReq, comment *entity.Comment) (commentResp *schema.GetCommentResp, err error) { commentResp = &schema.GetCommentResp{ CommentID: comment.ID, CreatedAt: comment.CreatedAt.Unix(), UserID: comment.UserID, ReplyUserID: comment.GetReplyUserID(), ReplyCommentID: comment.GetReplyCommentID(), ObjectID: comment.ObjectID, VoteCount: comment.VoteCount, OriginalText: comment.OriginalText, ParsedText: comment.ParsedText, } // get comment user info if len(commentResp.UserID) > 0 { commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.UserID) if err != nil { return nil, err } if exist { commentResp.Username = commentUser.Username commentResp.UserDisplayName = commentUser.DisplayName commentResp.UserAvatar = commentUser.Avatar commentResp.UserStatus = commentUser.Status } } // get reply user info if len(commentResp.ReplyUserID) > 0 { replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.ReplyUserID) if err != nil { return nil, err } if exist { commentResp.ReplyUsername = replyUser.Username commentResp.ReplyUserDisplayName = replyUser.DisplayName commentResp.ReplyUserStatus = replyUser.Status } } // check if current user vote this comment commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID) commentResp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, commentResp.UserID, comment.CreatedAt, req.CanEdit, req.CanDelete) return commentResp, nil } func (cs *CommentService) checkIsVote(ctx context.Context, userID, commentID string) (isVote bool) { status := cs.voteCommon.GetVoteStatus(ctx, commentID, userID) return len(status) > 0 } // GetCommentPersonalWithPage get personal comment list page func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *schema.GetCommentPersonalWithPageReq) ( pageModel *pager.PageModel, err error) { if len(req.Username) > 0 { userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } req.UserID = userInfo.ID } if len(req.UserID) == 0 { return nil, errors.BadRequest(reason.UserNotFound) } dto := &CommentQuery{ PageCond: pager.PageCond{Page: req.Page, PageSize: req.PageSize}, UserID: req.UserID, QueryCond: "created_at", } commentList, total, err := cs.commentRepo.GetCommentPage(ctx, dto) if err != nil { return nil, err } resp := make([]*schema.GetCommentPersonalWithPageResp, 0) for _, comment := range commentList { commentResp := &schema.GetCommentPersonalWithPageResp{ CommentID: comment.ID, CreatedAt: comment.CreatedAt.Unix(), ObjectID: comment.ObjectID, Content: comment.ParsedText, // todo trim } if len(comment.ObjectID) > 0 { objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID) if err != nil { log.Error(err) } else { commentResp.ObjectType = objInfo.ObjectType commentResp.Title = objInfo.Title commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title) commentResp.QuestionID = objInfo.QuestionID commentResp.AnswerID = objInfo.AnswerID if objInfo.QuestionStatus == entity.QuestionStatusDeleted { commentResp.Title = "Deleted question" } } } resp = append(resp, commentResp) } return pager.NewPageModel(total, resp), nil } func (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID, questionID, questionTitle, commentID, commentUserID, commentSummary string) { if questionUserID == commentUserID { return } // send internal notification msg := &schema.NotificationMsg{ ReceiverUserID: questionUserID, TriggerUserID: commentUserID, Type: schema.NotificationTypeInbox, ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationCommentQuestion cs.notificationQueueService.Send(ctx, msg) // send external notification receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID) if err != nil { log.Error(err) return } if !exist { log.Warnf("user %s not found", questionUserID) return } externalNotificationMsg := &schema.ExternalNotificationMsg{ ReceiverUserID: receiverUserInfo.ID, ReceiverEmail: receiverUserInfo.EMail, ReceiverLang: receiverUserInfo.Language, } rawData := &schema.NewCommentTemplateRawData{ QuestionTitle: questionTitle, QuestionID: questionID, CommentID: commentID, CommentSummary: commentSummary, UnsubscribeCode: token.GenerateToken(), } commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID) if commentUser != nil { rawData.CommentUserDisplayName = commentUser.DisplayName } externalNotificationMsg.NewCommentTemplateRawData = rawData cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } func (cs *CommentService) notificationAnswerComment(ctx context.Context, questionID, questionTitle, answerID, answerUserID, commentID, commentUserID, commentSummary string) { if answerUserID == commentUserID { return } // Send internal notification. msg := &schema.NotificationMsg{ ReceiverUserID: answerUserID, TriggerUserID: commentUserID, Type: schema.NotificationTypeInbox, ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationCommentAnswer cs.notificationQueueService.Send(ctx, msg) // Send external notification. receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID) if err != nil { log.Error(err) return } if !exist { log.Warnf("user %s not found", answerUserID) return } externalNotificationMsg := &schema.ExternalNotificationMsg{ ReceiverUserID: receiverUserInfo.ID, ReceiverEmail: receiverUserInfo.EMail, ReceiverLang: receiverUserInfo.Language, } rawData := &schema.NewCommentTemplateRawData{ QuestionTitle: questionTitle, QuestionID: questionID, AnswerID: answerID, CommentID: commentID, CommentSummary: commentSummary, UnsubscribeCode: token.GenerateToken(), } commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID) if commentUser != nil { rawData.CommentUserDisplayName = commentUser.DisplayName } externalNotificationMsg.NewCommentTemplateRawData = rawData cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID, questionID, questionTitle, commentSummary string) { msg := &schema.NotificationMsg{ ReceiverUserID: replyUserID, TriggerUserID: commentUserID, Type: schema.NotificationTypeInbox, ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationReplyToYou cs.notificationQueueService.Send(ctx, msg) // Send external notification. receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, replyUserID) if err != nil { log.Error(err) return } if !exist { log.Warnf("user %s not found", replyUserID) return } externalNotificationMsg := &schema.ExternalNotificationMsg{ ReceiverUserID: receiverUserInfo.ID, ReceiverEmail: receiverUserInfo.EMail, ReceiverLang: receiverUserInfo.Language, } rawData := &schema.NewCommentTemplateRawData{ QuestionTitle: questionTitle, QuestionID: questionID, CommentID: commentID, CommentSummary: commentSummary, UnsubscribeCode: token.GenerateToken(), } commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID) if commentUser != nil { rawData.CommentUserDisplayName = commentUser.DisplayName } externalNotificationMsg.NewCommentTemplateRawData = rawData cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } func (cs *CommentService) notificationMention( ctx context.Context, mentionUsernameList []string, commentID, commentUserID string, alreadyNotifiedUserID map[string]bool) (alreadyNotifiedUserIDs []string) { for _, username := range mentionUsernameList { userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username) if err != nil { log.Error(err) continue } if exist && !alreadyNotifiedUserID[userInfo.ID] { msg := &schema.NotificationMsg{ ReceiverUserID: userInfo.ID, TriggerUserID: commentUserID, Type: schema.NotificationTypeInbox, ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationMentionYou cs.notificationQueueService.Send(ctx, msg) alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID) } } return alreadyNotifiedUserIDs } ================================================ FILE: internal/service/comment_common/comment_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package comment_common import ( "context" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/errors" ) // CommentCommonRepo comment repository type CommentCommonRepo interface { GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) GetCommentWithoutStatus(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) GetCommentCount(ctx context.Context) (count int64, err error) RemoveAllUserComment(ctx context.Context, userID string) (err error) UpdateCommentStatus(ctx context.Context, commentID string, status int) (err error) } // CommentCommonService user service type CommentCommonService struct { commentRepo CommentCommonRepo } // NewCommentCommonService new comment service func NewCommentCommonService( commentRepo CommentCommonRepo) *CommentCommonService { return &CommentCommonService{ commentRepo: commentRepo, } } // GetComment get comment one func (cs *CommentCommonService) GetComment(ctx context.Context, commentID string) (resp *schema.GetCommentResp, err error) { comment, exist, err := cs.commentRepo.GetComment(ctx, commentID) if err != nil { return } if !exist { return nil, errors.BadRequest(reason.CommentNotFound) } resp = &schema.GetCommentResp{} resp.SetFromComment(comment) return resp, nil } ================================================ FILE: internal/service/config/config_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package config import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/entity" ) // ConfigRepo config repository type ConfigRepo interface { GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) GetConfigByKeyFromDB(ctx context.Context, key string) (c *entity.Config, err error) UpdateConfig(ctx context.Context, key, value string) (err error) } // ConfigService user service type ConfigService struct { configRepo ConfigRepo } // NewConfigService new config service func NewConfigService(configRepo ConfigRepo) *ConfigService { return &ConfigService{ configRepo: configRepo, } } // GetIntValue get config int value func (cs *ConfigService) GetIntValue(ctx context.Context, key string) (val int, err error) { cf, err := cs.configRepo.GetConfigByKey(ctx, key) if err != nil { return 0, err } return cf.GetIntValue(), nil } // GetStringValue get config string value func (cs *ConfigService) GetStringValue(ctx context.Context, key string) (val string, err error) { cf, err := cs.configRepo.GetConfigByKey(ctx, key) if err != nil { return "", err } return cf.Value, nil } // GetStringValueFromDB gets config string value directly from DB, bypassing cache. func (cs *ConfigService) GetStringValueFromDB(ctx context.Context, key string) (val string, err error) { cf, err := cs.configRepo.GetConfigByKeyFromDB(ctx, key) if err != nil { return "", err } return cf.Value, nil } // GetArrayStringValue get config array string value func (cs *ConfigService) GetArrayStringValue(ctx context.Context, key string) (val []string, err error) { cf, err := cs.configRepo.GetConfigByKey(ctx, key) if err != nil { return nil, err } return cf.GetArrayStringValue(), nil } func (cs *ConfigService) GetJsonConfigByIDAndSetToObject(ctx context.Context, id int, obj any) (err error) { cf, err := cs.configRepo.GetConfigByID(ctx, id) if err != nil { return err } err = json.Unmarshal([]byte(cf.Value), obj) if err != nil { return fmt.Errorf("[%s] config value is not json format", cf.Key) } return nil } // GetConfigByID get config by id func (cs *ConfigService) GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) { return cs.configRepo.GetConfigByID(ctx, id) } func (cs *ConfigService) GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) { return cs.configRepo.GetConfigByKey(ctx, key) } // GetIDByKey get config id by key func (cs *ConfigService) GetIDByKey(ctx context.Context, key string) (id int, err error) { cf, err := cs.configRepo.GetConfigByKey(ctx, key) if err != nil { return 0, err } return cf.ID, nil } func (cs *ConfigService) UpdateConfig(ctx context.Context, key, value string) (err error) { return cs.configRepo.UpdateConfig(ctx, key, value) } ================================================ FILE: internal/service/content/answer_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package content import ( "context" "encoding/json" "time" "github.com/apache/answer/internal/service/eventqueue" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/activityqueue" answercommon "github.com/apache/answer/internal/service/answer_common" collectioncommon "github.com/apache/answer/internal/service/collection_common" "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/permission" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/review" "github.com/apache/answer/internal/service/revision_common" "github.com/apache/answer/internal/service/role" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/token" "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // AnswerService user service type AnswerService struct { answerRepo answercommon.AnswerRepo questionRepo questioncommon.QuestionRepo questionCommon *questioncommon.QuestionCommon answerActivityService *activity.AnswerActivityService userCommon *usercommon.UserCommon collectionCommon *collectioncommon.CollectionCommon userRepo usercommon.UserRepo revisionService *revision_common.RevisionService AnswerCommon *answercommon.AnswerCommon voteRepo activity_common.VoteRepo emailService *export.EmailService roleService *role.UserRoleRelService notificationQueueService noticequeue.Service externalNotificationQueueService noticequeue.ExternalService activityQueueService activityqueue.Service reviewService *review.ReviewService eventQueueService eventqueue.Service } func NewAnswerService( answerRepo answercommon.AnswerRepo, questionRepo questioncommon.QuestionRepo, questionCommon *questioncommon.QuestionCommon, userCommon *usercommon.UserCommon, collectionCommon *collectioncommon.CollectionCommon, userRepo usercommon.UserRepo, revisionService *revision_common.RevisionService, answerAcceptActivityRepo *activity.AnswerActivityService, answerCommon *answercommon.AnswerCommon, voteRepo activity_common.VoteRepo, emailService *export.EmailService, roleService *role.UserRoleRelService, notificationQueueService noticequeue.Service, externalNotificationQueueService noticequeue.ExternalService, activityQueueService activityqueue.Service, reviewService *review.ReviewService, eventQueueService eventqueue.Service, ) *AnswerService { return &AnswerService{ answerRepo: answerRepo, questionRepo: questionRepo, userCommon: userCommon, collectionCommon: collectionCommon, questionCommon: questionCommon, userRepo: userRepo, revisionService: revisionService, answerActivityService: answerAcceptActivityRepo, AnswerCommon: answerCommon, voteRepo: voteRepo, emailService: emailService, roleService: roleService, notificationQueueService: notificationQueueService, externalNotificationQueueService: externalNotificationQueueService, activityQueueService: activityQueueService, reviewService: reviewService, eventQueueService: eventQueueService, } } // RemoveAnswer delete answer func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAnswerReq) (err error) { answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID) if err != nil { return err } if !exist { return nil } // if the status is deleted, return directly if answerInfo.Status == entity.AnswerStatusDeleted { return nil } roleID, err := as.roleService.GetUserRole(ctx, req.UserID) if err != nil { return err } if roleID != role.RoleAdminID && roleID != role.RoleModeratorID { if answerInfo.UserID != req.UserID { return errors.BadRequest(reason.AnswerCannotDeleted) } if answerInfo.VoteCount > 0 { return errors.BadRequest(reason.AnswerCannotDeleted) } if answerInfo.Accepted == schema.AnswerAcceptedEnable { return errors.BadRequest(reason.AnswerCannotDeleted) } _, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) if err != nil { return errors.BadRequest(reason.AnswerCannotDeleted) } if !exist { return errors.BadRequest(reason.AnswerCannotDeleted) } } err = as.answerRepo.RemoveAnswer(ctx, req.ID) if err != nil { return err } // user add question count err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID) if err != nil { log.Error("IncreaseAnswerCount error", err.Error()) } userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerInfo.UserID) if err != nil { log.Error("GetCountByUserID error", err.Error()) } err = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount)) if err != nil { log.Error("user IncreaseAnswerCount error", err.Error()) } err = as.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{ FromQuestionID: answerInfo.QuestionID, FromAnswerID: answerInfo.ID, }, &entity.QuestionLink{ ToQuestionID: answerInfo.QuestionID, ToAnswerID: answerInfo.ID, }) if err != nil { log.Error("RemoveQuestionLink error", err.Error()) } // #2372 In order to simplify the process and complexity, as well as to consider if it is in-house, // facing the problem of recovery. // err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount) // if err != nil { // log.Errorf("delete answer activity change failed: %s", err.Error()) // } as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: answerInfo.ID, OriginalObjectID: answerInfo.ID, ActivityTypeKey: constant.ActAnswerDeleted, }) as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerDelete, req.UserID).TID(answerInfo.ID). AID(answerInfo.ID, answerInfo.UserID)) return } // RecoverAnswer recover deleted answer func (as *AnswerService) RecoverAnswer(ctx context.Context, req *schema.RecoverAnswerReq) (err error) { answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.AnswerID) if err != nil { return err } if !exist { return errors.BadRequest(reason.AnswerNotFound) } if answerInfo.Status != entity.AnswerStatusDeleted { return nil } if err = as.answerRepo.RecoverAnswer(ctx, req.AnswerID); err != nil { return err } if err = as.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{ FromQuestionID: answerInfo.QuestionID, FromAnswerID: answerInfo.ID, }, &entity.QuestionLink{ ToQuestionID: answerInfo.QuestionID, ToAnswerID: answerInfo.ID, }); err != nil { return err } if err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID); err != nil { log.Errorf("update answer count failed: %s", err.Error()) } userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerInfo.UserID) if err != nil { log.Errorf("get user answer count failed: %s", err.Error()) } else { err = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount)) if err != nil { log.Errorf("update user answer count failed: %s", err.Error()) } } as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: answerInfo.ID, OriginalObjectID: answerInfo.ID, ActivityTypeKey: constant.ActAnswerUndeleted, }) return nil } func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (string, error) { questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID) if err != nil { return "", err } if !exist { return "", errors.BadRequest(reason.QuestionNotFound) } if questionInfo.Status == entity.QuestionStatusClosed || questionInfo.Status == entity.QuestionStatusDeleted { err = errors.BadRequest(reason.AnswerCannotAddByClosedQuestion) return "", err } insertData := &entity.Answer{} insertData.UserID = req.UserID insertData.OriginalText = req.Content insertData.ParsedText = req.HTML insertData.Accepted = schema.AnswerAcceptedFailed insertData.QuestionID = req.QuestionID insertData.RevisionID = "0" insertData.LastEditUserID = "0" insertData.Status = entity.AnswerStatusPending // insertData.UpdatedAt = now if err = as.answerRepo.AddAnswer(ctx, insertData); err != nil { return "", err } insertData.Status = as.reviewService.AddAnswerReview(ctx, insertData, req.IP, req.UserAgent) if err := as.answerRepo.UpdateAnswerStatus(ctx, insertData.ID, insertData.Status); err != nil { return "", err } if insertData.Status == entity.AnswerStatusAvailable { insertData.ParsedText, err = as.questionCommon.UpdateQuestionLink(ctx, insertData.QuestionID, insertData.ID, insertData.ParsedText, insertData.OriginalText) if err != nil { return "", err } if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"parsed_text"}); err != nil { return "", err } } err = as.questionCommon.UpdateAnswerCount(ctx, req.QuestionID) if err != nil { log.Error("IncreaseAnswerCount error", err.Error()) } err = as.questionCommon.UpdateLastAnswer(ctx, req.QuestionID, uid.DeShortID(insertData.ID)) if err != nil { log.Error("UpdateLastAnswer error", err.Error()) } err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID) if err != nil { return insertData.ID, err } userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, req.UserID) if err != nil { log.Error("GetCountByUserID error", err.Error()) } err = as.userCommon.UpdateAnswerCount(ctx, req.UserID, int(userAnswerCount)) if err != nil { log.Error("user IncreaseAnswerCount error", err.Error()) } revisionDTO := &schema.AddRevisionDTO{ UserID: insertData.UserID, ObjectID: insertData.ID, Title: "", } infoJSON, _ := json.Marshal(insertData) revisionDTO.Content = string(infoJSON) revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return insertData.ID, err } if insertData.Status == entity.AnswerStatusAvailable { as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title, htmltext.FetchExcerpt(insertData.ParsedText, "...", 240)) } as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: insertData.UserID, ObjectID: insertData.ID, OriginalObjectID: insertData.ID, ActivityTypeKey: constant.ActAnswerAnswered, RevisionID: revisionID, }) as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: insertData.UserID, ObjectID: insertData.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionAnswered, }) as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerCreate, req.UserID).TID(insertData.ID). AID(insertData.ID, insertData.UserID)) return insertData.ID, nil } func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq) (string, error) { var canUpdate bool _, existUnreviewed, err := as.revisionService.ExistUnreviewedByObjectID(ctx, req.ID) if err != nil { return "", err } if existUnreviewed { return "", errors.BadRequest(reason.AnswerCannotUpdate) } answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID) if err != nil { return "", err } if !exist { return "", errors.BadRequest(reason.AnswerNotFound) } if answerInfo.Status == entity.AnswerStatusDeleted { return "", errors.BadRequest(reason.AnswerCannotUpdate) } questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) if err != nil { return "", err } if !exist { return "", errors.BadRequest(reason.QuestionNotFound) } // If the content is the same, ignore it if answerInfo.OriginalText == req.Content { return "", nil } insertData := &entity.Answer{} insertData.ID = req.ID insertData.UserID = answerInfo.UserID insertData.QuestionID = questionInfo.ID insertData.OriginalText = req.Content insertData.ParsedText = req.HTML insertData.UpdatedAt = time.Now() insertData.LastEditUserID = "0" if answerInfo.UserID != req.UserID { insertData.LastEditUserID = req.UserID } revisionDTO := &schema.AddRevisionDTO{ UserID: req.UserID, ObjectID: req.ID, Log: req.EditSummary, } if req.NoNeedReview || answerInfo.UserID == req.UserID { canUpdate = true } if !canUpdate { revisionDTO.Status = entity.RevisionUnreviewedStatus } else { insertData.ParsedText, err = as.questionCommon.UpdateQuestionLink(ctx, insertData.QuestionID, insertData.ID, insertData.ParsedText, insertData.OriginalText) if err != nil { return "", err } if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}); err != nil { return "", err } err = as.questionCommon.UpdatePostTime(ctx, questionInfo.ID) if err != nil { return insertData.ID, err } as.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID) revisionDTO.Status = entity.RevisionReviewPassStatus } infoJSON, _ := json.Marshal(insertData) revisionDTO.Content = string(infoJSON) revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return insertData.ID, err } if canUpdate { as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: insertData.ID, OriginalObjectID: insertData.ID, ActivityTypeKey: constant.ActAnswerEdited, RevisionID: revisionID, }) as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerUpdate, req.UserID).TID(insertData.ID). AID(insertData.ID, insertData.UserID)) } return insertData.ID, nil } // AcceptAnswer accept answer func (as *AnswerService) AcceptAnswer(ctx context.Context, req *schema.AcceptAnswerReq) (err error) { // find question questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID) if err != nil { return err } if !exist { return errors.BadRequest(reason.QuestionNotFound) } questionInfo.ID = uid.DeShortID(questionInfo.ID) if questionInfo.AcceptedAnswerID == req.AnswerID { return nil } // find answer var acceptedAnswerInfo *entity.Answer if len(req.AnswerID) > 1 { acceptedAnswerInfo, exist, err = as.answerRepo.GetByID(ctx, req.AnswerID) if err != nil { return err } if !exist { return errors.BadRequest(reason.AnswerNotFound) } // check answer belong to question if acceptedAnswerInfo.QuestionID != req.QuestionID { return errors.BadRequest(reason.AnswerNotFound) } acceptedAnswerInfo.ID = uid.DeShortID(acceptedAnswerInfo.ID) } // update answers status if err = as.answerRepo.UpdateAcceptedStatus(ctx, req.AnswerID, req.QuestionID); err != nil { return err } // update question status err = as.questionCommon.UpdateAccepted(ctx, req.QuestionID, req.AnswerID) if err != nil { log.Error("UpdateLastAnswer error", err.Error()) } var oldAnswerInfo *entity.Answer if len(questionInfo.AcceptedAnswerID) > 1 { oldAnswerInfo, _, err = as.answerRepo.GetByID(ctx, questionInfo.AcceptedAnswerID) if err != nil { return err } oldAnswerInfo.ID = uid.DeShortID(oldAnswerInfo.ID) } if acceptedAnswerInfo != nil { as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionAccept, req.UserID).TID(acceptedAnswerInfo.ID). QID(questionInfo.ID, questionInfo.UserID).AID(acceptedAnswerInfo.ID, acceptedAnswerInfo.UserID)) } as.updateAnswerRank(ctx, req.UserID, questionInfo, acceptedAnswerInfo, oldAnswerInfo) return nil } func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string, questionInfo *entity.Question, newAnswerInfo *entity.Answer, oldAnswerInfo *entity.Answer, ) { // if this question is already been answered, should cancel old answer rank if oldAnswerInfo != nil { err := as.answerActivityService.CancelAcceptAnswer(ctx, userID, questionInfo.AcceptedAnswerID, questionInfo.ID, questionInfo.UserID, oldAnswerInfo.UserID) if err != nil { log.Error(err) } } if newAnswerInfo != nil { err := as.answerActivityService.AcceptAnswer(ctx, userID, newAnswerInfo.ID, questionInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == questionInfo.UserID) if err != nil { log.Error(err) } } } func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string) (*schema.AnswerInfo, *schema.QuestionInfoResp, bool, error) { answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID) if err != nil { return nil, nil, has, err } info := as.ShowFormat(ctx, answerInfo) // todo questionFunc questionInfo, err := as.questionCommon.Info(ctx, answerInfo.QuestionID, loginUserID) if err != nil { return nil, nil, has, err } // todo UserFunc userIds := make([]string, 0) userIds = append(userIds, answerInfo.UserID) userIds = append(userIds, answerInfo.LastEditUserID) userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIds) if err != nil { return nil, nil, has, err } _, ok := userInfoMap[answerInfo.UserID] if ok { info.UserInfo = userInfoMap[answerInfo.UserID] } _, ok = userInfoMap[answerInfo.LastEditUserID] if ok { info.UpdateUserInfo = userInfoMap[answerInfo.LastEditUserID] } if loginUserID == "" { return info, questionInfo, has, nil } info.VoteStatus = as.voteRepo.GetVoteStatus(ctx, answerID, loginUserID) collectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{answerInfo.ID}) if err != nil { return nil, nil, has, err } if len(collectedMap) > 0 { info.Collected = true } return info, questionInfo, has, nil } func (as *AnswerService) GetDetail(ctx context.Context, answerID string) (*schema.AnswerInfo, error) { answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID) if err != nil { return nil, err } if !has { return nil, errors.BadRequest(reason.AnswerNotFound) } info := as.ShowFormat(ctx, answerInfo) return info, nil } func (as *AnswerService) GetCountByUserIDQuestionID(ctx context.Context, userId string, questionId string) (ids []string, err error) { return as.answerRepo.GetIDsByUserIDAndQuestionID(ctx, userId, questionId) } func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.AdminUpdateAnswerStatusReq) error { setStatus, ok := entity.AdminAnswerSearchStatus[req.Status] if !ok { return errors.BadRequest(reason.RequestFormatError) } answerInfo, exist, err := as.answerRepo.GetAnswer(ctx, req.AnswerID) if err != nil { return err } if !exist { return errors.BadRequest(reason.AnswerNotFound) } if setStatus == entity.AnswerStatusDeleted { if err := as.RemoveAnswer(ctx, &schema.RemoveAnswerReq{ ID: req.AnswerID, UserID: req.UserID, CanDelete: true, }); err != nil { return err } msg := &schema.NotificationMsg{} msg.ObjectID = answerInfo.ID msg.Type = schema.NotificationTypeInbox msg.ReceiverUserID = answerInfo.UserID msg.TriggerUserID = answerInfo.UserID msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationYourAnswerWasDeleted as.notificationQueueService.Send(ctx, msg) } // recover if setStatus == entity.QuestionStatusAvailable && answerInfo.Status == entity.QuestionStatusDeleted { if err := as.RecoverAnswer(ctx, &schema.RecoverAnswerReq{ AnswerID: req.AnswerID, UserID: req.UserID, }); err != nil { return err } } return nil } func (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) { list := make([]*schema.AnswerInfo, 0) dbSearch := entity.AnswerSearch{} dbSearch.QuestionID = req.QuestionID dbSearch.Page = req.Page dbSearch.PageSize = req.PageSize dbSearch.Order = req.Order dbSearch.IncludeDeleted = req.CanDelete dbSearch.LoginUserID = req.UserID answerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch) if err != nil { return list, count, err } answerList, err := as.SearchFormatInfo(ctx, answerOriginalList, req) if err != nil { return answerList, count, err } return answerList, count, nil } func (as *AnswerService) SearchFormatInfo(ctx context.Context, answers []*entity.Answer, req *schema.AnswerListReq) ( []*schema.AnswerInfo, error) { list := make([]*schema.AnswerInfo, 0) objectIDs := make([]string, 0) userIDs := make([]string, 0) for _, info := range answers { item := as.ShowFormat(ctx, info) list = append(list, item) objectIDs = append(objectIDs, info.ID) userIDs = append(userIDs, info.UserID, info.LastEditUserID) } userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs) if err != nil { return list, err } for _, item := range list { item.UserInfo = userInfoMap[item.UserID] item.UpdateUserInfo = userInfoMap[item.UpdateUserID] } if len(req.UserID) == 0 { return list, nil } collectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, req.UserID, objectIDs) if err != nil { return nil, err } for _, item := range list { item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, req.UserID) item.Collected = collectedMap[item.ID] item.MemberActions = permission.GetAnswerPermission(ctx, req.UserID, item.UserID, item.Status, req.CanEdit, req.CanDelete, req.CanRecover) } return list, nil } func (as *AnswerService) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo { return as.AnswerCommon.ShowFormat(ctx, data) } func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionUserID, answerID, answerUserID string) { // If the answer is updated by me, there is no notification for myself. // equivalent behaviour as AnswerService.notificationAnswerTheQuestion if questionUserID == answerUserID { return } msg := &schema.NotificationMsg{ TriggerUserID: answerUserID, ReceiverUserID: questionUserID, Type: schema.NotificationTypeInbox, ObjectID: answerID, } msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationUpdateAnswer as.notificationQueueService.Send(ctx, msg) } func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, questionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) { // If the question is answered by me, there is no notification for myself. if questionUserID == answerUserID { return } msg := &schema.NotificationMsg{ TriggerUserID: answerUserID, ReceiverUserID: questionUserID, Type: schema.NotificationTypeInbox, ObjectID: answerID, } msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationAnswerTheQuestion as.notificationQueueService.Send(ctx, msg) receiverUserInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID) if err != nil { log.Error(err) return } if !exist { log.Warnf("user %s not found", questionUserID) return } externalNotificationMsg := &schema.ExternalNotificationMsg{ ReceiverUserID: receiverUserInfo.ID, ReceiverEmail: receiverUserInfo.EMail, ReceiverLang: receiverUserInfo.Language, } rawData := &schema.NewAnswerTemplateRawData{ QuestionTitle: questionTitle, QuestionID: questionID, AnswerID: answerID, AnswerSummary: answerSummary, UnsubscribeCode: token.GenerateToken(), } answerUser, _, _ := as.userCommon.GetUserBasicInfoByID(ctx, answerUserID) if answerUser != nil { rawData.AnswerUserDisplayName = answerUser.DisplayName } externalNotificationMsg.NewAnswerTemplateRawData = rawData as.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } ================================================ FILE: internal/service/content/question_hottest_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package content import ( "context" "math" "time" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/log" ) func (q *QuestionService) RefreshHottestCron(ctx context.Context) { var ( page = 1 pageSize = 100 ) for { questionList, _, err := q.questionRepo.GetQuestionPage( ctx, page, pageSize, []string{}, "", "newest", schema.HotInDays, false, false) if err != nil { return } for _, question := range questionList { updatedAt := question.UpdatedAt.Unix() if updatedAt < 0 { updatedAt = question.CreatedAt.Unix() } qAgeInHours := (time.Now().Unix() - question.CreatedAt.Unix()) / 3600 qUpdated := (time.Now().Unix() - updatedAt) / 3600 aScores, err := q.answerRepo.SumVotesByQuestionID(ctx, question.ID) if err != nil { aScores = 0 } score := q.getScore(float64(question.ViewCount), float64(question.AnswerCount), float64(question.VoteCount), aScores, float64(qAgeInHours), float64(qUpdated)) if score < 0 { score = 0 } questioninfo := &entity.Question{} questioninfo.ID = question.ID questioninfo.HotScore = int(math.Ceil(score * 10000)) err = q.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"hot_score"}) if err != nil { log.Error("update question hot score error,question ID:", question.ID, " error: ", err) } } if len(questionList) < pageSize { break } page++ } } func (q *QuestionService) getScore(qViews, qAnswers, qScore, aScores, qAgeInHours, qUpdated float64) (score float64) { score = ((math.Log(qViews) * 4) + ((qAnswers * qScore) / 5) + aScores) / math.Pow(((qAgeInHours+1)-((qAgeInHours-qUpdated)/2)), 1.5) return score } ================================================ FILE: internal/service/content/question_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package content import ( "encoding/json" "fmt" "strings" "time" "github.com/apache/answer/internal/service/eventqueue" "github.com/apache/answer/plugin" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/activityqueue" answercommon "github.com/apache/answer/internal/service/answer_common" collectioncommon "github.com/apache/answer/internal/service/collection_common" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/export" metacommon "github.com/apache/answer/internal/service/meta_common" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/notification" "github.com/apache/answer/internal/service/permission" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/review" "github.com/apache/answer/internal/service/revision_common" "github.com/apache/answer/internal/service/role" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/internal/service/tag" tagcommon "github.com/apache/answer/internal/service/tag_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/token" "github.com/apache/answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "golang.org/x/net/context" ) // QuestionRepo question repository // QuestionService user service type QuestionService struct { activityRepo activity_common.ActivityRepo questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo tagCommon *tagcommon.TagCommonService tagService *tag.TagService questioncommon *questioncommon.QuestionCommon userCommon *usercommon.UserCommon userRepo usercommon.UserRepo userRoleRelService *role.UserRoleRelService revisionService *revision_common.RevisionService metaService *metacommon.MetaCommonService collectionCommon *collectioncommon.CollectionCommon answerActivityService *activity.AnswerActivityService emailService *export.EmailService notificationQueueService noticequeue.Service externalNotificationQueueService noticequeue.ExternalService activityQueueService activityqueue.Service siteInfoService siteinfo_common.SiteInfoCommonService newQuestionNotificationService *notification.ExternalNotificationService reviewService *review.ReviewService configService *config.ConfigService eventQueueService eventqueue.Service reviewRepo review.ReviewRepo } func NewQuestionService( activityRepo activity_common.ActivityRepo, questionRepo questioncommon.QuestionRepo, answerRepo answercommon.AnswerRepo, tagCommon *tagcommon.TagCommonService, tagService *tag.TagService, questioncommon *questioncommon.QuestionCommon, userCommon *usercommon.UserCommon, userRepo usercommon.UserRepo, userRoleRelService *role.UserRoleRelService, revisionService *revision_common.RevisionService, metaService *metacommon.MetaCommonService, collectionCommon *collectioncommon.CollectionCommon, answerActivityService *activity.AnswerActivityService, emailService *export.EmailService, notificationQueueService noticequeue.Service, externalNotificationQueueService noticequeue.ExternalService, activityQueueService activityqueue.Service, siteInfoService siteinfo_common.SiteInfoCommonService, newQuestionNotificationService *notification.ExternalNotificationService, reviewService *review.ReviewService, configService *config.ConfigService, eventQueueService eventqueue.Service, reviewRepo review.ReviewRepo, ) *QuestionService { return &QuestionService{ activityRepo: activityRepo, questionRepo: questionRepo, answerRepo: answerRepo, tagCommon: tagCommon, tagService: tagService, questioncommon: questioncommon, userCommon: userCommon, userRepo: userRepo, userRoleRelService: userRoleRelService, revisionService: revisionService, metaService: metaService, collectionCommon: collectionCommon, answerActivityService: answerActivityService, emailService: emailService, notificationQueueService: notificationQueueService, externalNotificationQueueService: externalNotificationQueueService, activityQueueService: activityQueueService, siteInfoService: siteInfoService, newQuestionNotificationService: newQuestionNotificationService, reviewService: reviewService, configService: configService, eventQueueService: eventQueueService, reviewRepo: reviewRepo, } } func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQuestionReq) error { questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) if err != nil { return err } if !has { return nil } cf, err := qs.configService.GetConfigByID(ctx, req.CloseType) if err != nil || cf == nil { return errors.BadRequest(reason.ReportNotFound) } if cf.Key == constant.ReasonADuplicate && !checker.IsURL(req.CloseMsg) { return errors.BadRequest(reason.InvalidURLError) } questionInfo.Status = entity.QuestionStatusClosed err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status) if err != nil { return err } closeMeta, _ := json.Marshal(schema.CloseQuestionMeta{ CloseType: req.CloseType, CloseMsg: req.CloseMsg, }) err = qs.metaService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta)) if err != nil { return err } if cf.Key == constant.ReasonADuplicate { qs.questioncommon.AddQuestionLinkForCloseReason(ctx, questionInfo, req.CloseMsg) } qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionClosed, }) return nil } // ReopenQuestion reopen question func (qs *QuestionService) ReopenQuestion(ctx context.Context, req *schema.ReopenQuestionReq) error { questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID) if err != nil { return err } if !has { return nil } questionInfo.Status = entity.QuestionStatusAvailable err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status) if err != nil { return err } qs.questioncommon.RemoveQuestionLinkForReopen(ctx, questionInfo) qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionReopened, }) return nil } func (qs *QuestionService) AddQuestionCheckTags(ctx context.Context, tags []*entity.Tag) ([]string, error) { list := make([]string, 0) for _, tag := range tags { if tag.Reserved { list = append(list, tag.DisplayName) } } if len(list) > 0 { return list, errors.BadRequest(reason.RequestFormatError) } return []string{}, nil } func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.QuestionAdd) (errorlist any, err error) { minimumTags, err := qs.tagCommon.GetMinimumTags(ctx) if err != nil { return } if len(req.Tags) < minimumTags { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount), }) err = errors.BadRequest(reason.TagMinCount) return errorlist, err } minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx) if err != nil { return } if len(req.Content) < minimumContentLength { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "content", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum), }) err = errors.BadRequest(reason.QuestionContentLessThanMinimum) return errorlist, err } recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags) if err != nil { return } if !recommendExist { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), }) err = errors.BadRequest(reason.RecommendTagEnter) return errorlist, err } tagNameList := make([]string, 0) for _, tag := range req.Tags { tagNameList = append(tagNameList, tag.SlugName) } Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) if tagerr != nil { return errorlist, tagerr } if !req.CanUseReservedTag { taglist, err := qs.AddQuestionCheckTags(ctx, Tags) errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`, strings.Join(taglist, ",")) if err != nil { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: errMsg, }) err = errors.BadRequest(reason.RecommendTagEnter) return errorlist, err } } return nil, nil } // HasNewTag func (qs *QuestionService) HasNewTag(ctx context.Context, tags []*schema.TagItem) (bool, error) { return qs.tagCommon.HasNewTag(ctx, tags) } // AddQuestion add question func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo any, err error) { minimumTags, err := qs.tagCommon.GetMinimumTags(ctx) if err != nil { return } if len(req.Tags) < minimumTags { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount), }) err = errors.BadRequest(reason.TagMinCount) return errorlist, err } minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx) if err != nil { return } if len(req.Content) < minimumContentLength { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "content", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum), }) err = errors.BadRequest(reason.QuestionContentLessThanMinimum) return errorlist, err } recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags) if err != nil { return } if !recommendExist { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), }) err = errors.BadRequest(reason.RecommendTagEnter) return errorlist, err } tagNameList := make([]string, 0) for _, tag := range req.Tags { tag.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-") tagNameList = append(tagNameList, tag.SlugName) } tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) if tagerr != nil { return questionInfo, tagerr } if !req.CanUseReservedTag { taglist, err := qs.AddQuestionCheckTags(ctx, tags) errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`, strings.Join(taglist, ",")) if err != nil { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: errMsg, }) err = errors.BadRequest(reason.RecommendTagEnter) return errorlist, err } } question := &entity.Question{} now := time.Now() question.UserID = req.UserID question.Title = req.Title question.OriginalText = req.Content question.ParsedText = req.HTML question.AcceptedAnswerID = "0" question.LastAnswerID = "0" question.LastEditUserID = "0" // question.PostUpdateTime = nil question.Status = entity.QuestionStatusPending question.RevisionID = "0" question.CreatedAt = now question.PostUpdateTime = now question.Pin = entity.QuestionUnPin question.Show = entity.QuestionShow // question.UpdatedAt = nil err = qs.questionRepo.AddQuestion(ctx, question) if err != nil { return } question.Status = qs.reviewService.AddQuestionReview(ctx, question, req.Tags, req.IP, req.UserAgent) if err := qs.questionRepo.UpdateQuestionStatus(ctx, question.ID, question.Status); err != nil { return nil, err } if question.Status == entity.QuestionStatusAvailable { question.ParsedText, err = qs.questioncommon.UpdateQuestionLink(ctx, question.ID, "", question.ParsedText, question.OriginalText) if err != nil { return nil, err } err = qs.questionRepo.UpdateQuestion(ctx, question, []string{"parsed_text"}) if err != nil { return nil, err } } objectTagData := schema.TagChange{} objectTagData.ObjectID = question.ID objectTagData.Tags = req.Tags objectTagData.UserID = req.UserID errorlist, err := qs.ChangeTag(ctx, &objectTagData) if err != nil { return errorlist, err } _ = qs.questionRepo.UpdateSearch(ctx, question.ID) revisionDTO := &schema.AddRevisionDTO{ UserID: question.UserID, ObjectID: question.ID, Title: question.Title, } questionWithTagsRevision := qs.changeQuestionToRevision(ctx, question, tags) infoJSON, _ := json.Marshal(questionWithTagsRevision) revisionDTO.Content = string(infoJSON) revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return } // user add question count userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, question.UserID) if err != nil { log.Errorf("get user question count error %v", err) } else { err = qs.userCommon.UpdateQuestionCount(ctx, question.UserID, userQuestionCount) if err != nil { log.Errorf("update user question count error %v", err) } } qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: question.UserID, ObjectID: question.ID, OriginalObjectID: question.ID, ActivityTypeKey: constant.ActQuestionAsked, RevisionID: revisionID, }) if question.Status == entity.QuestionStatusAvailable { newTags, newTagsErr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) if newTagsErr != nil { log.Error("get question newTags error %v", newTagsErr) qs.externalNotificationQueueService.Send(ctx, schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, tags)) } else { qs.externalNotificationQueueService.Send(ctx, schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, newTags)) } } qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionCreate, req.UserID).TID(question.ID). QID(question.ID, question.UserID)) questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission) return } // OperationQuestion func (qs *QuestionService) OperationQuestion(ctx context.Context, req *schema.OperationQuestionReq) (err error) { questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) if err != nil { return err } if !has { return nil } // Hidden question cannot be placed at the top if questionInfo.Show == entity.QuestionHide && req.Operation == schema.QuestionOperationPin { return nil } // Question cannot be hidden when they are at the top if questionInfo.Pin == entity.QuestionPin && req.Operation == schema.QuestionOperationHide { return nil } switch req.Operation { case schema.QuestionOperationHide: questionInfo.Show = entity.QuestionHide err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{ FromQuestionID: questionInfo.ID, }, &entity.QuestionLink{ ToQuestionID: questionInfo.ID, }) if err != nil { return } err = qs.tagCommon.HideTagRelListByObjectID(ctx, req.ID) if err != nil { return err } err = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID) if err != nil { return err } case schema.QuestionOperationShow: questionInfo.Show = entity.QuestionShow err = qs.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{ FromQuestionID: questionInfo.ID, }, &entity.QuestionLink{ ToQuestionID: questionInfo.ID, }) if err != nil { return } err = qs.tagCommon.ShowTagRelListByObjectID(ctx, req.ID) if err != nil { return err } err = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID) if err != nil { return err } case schema.QuestionOperationPin: questionInfo.Pin = entity.QuestionPin case schema.QuestionOperationUnPin: questionInfo.Pin = entity.QuestionUnPin } err = qs.questionRepo.UpdateQuestionOperation(ctx, questionInfo) if err != nil { return err } actMap := make(map[string]constant.ActivityTypeKey) actMap[schema.QuestionOperationPin] = constant.ActQuestionPin actMap[schema.QuestionOperationUnPin] = constant.ActQuestionUnPin actMap[schema.QuestionOperationHide] = constant.ActQuestionHide actMap[schema.QuestionOperationShow] = constant.ActQuestionShow _, ok := actMap[req.Operation] if ok { qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: actMap[req.Operation], }) } return nil } // RemoveQuestion delete question func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) { questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) if err != nil { return err } // if the status is deleted, return directly if questionInfo.Status == entity.QuestionStatusDeleted { return nil } if !has { return nil } if !req.IsAdmin { if questionInfo.UserID != req.UserID { return errors.BadRequest(reason.QuestionCannotDeleted) } if questionInfo.AcceptedAnswerID != "0" { return errors.BadRequest(reason.QuestionCannotDeleted) } if questionInfo.AnswerCount > 1 { return errors.BadRequest(reason.QuestionCannotDeleted) } if questionInfo.AnswerCount == 1 { answersearch := &entity.AnswerSearch{} answersearch.QuestionID = req.ID answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch) if err != nil { return err } for _, answer := range answerList { if answer.VoteCount > 0 { return errors.BadRequest(reason.QuestionCannotDeleted) } } } } questionInfo.Status = entity.QuestionStatusDeleted err = qs.questionRepo.UpdateQuestionStatusWithOutUpdateTime(ctx, questionInfo) if err != nil { return err } userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, questionInfo.UserID) if err != nil { log.Error("user GetUserQuestionCount error", err.Error()) } else { err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount) if err != nil { log.Error("user IncreaseQuestionCount error", err.Error()) } } // If this question has been reviewed, then delete the review. reviewInfo, exist, err := qs.reviewRepo.GetReviewByObject(ctx, questionInfo.ID) if exist && err == nil { err = qs.reviewRepo.UpdateReviewStatus(ctx, reviewInfo.ID, req.UserID, entity.ReviewStatusRejected) if err != nil { return errors.InternalServer(reason.DatabaseError) } } // tag count tagIDs := make([]string, 0) Tags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID) if tagerr != nil { log.Error("GetObjectEntityTag error", tagerr) return nil } for _, v := range Tags { tagIDs = append(tagIDs, v.ID) } err = qs.tagCommon.RemoveTagRelListByObjectID(ctx, req.ID) if err != nil { log.Error("RemoveTagRelListByObjectID error", err.Error()) } err = qs.tagCommon.RefreshTagQuestionCount(ctx, tagIDs) if err != nil { log.Error("efreshTagQuestionCount error", err.Error()) } // #2372 In order to simplify the process and complexity, as well as to consider if it is in-house, // facing the problem of recovery. // err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount) // if err != nil { // log.Errorf("user DeleteQuestion rank rollback error %s", err.Error()) // } err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{ FromQuestionID: questionInfo.ID, }, &entity.QuestionLink{ ToQuestionID: questionInfo.ID, }) if err != nil { return } qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: questionInfo.UserID, TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionDeleted, }) qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionDelete, req.UserID).TID(questionInfo.ID). QID(questionInfo.ID, questionInfo.UserID)) return nil } func (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *schema.QuestionUpdate) (errorlist []*validator.FormErrorField, err error) { dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) if err != nil { return } if !has { return } oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID) if tagerr != nil { log.Error("GetObjectEntityTag error", tagerr) return nil, nil } tagNameList := make([]string, 0) oldtagNameList := make([]string, 0) for _, tag := range req.Tags { tagNameList = append(tagNameList, tag.SlugName) } for _, tag := range oldTags { oldtagNameList = append(oldtagNameList, tag.SlugName) } isChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList) // If the content is the same, ignore it if dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange { return } Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) if tagerr != nil { log.Error("GetTagListByNames error", tagerr) return nil, nil } // if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added. if !req.CanUseReservedTag { CheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags) if !CheckOldTag { errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`, strings.Join(CheckOldTaglist, ",")) errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: errMsg, }) err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) return errorlist, err } if !CheckNewTag { errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`, strings.Join(CheckNewTaglist, ",")) errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: errMsg, }) err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) return errorlist, err } } return nil, nil } func (qs *QuestionService) RecoverQuestion(ctx context.Context, req *schema.QuestionRecoverReq) (err error) { questionInfo, exist, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID) if err != nil { return err } if !exist { return errors.BadRequest(reason.QuestionNotFound) } if questionInfo.Status != entity.QuestionStatusDeleted { return nil } err = qs.questionRepo.RecoverQuestion(ctx, req.QuestionID) if err != nil { return err } // update user's question count userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, questionInfo.UserID) if err != nil { log.Error("user GetUserQuestionCount error", err.Error()) } else { err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount) if err != nil { log.Error("user IncreaseQuestionCount error", err.Error()) } } // update tag's question count if err = qs.tagCommon.RecoverTagRelListByObjectID(ctx, questionInfo.ID); err != nil { log.Errorf("remove tag rel list by object id error %v", err) } tagIDs := make([]string, 0) tags, err := qs.tagCommon.GetObjectEntityTag(ctx, questionInfo.ID) if err != nil { return err } for _, v := range tags { tagIDs = append(tagIDs, v.ID) } if len(tagIDs) > 0 { if err = qs.tagCommon.RefreshTagQuestionCount(ctx, tagIDs); err != nil { log.Errorf("update tag's question count failed, %v", err) } } err = qs.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{ FromQuestionID: questionInfo.ID, }, &entity.QuestionLink{ ToQuestionID: questionInfo.ID, }) if err != nil { return } qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionUndeleted, }) return nil } func (qs *QuestionService) UpdateQuestionInviteUser(ctx context.Context, req *schema.QuestionUpdateInviteUser) (err error) { originQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, req.ID) if err != nil { return err } if !exist { return errors.BadRequest(reason.QuestionNotFound) } // verify invite user inviteUserInfoList, err := qs.userCommon.BatchGetUserBasicInfoByUserNames(ctx, req.InviteUser) if err != nil { log.Error("BatchGetUserBasicInfoByUserNames error", err.Error()) } inviteUserIDs := make([]string, 0) for _, item := range req.InviteUser { _, ok := inviteUserInfoList[item] if ok { inviteUserIDs = append(inviteUserIDs, inviteUserInfoList[item].ID) } } inviteUserStr := "" inviteUserByte, err := json.Marshal(inviteUserIDs) if err != nil { log.Error("json.Marshal error", err.Error()) inviteUserStr = "[]" } else { inviteUserStr = string(inviteUserByte) } question := &entity.Question{} question.ID = uid.DeShortID(req.ID) question.InviteUserID = inviteUserStr saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"invite_user_id"}) if saveerr != nil { return saveerr } // send notification oldInviteUserIDsStr := originQuestion.InviteUserID oldInviteUserIDs := make([]string, 0) needSendNotificationUserIDs := make([]string, 0) if oldInviteUserIDsStr != "" { err = json.Unmarshal([]byte(oldInviteUserIDsStr), &oldInviteUserIDs) if err == nil { needSendNotificationUserIDs = converter.ArrayNotInArray(oldInviteUserIDs, inviteUserIDs) } } else { needSendNotificationUserIDs = inviteUserIDs } go qs.notificationInviteUser(ctx, needSendNotificationUserIDs, originQuestion.ID, originQuestion.Title, req.UserID) return nil } func (qs *QuestionService) notificationInviteUser( ctx context.Context, invitedUserIDs []string, questionID, questionTitle, questionUserID string) { inviter, exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, questionUserID) if err != nil { log.Error(err) return } if !exist { log.Warnf("user %s not found", questionUserID) return } users, err := qs.userRepo.BatchGetByID(ctx, invitedUserIDs) if err != nil { log.Error(err) return } invitee := make(map[string]*entity.User, len(users)) for _, user := range users { invitee[user.ID] = user } for _, userID := range invitedUserIDs { msg := &schema.NotificationMsg{ ReceiverUserID: userID, TriggerUserID: questionUserID, Type: schema.NotificationTypeInbox, ObjectID: questionID, } msg.ObjectType = constant.QuestionObjectType msg.NotificationAction = constant.NotificationInvitedYouToAnswer qs.notificationQueueService.Send(ctx, msg) receiverUserInfo, ok := invitee[userID] if !ok { log.Warnf("user %s not found", userID) return } externalNotificationMsg := &schema.ExternalNotificationMsg{ ReceiverUserID: receiverUserInfo.ID, ReceiverEmail: receiverUserInfo.EMail, ReceiverLang: receiverUserInfo.Language, } rawData := &schema.NewInviteAnswerTemplateRawData{ InviterDisplayName: inviter.DisplayName, QuestionTitle: questionTitle, QuestionID: questionID, UnsubscribeCode: token.GenerateToken(), } externalNotificationMsg.NewInviteAnswerTemplateRawData = rawData qs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } } // UpdateQuestion update question func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) { var canUpdate bool questionInfo = &schema.QuestionInfoResp{} _, existUnreviewed, err := qs.revisionService.ExistUnreviewedByObjectID(ctx, req.ID) if err != nil { return } if existUnreviewed { err = errors.BadRequest(reason.QuestionCannotUpdate) return } dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) if err != nil { return } if !has { return } if dbinfo.Status == entity.QuestionStatusDeleted { err = errors.BadRequest(reason.QuestionCannotUpdate) return nil, err } now := time.Now() question := &entity.Question{} question.Title = req.Title question.OriginalText = req.Content question.ParsedText = req.HTML question.ID = uid.DeShortID(req.ID) question.UpdatedAt = now question.PostUpdateTime = now question.UserID = dbinfo.UserID question.LastEditUserID = req.UserID minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx) if err != nil { return } if len(req.Content) < minimumContentLength { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "content", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum), }) err = errors.BadRequest(reason.QuestionContentLessThanMinimum) return errorlist, err } oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, question.ID) if tagerr != nil { return questionInfo, tagerr } tagNameList := make([]string, 0) oldtagNameList := make([]string, 0) for _, tag := range req.Tags { tag.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-") tagNameList = append(tagNameList, tag.SlugName) } for _, tag := range oldTags { oldtagNameList = append(oldtagNameList, tag.SlugName) } isChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList) // If the content is the same, ignore it if dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange { return } Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) if tagerr != nil { return questionInfo, tagerr } // if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added. if !req.CanUseReservedTag { CheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags) if !CheckOldTag { errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`, strings.Join(CheckOldTaglist, ",")) errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: errMsg, }) err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) return errorlist, err } if !CheckNewTag { errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`, strings.Join(CheckNewTaglist, ",")) errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: errMsg, }) err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) return errorlist, err } } // Check whether mandatory labels are selected recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags) if err != nil { return } if !recommendExist { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), }) err = errors.BadRequest(reason.RecommendTagEnter) return errorlist, err } // Administrators and themselves do not need to be audited revisionDTO := &schema.AddRevisionDTO{ UserID: question.UserID, ObjectID: question.ID, Title: question.Title, Log: req.EditSummary, } if req.NoNeedReview { canUpdate = true } // It's not you or the administrator that needs to be reviewed if !canUpdate { revisionDTO.Status = entity.RevisionUnreviewedStatus revisionDTO.UserID = req.UserID // use revision userid } else { // Direct modification revisionDTO.Status = entity.RevisionReviewPassStatus // update question to db question.ParsedText, err = qs.questioncommon.UpdateQuestionLink(ctx, question.ID, "", question.ParsedText, question.OriginalText) if err != nil { return questionInfo, err } saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"}) if saveerr != nil { return questionInfo, saveerr } objectTagData := schema.TagChange{} objectTagData.ObjectID = question.ID objectTagData.Tags = req.Tags objectTagData.UserID = req.UserID errorlist, tagerr := qs.ChangeTag(ctx, &objectTagData) if tagerr != nil { return errorlist, tagerr } } questionWithTagsRevision := qs.changeQuestionToRevision(ctx, question, Tags) infoJSON, _ := json.Marshal(questionWithTagsRevision) revisionDTO.Content = string(infoJSON) revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return } if canUpdate { qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: question.ID, ActivityTypeKey: constant.ActQuestionEdited, RevisionID: revisionID, OriginalObjectID: question.ID, }) qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionUpdate, req.UserID).TID(question.ID). QID(question.ID, question.UserID)) } questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission) return } // GetQuestion get question one func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID string, per schema.QuestionPermission) (resp *schema.QuestionInfoResp, err error) { question, err := qs.questioncommon.Info(ctx, questionID, userID) if err != nil { return } // If the question is deleted or pending, only the administrator and the author can view it if (question.Status == entity.QuestionStatusDeleted || question.Status == entity.QuestionStatusPending) && !per.CanReopen && question.UserID != userID { return nil, errors.NotFound(reason.QuestionNotFound) } if question.Status != entity.QuestionStatusClosed { per.CanReopen = false } if question.Status == entity.QuestionStatusClosed { per.CanClose = false } if question.Pin == entity.QuestionPin { per.CanPin = false per.CanHide = false } if question.Pin == entity.QuestionUnPin { per.CanUnPin = false } if question.Show == entity.QuestionShow { per.CanShow = false } if question.Show == entity.QuestionHide { per.CanHide = false per.CanPin = false } if question.Status == entity.QuestionStatusDeleted { operation := &schema.Operation{} operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionAlreadyDeleted) operation.Level = schema.OperationLevelDanger question.Operation = operation } if question.Status == entity.QuestionStatusPending { operation := &schema.Operation{} operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionUnderReview) operation.Level = schema.OperationLevelSecondary question.Operation = operation } question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240) question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID, question.Status, per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen, per.CanPin, per.CanHide, per.CanUnPin, per.CanShow, per.CanRecover) question.ExtendsActions = permission.GetQuestionExtendsPermission(ctx, per.CanInviteOtherToAnswer) return question, nil } // GetQuestionAndAddPV get question one func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, loginUserID string, per schema.QuestionPermission) ( resp *schema.QuestionInfoResp, err error) { err = qs.questioncommon.UpdatePv(ctx, questionID) if err != nil { log.Error(err) } return qs.GetQuestion(ctx, questionID, loginUserID, per) } func (qs *QuestionService) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) { return qs.questioncommon.InviteUserInfo(ctx, questionID) } func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) (errorlist []*validator.FormErrorField, err error) { minimumTags, err := qs.tagCommon.GetMinimumTags(ctx) if err != nil { return nil, err } return qs.tagCommon.ObjectChangeTag(ctx, objectTagData, minimumTags) } func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) { return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData) } // PersonalQuestionPage get question list by user func (qs *QuestionService) PersonalQuestionPage(ctx context.Context, req *schema.PersonalQuestionPageReq) ( pageModel *pager.PageModel, err error) { userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } search := &schema.QuestionPageReq{} search.OrderCond = req.OrderCond search.Page = req.Page search.PageSize = req.PageSize search.UserIDBeSearched = userinfo.ID search.LoginUserID = req.LoginUserID // Only author and administrator can view the pending question if req.LoginUserID == userinfo.ID || req.IsAdmin { search.ShowPending = true } questionList, total, err := qs.GetQuestionPage(ctx, search) if err != nil { return nil, err } userQuestionInfoList := make([]*schema.UserQuestionInfo, 0) for _, item := range questionList { info := &schema.UserQuestionInfo{} _ = copier.Copy(info, item) status, ok := entity.AdminQuestionSearchStatusIntToString[item.Status] if ok { info.Status = status } userQuestionInfoList = append(userQuestionInfoList, info) } return pager.NewPageModel(total, userQuestionInfoList), nil } func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.PersonalAnswerPageReq) ( pageModel *pager.PageModel, err error) { userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } cond := &entity.PersonalAnswerPageQueryCond{} cond.UserID = userinfo.ID cond.Page = req.Page cond.PageSize = req.PageSize cond.ShowPending = req.IsAdmin || req.LoginUserID == cond.UserID if req.OrderCond == "newest" { cond.Order = entity.AnswerSearchOrderByTime } else { cond.Order = entity.AnswerSearchOrderByDefault } questionIDs := make([]string, 0) answerList, total, err := qs.questioncommon.AnswerCommon.PersonalAnswerPage(ctx, cond) if err != nil { return nil, err } answerlist := make([]*schema.AnswerInfo, 0) userAnswerlist := make([]*schema.UserAnswerInfo, 0) for _, item := range answerList { answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item) answerlist = append(answerlist, answerinfo) questionIDs = append(questionIDs, uid.DeShortID(item.QuestionID)) } questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID) if err != nil { return nil, err } for _, item := range answerlist { _, ok := questionMaps[item.QuestionID] if ok { item.QuestionInfo = questionMaps[item.QuestionID] } else { continue } info := &schema.UserAnswerInfo{} _ = copier.Copy(info, item) info.AnswerID = item.ID info.QuestionID = item.QuestionID if item.QuestionInfo.Status == entity.QuestionStatusDeleted { info.QuestionInfo.Title = "Deleted question" } userAnswerlist = append(userAnswerlist, info) } return pager.NewPageModel(total, userAnswerlist), nil } // PersonalCollectionPage get collection list by user func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *schema.PersonalCollectionPageReq) ( pageModel *pager.PageModel, err error) { list := make([]*schema.QuestionInfoResp, 0) collectionSearch := &entity.CollectionSearch{} collectionSearch.UserID = req.UserID collectionSearch.Page = req.Page collectionSearch.PageSize = req.PageSize collectionList, total, err := qs.collectionCommon.SearchList(ctx, collectionSearch) if err != nil { return nil, err } questionIDs := make([]string, 0) for _, item := range collectionList { questionIDs = append(questionIDs, item.ObjectID) } questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.UserID) if err != nil { return nil, err } for _, id := range questionIDs { if handler.GetEnableShortID(ctx) { id = uid.EnShortID(id) } _, ok := questionMaps[id] if ok { questionMaps[id].LastAnsweredUserInfo = nil questionMaps[id].UpdateUserInfo = nil questionMaps[id].Content = "" questionMaps[id].HTML = "" if questionMaps[id].Status == entity.QuestionStatusDeleted { questionMaps[id].Title = "Deleted question" } list = append(list, questionMaps[id]) } } return pager.NewPageModel(total, list), nil } func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName string, loginUserID string) ([]*schema.UserQuestionInfo, []*schema.UserAnswerInfo, error) { answerlist := make([]*schema.AnswerInfo, 0) userAnswerlist := make([]*schema.UserAnswerInfo, 0) userQuestionlist := make([]*schema.UserQuestionInfo, 0) userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName) if err != nil { return userQuestionlist, userAnswerlist, err } if !Exist { return userQuestionlist, userAnswerlist, nil } search := &schema.QuestionPageReq{} search.OrderCond = "score" search.Page = 0 search.PageSize = 5 search.UserIDBeSearched = userinfo.ID search.LoginUserID = loginUserID questionlist, _, err := qs.GetQuestionPage(ctx, search) if err != nil { return userQuestionlist, userAnswerlist, err } answersearch := &entity.AnswerSearch{} answersearch.UserID = userinfo.ID answersearch.PageSize = 5 answersearch.Order = entity.AnswerSearchOrderByVote questionIDs := make([]string, 0) answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch) if err != nil { return userQuestionlist, userAnswerlist, err } for _, item := range answerList { answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item) answerlist = append(answerlist, answerinfo) questionIDs = append(questionIDs, item.QuestionID) } questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID) if err != nil { return userQuestionlist, userAnswerlist, err } for _, item := range answerlist { _, ok := questionMaps[item.QuestionID] if ok { item.QuestionInfo = questionMaps[item.QuestionID] } } for _, item := range questionlist { info := &schema.UserQuestionInfo{} _ = copier.Copy(info, item) info.UrlTitle = htmltext.UrlTitle(info.Title) userQuestionlist = append(userQuestionlist, info) } for _, item := range answerlist { info := &schema.UserAnswerInfo{} _ = copier.Copy(info, item) info.AnswerID = item.ID info.QuestionID = item.QuestionID info.QuestionInfo.UrlTitle = htmltext.UrlTitle(info.QuestionInfo.Title) userAnswerlist = append(userAnswerlist, info) } return userQuestionlist, userAnswerlist, nil } // GetQuestionsByTitle get questions by title func (qs *QuestionService) GetQuestionsByTitle(ctx context.Context, title string) ( resp []*schema.QuestionBaseInfo, err error) { resp = make([]*schema.QuestionBaseInfo, 0) if len(title) == 0 { return resp, nil } // check search plugin var finder plugin.Search _ = plugin.CallSearch(func(search plugin.Search) error { finder = search return nil }) var questions []*entity.Question if finder != nil { // call search plugin if available words := []string{title} res, _, err := finder.SearchQuestions(ctx, &plugin.SearchBasicCond{ Words: words, Page: 1, PageSize: 10, }) if err != nil { return resp, err } // get question ids from res questionIDs := make([]string, 0) for _, question := range res { questionIDs = append(questionIDs, question.ID) } var questionErr error questions, questionErr = qs.questionRepo.FindByID(ctx, questionIDs) if questionErr != nil { return resp, questionErr } } else { var questionErr error questions, questionErr = qs.questionRepo.GetQuestionsByTitle(ctx, title, 10) if questionErr != nil { return resp, questionErr } } for _, question := range questions { item := &schema.QuestionBaseInfo{} item.ID = question.ID item.Title = question.Title item.UrlTitle = htmltext.UrlTitle(question.Title) item.ViewCount = question.ViewCount item.AnswerCount = question.AnswerCount item.CollectionCount = question.CollectionCount item.FollowCount = question.FollowCount status, ok := entity.AdminQuestionSearchStatusIntToString[question.Status] if ok { item.Status = status } if question.AcceptedAnswerID != "0" { item.AcceptedAnswer = true } resp = append(resp, item) } return resp, nil } // SimilarQuestion func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionPageResp, int64, error) { question, err := qs.questioncommon.Info(ctx, questionID, loginUserID) if err != nil { return nil, 0, nil } tagNames := make([]string, 0, len(question.Tags)) for _, tag := range question.Tags { tagNames = append(tagNames, tag.SlugName) } search := &schema.QuestionPageReq{} search.OrderCond = "hot" search.Page = 0 search.PageSize = 6 if len(tagNames) > 0 { search.Tag = tagNames[0] } search.LoginUserID = loginUserID similarQuestions, _, err := qs.GetQuestionPage(ctx, search) if err != nil { return nil, 0, err } var result []*schema.QuestionPageResp for _, v := range similarQuestions { if uid.DeShortID(v.ID) != questionID { result = append(result, v) } } return result, int64(len(result)), nil } // GetQuestionPage query questions page func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.QuestionPageReq) ( questions []*schema.QuestionPageResp, total int64, err error) { questions = make([]*schema.QuestionPageResp, 0) // query by user role showHidden := false if req.LoginUserID != "" && req.UserIDBeSearched != "" { showHidden = req.LoginUserID == req.UserIDBeSearched if !showHidden { userRole, err := qs.userRoleRelService.GetUserRole(ctx, req.LoginUserID) if err != nil { return nil, 0, err } showHidden = userRole == role.RoleAdminID || userRole == role.RoleModeratorID } } // query by tag condition var tagIDs = make([]string, 0) if len(req.Tag) > 0 { tagInfo, exist, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag)) if err != nil { return nil, 0, err } if exist { synTagIds, err := qs.tagCommon.GetTagIDsByMainTagID(ctx, tagInfo.ID) if err != nil { return nil, 0, err } tagIDs = append(tagIDs, synTagIds...) tagIDs = append(tagIDs, tagInfo.ID) } else { return questions, 0, nil } } // query by user condition if req.Username != "" { userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) if err != nil { return nil, 0, err } if !exist { return questions, 0, nil } req.UserIDBeSearched = userinfo.ID } if req.OrderCond == schema.QuestionOrderCondHot { req.InDays = schema.HotInDays } questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize, tagIDs, req.UserIDBeSearched, req.OrderCond, req.InDays, showHidden, req.ShowPending) if err != nil { return nil, 0, err } questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, req.OrderCond) if err != nil { return nil, 0, err } return questions, total, nil } // GetRecommendQuestionPage retrieves recommended question page based on following tags and questions. func (qs *QuestionService) GetRecommendQuestionPage(ctx context.Context, req *schema.QuestionPageReq) ( questions []*schema.QuestionPageResp, total int64, err error) { followingTagsResp, err := qs.tagService.GetFollowingTags(ctx, req.LoginUserID) if err != nil { return nil, 0, err } tagIDs := make([]string, 0, len(followingTagsResp)) for _, tag := range followingTagsResp { tagIDs = append(tagIDs, tag.TagID) } activityType, err := qs.activityRepo.GetActivityTypeByObjectType(ctx, constant.QuestionObjectType, "follow") if err != nil { return nil, 0, err } activities, err := qs.activityRepo.GetUserActivitiesByActivityType(ctx, req.LoginUserID, activityType) if err != nil { return nil, 0, err } followedQuestionIDs := make([]string, 0, len(activities)) for _, activity := range activities { if activity.Cancelled == entity.ActivityCancelled { continue } followedQuestionIDs = append(followedQuestionIDs, activity.ObjectID) } questionList, total, err := qs.questionRepo.GetRecommendQuestionPageByTags(ctx, req.LoginUserID, tagIDs, followedQuestionIDs, req.Page, req.PageSize) if err != nil { return nil, 0, err } questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, schema.QuestionOrderCondFrequent) if err != nil { return nil, 0, err } return questions, total, nil } func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, req *schema.AdminUpdateQuestionStatusReq) error { setStatus, ok := entity.AdminQuestionSearchStatus[req.Status] if !ok { return errors.BadRequest(reason.RequestFormatError) } questionInfo, exist, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID) if err != nil { return err } if !exist { return errors.BadRequest(reason.QuestionNotFound) } err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, setStatus) if err != nil { return err } msg := &schema.NotificationMsg{} if setStatus == entity.QuestionStatusDeleted { // #2372 In order to simplify the process and complexity, as well as to consider if it is in-house, // facing the problem of recovery. // err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount) // if err != nil { // log.Errorf("admin delete question then rank rollback error %s", err.Error()) // } qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: questionInfo.UserID, TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionDeleted, }) msg.NotificationAction = constant.NotificationYourQuestionWasDeleted } if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusClosed { qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: questionInfo.UserID, TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionReopened, }) } if setStatus == entity.QuestionStatusClosed && questionInfo.Status != entity.QuestionStatusClosed { qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: questionInfo.UserID, TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionClosed, }) msg.NotificationAction = constant.NotificationYourQuestionIsClosed } // recover if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusDeleted { qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionUndeleted, }) } if len(msg.NotificationAction) > 0 { msg.ObjectID = questionInfo.ID msg.Type = schema.NotificationTypeInbox msg.ReceiverUserID = questionInfo.UserID msg.TriggerUserID = req.UserID msg.ObjectType = constant.QuestionObjectType qs.notificationQueueService.Send(ctx, msg) } return nil } func (qs *QuestionService) AdminQuestionPage( ctx context.Context, req *schema.AdminQuestionPageReq) ( resp *pager.PageModel, err error) { list := make([]*schema.AdminQuestionInfo, 0) questionList, count, err := qs.questionRepo.AdminQuestionPage(ctx, req) if err != nil { return nil, err } userIds := make([]string, 0) for _, info := range questionList { item := &schema.AdminQuestionInfo{} _ = copier.Copy(item, info) item.CreateTime = info.CreatedAt.Unix() item.UpdateTime = info.PostUpdateTime.Unix() item.EditTime = info.UpdatedAt.Unix() list = append(list, item) userIds = append(userIds, info.UserID) } userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) if err != nil { return nil, err } for _, item := range list { if u, ok := userInfoMap[item.UserID]; ok { item.UserInfo = u } } return pager.NewPageModel(count, list), nil } // AdminAnswerPage search answer list func (qs *QuestionService) AdminAnswerPage(ctx context.Context, req *schema.AdminAnswerPageReq) ( resp *pager.PageModel, err error) { answerList, count, err := qs.questioncommon.AnswerCommon.AdminSearchList(ctx, req) if err != nil { return nil, err } questionIDs := make([]string, 0) userIds := make([]string, 0) answerResp := make([]*schema.AdminAnswerInfo, 0) for _, item := range answerList { answerInfo := qs.questioncommon.AnswerCommon.AdminShowFormat(ctx, item) answerResp = append(answerResp, answerInfo) questionIDs = append(questionIDs, item.QuestionID) userIds = append(userIds, item.UserID) } userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) if err != nil { return nil, err } questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID) if err != nil { return nil, err } for _, item := range answerResp { if q, ok := questionMaps[item.QuestionID]; ok { item.QuestionInfo.Title = q.Title } if u, ok := userInfoMap[item.UserID]; ok { item.UserInfo = u } } return pager.NewPageModel(count, answerResp), nil } func (qs *QuestionService) changeQuestionToRevision(_ context.Context, questionInfo *entity.Question, tags []*entity.Tag) ( questionRevision *entity.QuestionWithTagsRevision) { questionRevision = &entity.QuestionWithTagsRevision{} questionRevision.Question = *questionInfo for _, tag := range tags { item := &entity.TagSimpleInfoForRevision{} _ = copier.Copy(item, tag) questionRevision.Tags = append(questionRevision.Tags, item) } return questionRevision } func (qs *QuestionService) SitemapCron(ctx context.Context) { siteSeo, err := qs.siteInfoService.GetSiteSeo(ctx) if err != nil { log.Error(err) return } ctx = context.WithValue(ctx, constant.ShortIDContextKey, siteSeo.IsShortLink()) qs.questioncommon.SitemapCron(ctx) } func (qs *QuestionService) GetQuestionLink(ctx context.Context, req *schema.GetQuestionLinkReq) ( questions []*schema.QuestionPageResp, total int64, err error) { if req.OrderCond == schema.QuestionOrderCondHot { req.InDays = schema.HotInDays } questionList, total, err := qs.questionRepo.GetQuestionLink(ctx, req.Page, req.PageSize, req.QuestionID, req.OrderCond, req.InDays) if err != nil { return nil, 0, err } questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, req.OrderCond) if err != nil { return nil, 0, err } return questions, total, nil } ================================================ FILE: internal/service/content/revision_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package content import ( "context" "encoding/json" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/activityqueue" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/object_info" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/report_common" "github.com/apache/answer/internal/service/review" "github.com/apache/answer/internal/service/revision" "github.com/apache/answer/internal/service/tag_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/obj" "github.com/apache/answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // RevisionService user service type RevisionService struct { revisionRepo revision.RevisionRepo userCommon *usercommon.UserCommon questionCommon *questioncommon.QuestionCommon answerService *AnswerService objectInfoService *object_info.ObjService questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo tagRepo tag_common.TagRepo tagCommon *tag_common.TagCommonService notificationQueueService noticequeue.Service activityQueueService activityqueue.Service reportRepo report_common.ReportRepo reviewService *review.ReviewService reviewActivity activity.ReviewActivityRepo } func NewRevisionService( revisionRepo revision.RevisionRepo, userCommon *usercommon.UserCommon, questionCommon *questioncommon.QuestionCommon, answerService *AnswerService, objectInfoService *object_info.ObjService, questionRepo questioncommon.QuestionRepo, answerRepo answercommon.AnswerRepo, tagRepo tag_common.TagRepo, tagCommon *tag_common.TagCommonService, notificationQueueService noticequeue.Service, activityQueueService activityqueue.Service, reportRepo report_common.ReportRepo, reviewService *review.ReviewService, reviewActivity activity.ReviewActivityRepo, ) *RevisionService { return &RevisionService{ revisionRepo: revisionRepo, userCommon: userCommon, questionCommon: questionCommon, answerService: answerService, objectInfoService: objectInfoService, questionRepo: questionRepo, answerRepo: answerRepo, tagRepo: tagRepo, tagCommon: tagCommon, notificationQueueService: notificationQueueService, activityQueueService: activityQueueService, reportRepo: reportRepo, reviewService: reviewService, reviewActivity: reviewActivity, } } func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.RevisionAuditReq) (err error) { revisioninfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, req.ID) if err != nil { return } if !exist { return } if revisioninfo.Status != entity.RevisionUnreviewedStatus { return } if req.Operation == schema.RevisionAuditReject { err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewRejectStatus, req.UserID) return } if req.Operation == schema.RevisionAuditApprove { objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(revisioninfo.ObjectID) if objectTypeerr != nil { return objectTypeerr } revisionitem := &schema.GetRevisionResp{} _ = copier.Copy(revisionitem, revisioninfo) rs.parseItem(ctx, revisionitem) var saveErr error switch objectType { case constant.QuestionObjectType: if !req.CanReviewQuestion { saveErr = errors.BadRequest(reason.RevisionNoPermission) } else { saveErr = rs.revisionAuditQuestion(ctx, revisionitem) } case constant.AnswerObjectType: if !req.CanReviewAnswer { saveErr = errors.BadRequest(reason.RevisionNoPermission) } else { saveErr = rs.revisionAuditAnswer(ctx, revisionitem) } case constant.TagObjectType: if !req.CanReviewTag { saveErr = errors.BadRequest(reason.RevisionNoPermission) } else { saveErr = rs.revisionAuditTag(ctx, revisionitem) } } if saveErr != nil { return saveErr } err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewPassStatus, req.UserID) if err != nil { return err } err = rs.reviewActivity.Review(ctx, &schema.PassReviewActivity{ UserID: revisioninfo.UserID, TriggerUserID: req.UserID, ObjectID: revisioninfo.ObjectID, OriginalObjectID: "0", RevisionID: revisioninfo.ID, }) if err != nil { log.Errorf("add review activity failed: %v", err) } msg := &schema.NotificationMsg{ TriggerUserID: req.UserID, ReceiverUserID: revisioninfo.UserID, Type: schema.NotificationTypeAchievement, ObjectID: revisioninfo.ObjectID, ObjectType: objectType, } rs.notificationQueueService.Send(ctx, msg) return } return nil } func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { questioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfoResp) if ok { var PostUpdateTime time.Time dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, questioninfo.ID) if dberr != nil || !exist { return } PostUpdateTime = time.Unix(questioninfo.UpdateTime, 0) if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() { PostUpdateTime = dbquestion.PostUpdateTime } question := &entity.Question{} question.ID = questioninfo.ID question.Title = questioninfo.Title question.OriginalText = questioninfo.Content question.ParsedText = questioninfo.HTML question.UpdatedAt = time.Unix(questioninfo.UpdateTime, 0) question.PostUpdateTime = PostUpdateTime question.LastEditUserID = revisionitem.UserID saveerr := rs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"}) if saveerr != nil { return saveerr } objectTagTags := make([]*schema.TagItem, 0) for _, tag := range questioninfo.Tags { item := &schema.TagItem{} item.SlugName = tag.SlugName objectTagTags = append(objectTagTags, item) } objectTagData := schema.TagChange{} objectTagData.ObjectID = question.ID objectTagData.Tags = objectTagTags minimumTags, err := rs.tagCommon.GetMinimumTags(ctx) if err != nil { return err } _, saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData, minimumTags) if saveerr != nil { return saveerr } rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: revisionitem.UserID, ObjectID: revisionitem.ObjectID, ActivityTypeKey: constant.ActQuestionEdited, RevisionID: revisionitem.ID, OriginalObjectID: revisionitem.ObjectID, }) } return nil } func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { answerinfo, ok := revisionitem.ContentParsed.(*schema.AnswerInfo) if ok { var PostUpdateTime time.Time dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID) if dberr != nil || !exist { return } PostUpdateTime = time.Unix(answerinfo.UpdateTime, 0) if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() { PostUpdateTime = dbquestion.PostUpdateTime } insertData := new(entity.Answer) insertData.ID = answerinfo.ID insertData.OriginalText = answerinfo.Content insertData.ParsedText = answerinfo.HTML insertData.UpdatedAt = time.Unix(answerinfo.UpdateTime, 0) insertData.LastEditUserID = revisionitem.UserID saveerr := rs.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}) if saveerr != nil { return saveerr } saveerr = rs.questionCommon.UpdatePostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime) if saveerr != nil { return saveerr } questionInfo, exist, err := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID) if err != nil { return err } if !exist { return errors.BadRequest(reason.QuestionNotFound) } msg := &schema.NotificationMsg{ TriggerUserID: revisionitem.UserID, ReceiverUserID: questionInfo.UserID, Type: schema.NotificationTypeInbox, ObjectID: answerinfo.ID, } msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationUpdateAnswer rs.notificationQueueService.Send(ctx, msg) rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: revisionitem.UserID, ObjectID: insertData.ID, OriginalObjectID: insertData.ID, ActivityTypeKey: constant.ActAnswerEdited, RevisionID: revisionitem.ID, }) } return nil } func (rs *RevisionService) revisionAuditTag(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { taginfo, ok := revisionitem.ContentParsed.(*schema.GetTagResp) if ok { tag := &entity.Tag{} tag.ID = taginfo.TagID tag.OriginalText = taginfo.OriginalText tag.ParsedText = taginfo.ParsedText saveerr := rs.tagRepo.UpdateTag(ctx, tag) if saveerr != nil { return saveerr } tagInfo, exist, err := rs.tagCommon.GetTagByID(ctx, taginfo.TagID) if err != nil { return err } if !exist { return errors.BadRequest(reason.TagNotFound) } if tagInfo.MainTagID == 0 && len(tagInfo.SlugName) > 0 { log.Debugf("tag %s update slug_name", tagInfo.SlugName) tagList, err := rs.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)}) if err != nil { return err } updateTagSlugNames := make([]string, 0) for _, tag := range tagList { updateTagSlugNames = append(updateTagSlugNames, tag.SlugName) } err = rs.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName) if err != nil { return err } } rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: revisionitem.UserID, ObjectID: taginfo.TagID, OriginalObjectID: taginfo.TagID, ActivityTypeKey: constant.ActTagEdited, RevisionID: revisionitem.ID, }) } return nil } // GetUnreviewedRevisionPage get unreviewed list func (rs *RevisionService) GetUnreviewedRevisionPage(ctx context.Context, req *schema.RevisionSearch) ( resp *pager.PageModel, err error) { revisionResp := make([]*schema.GetUnreviewedRevisionResp, 0) if len(req.GetCanReviewObjectTypes()) == 0 { return pager.NewPageModel(0, revisionResp), nil } revisionPage, total, err := rs.revisionRepo.GetUnreviewedRevisionPage( ctx, req.Page, 1, req.GetCanReviewObjectTypes()) if err != nil { return nil, err } for _, rev := range revisionPage { item := &schema.GetUnreviewedRevisionResp{} _, ok := constant.ObjectTypeNumberMapping[rev.ObjectType] if !ok { continue } item.Type = constant.ObjectTypeNumberMapping[rev.ObjectType] info, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, rev.ObjectID) if err != nil { return nil, err } item.Info = info revisionitem := &schema.GetRevisionResp{} _ = copier.Copy(revisionitem, rev) rs.parseItem(ctx, revisionitem) item.UnreviewedInfo = revisionitem // get user info userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, revisionitem.UserID) if e != nil { return nil, e } if exists { var uinfo schema.UserBasicInfo _ = copier.Copy(&uinfo, userInfo) item.UnreviewedInfo.UserInfo = uinfo } item.Info.UrlTitle = htmltext.UrlTitle(item.Info.Title) item.UnreviewedInfo.UrlTitle = htmltext.UrlTitle(item.UnreviewedInfo.Title) revisionResp = append(revisionResp, item) } return pager.NewPageModel(total, revisionResp), nil } // GetRevisionList get revision list all func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetRevisionListReq) (resp []schema.GetRevisionResp, err error) { var ( rev entity.Revision revs []entity.Revision ) resp = []schema.GetRevisionResp{} objInfo, infoErr := rs.objectInfoService.GetInfo(ctx, req.ObjectID) if infoErr != nil { return nil, infoErr } if !req.IsAdmin && objInfo.IsDeleted() && objInfo.ObjectCreatorUserID != req.UserID { switch objInfo.ObjectType { case constant.QuestionObjectType: return nil, errors.NotFound(reason.QuestionNotFound) case constant.AnswerObjectType: return nil, errors.NotFound(reason.AnswerNotFound) case constant.TagObjectType: return nil, errors.NotFound(reason.TagNotFound) default: return nil, errors.NotFound(reason.ObjectNotFound) } } _ = copier.Copy(&rev, req) revs, err = rs.revisionRepo.GetRevisionList(ctx, &rev) if err != nil { return } for _, r := range revs { var ( uinfo schema.UserBasicInfo item schema.GetRevisionResp ) _ = copier.Copy(&item, r) rs.parseItem(ctx, &item) // get user info userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, item.UserID) if e != nil { return nil, e } if exists { err = copier.Copy(&uinfo, userInfo) item.UserInfo = uinfo } resp = append(resp, item) } return } func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisionResp) { var ( err error question entity.QuestionWithTagsRevision questionInfo *schema.QuestionInfoResp answer entity.Answer answerInfo *schema.AnswerInfo tag entity.Tag tagInfo *schema.GetTagResp ) shortID := handler.GetEnableShortID(ctx) if shortID { item.ObjectID = uid.EnShortID(item.ObjectID) } switch item.ObjectType { case constant.ObjectTypeStrMapping["question"]: err = json.Unmarshal([]byte(item.Content), &question) if err != nil { break } questionInfo = rs.questionCommon.ShowFormatWithTag(ctx, &question) if shortID { questionInfo.ID = uid.EnShortID(questionInfo.ID) } item.ContentParsed = questionInfo case constant.ObjectTypeStrMapping["answer"]: err = json.Unmarshal([]byte(item.Content), &answer) if err != nil { break } answerInfo = rs.answerService.ShowFormat(ctx, &answer) if shortID { answerInfo.ID = uid.EnShortID(answerInfo.ID) answerInfo.QuestionID = uid.EnShortID(answerInfo.QuestionID) } item.ContentParsed = answerInfo case constant.ObjectTypeStrMapping["tag"]: err = json.Unmarshal([]byte(item.Content), &tag) if err != nil { break } tagInfo = &schema.GetTagResp{ TagID: tag.ID, CreatedAt: tag.CreatedAt.Unix(), UpdatedAt: tag.UpdatedAt.Unix(), SlugName: tag.SlugName, DisplayName: tag.DisplayName, OriginalText: tag.OriginalText, ParsedText: tag.ParsedText, FollowCount: tag.FollowCount, QuestionCount: tag.QuestionCount, Recommend: tag.Recommend, Reserved: tag.Reserved, } tagInfo.GetExcerpt() item.ContentParsed = tagInfo } if err != nil { item.ContentParsed = item.Content } item.CreatedAtParsed = item.CreatedAt.Unix() } // CheckCanUpdateRevision can check revision func (rs *RevisionService) CheckCanUpdateRevision(ctx context.Context, req *schema.CheckCanQuestionUpdate) ( resp *schema.ErrTypeData, err error) { _, exist, err := rs.revisionRepo.ExistUnreviewedByObjectID(ctx, req.ID) if err != nil { return nil, nil } if exist { return &schema.ErrTypeToast, errors.BadRequest(reason.RevisionReviewUnderway) } return nil, nil } // GetReviewingType get reviewing type func (rs *RevisionService) GetReviewingType(ctx context.Context, req *schema.GetReviewingTypeReq) (resp []*schema.GetReviewingTypeResp, err error) { resp = make([]*schema.GetReviewingTypeResp, 0) // get queue amount if req.IsAdmin { reviewCount, err := rs.reviewService.GetReviewPendingCount(ctx) if err != nil { log.Errorf("get report count failed: %v", err) } else { resp = append(resp, &schema.GetReviewingTypeResp{ Name: string(constant.QueuedPost), Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewQueuedPostLabel), TodoAmount: reviewCount, }) } } // get flag amount if req.IsAdmin { reportCount, err := rs.reportRepo.GetReportCount(ctx) if err != nil { log.Errorf("get report count failed: %v", err) } else { resp = append(resp, &schema.GetReviewingTypeResp{ Name: string(constant.FlaggedPost), Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewFlaggedPostLabel), TodoAmount: reportCount, }) } } // get suggestion amount countUnreviewedRevision, err := rs.revisionRepo.CountUnreviewedRevision(ctx, req.GetCanReviewObjectTypes()) if err != nil { log.Errorf("get unreviewed revision count failed: %v", err) } else { resp = append(resp, &schema.GetReviewingTypeResp{ Name: string(constant.SuggestedPostEdit), Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewSuggestedPostEditLabel), TodoAmount: countUnreviewedRevision, }) } return resp, nil } ================================================ FILE: internal/service/content/search_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package content import ( "context" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/search_common" "github.com/apache/answer/internal/service/search_parser" "github.com/apache/answer/plugin" ) type SearchService struct { searchParser *search_parser.SearchParser searchRepo search_common.SearchRepo } func NewSearchService( searchParser *search_parser.SearchParser, searchRepo search_common.SearchRepo, ) *SearchService { return &SearchService{ searchParser: searchParser, searchRepo: searchRepo, } } // Search search contents func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp *schema.SearchResp, err error) { if dto.Page < 1 { dto.Page = 1 } if len(dto.Query) == 0 { return &schema.SearchResp{ Total: 0, SearchResults: make([]*schema.SearchResult, 0), }, nil } // search type cond := ss.searchParser.ParseStructure(ctx, dto) // check search plugin var finder plugin.Search _ = plugin.CallSearch(func(search plugin.Search) error { finder = search return nil }) resp = &schema.SearchResp{} // search plugin is not found, call system search if finder == nil { switch { case cond.SearchAll(): resp.SearchResults, resp.Total, err = ss.searchRepo.SearchContents(ctx, cond.Words, cond.Tags, cond.UserID, cond.VoteAmount, dto.Page, dto.Size, dto.Order) case cond.SearchQuestion(): resp.SearchResults, resp.Total, err = ss.searchRepo.SearchQuestions(ctx, cond.Words, cond.Tags, cond.NotAccepted, cond.Views, cond.AnswerAmount, dto.Page, dto.Size, dto.Order) case cond.SearchAnswer(): resp.SearchResults, resp.Total, err = ss.searchRepo.SearchAnswers(ctx, cond.Words, cond.Tags, cond.Accepted, cond.QuestionID, dto.Page, dto.Size, dto.Order) } return } return ss.searchByPlugin(ctx, finder, cond, dto) } func (ss *SearchService) searchByPlugin(ctx context.Context, finder plugin.Search, cond *schema.SearchCondition, dto *schema.SearchDTO) (resp *schema.SearchResp, err error) { var res []plugin.SearchResult resp = &schema.SearchResp{} switch { case cond.SearchAll(): res, resp.Total, err = finder.SearchContents(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order)) case cond.SearchQuestion(): res, resp.Total, err = finder.SearchQuestions(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order)) case cond.SearchAnswer(): res, resp.Total, err = finder.SearchAnswers(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order)) } if err != nil { return resp, err } resp.SearchResults, err = ss.searchRepo.ParseSearchPluginResult(ctx, res, cond.Words) return resp, err } ================================================ FILE: internal/service/content/user_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package content import ( "context" "encoding/json" "fmt" "time" "github.com/apache/answer/internal/service/eventqueue" "github.com/apache/answer/pkg/token" "github.com/apache/answer/internal/base/constant" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/user_notification_config" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/file_record" "github.com/apache/answer/internal/service/role" "github.com/apache/answer/internal/service/siteinfo_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/internal/service/user_external_login" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "golang.org/x/crypto/bcrypt" ) // UserService user service type UserService struct { userCommonService *usercommon.UserCommon userRepo usercommon.UserRepo userActivity activity.UserActiveActivityRepo activityRepo activity_common.ActivityRepo emailService *export.EmailService authService *auth.AuthService siteInfoService siteinfo_common.SiteInfoCommonService userRoleService *role.UserRoleRelService userExternalLoginService *user_external_login.UserExternalLoginService userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo userNotificationConfigService *user_notification_config.UserNotificationConfigService questionService *questioncommon.QuestionCommon eventQueueService eventqueue.Service fileRecordService *file_record.FileRecordService } func NewUserService(userRepo usercommon.UserRepo, userActivity activity.UserActiveActivityRepo, activityRepo activity_common.ActivityRepo, emailService *export.EmailService, authService *auth.AuthService, siteInfoService siteinfo_common.SiteInfoCommonService, userRoleService *role.UserRoleRelService, userCommonService *usercommon.UserCommon, userExternalLoginService *user_external_login.UserExternalLoginService, userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo, userNotificationConfigService *user_notification_config.UserNotificationConfigService, questionService *questioncommon.QuestionCommon, eventQueueService eventqueue.Service, fileRecordService *file_record.FileRecordService, ) *UserService { return &UserService{ userCommonService: userCommonService, userRepo: userRepo, userActivity: userActivity, activityRepo: activityRepo, emailService: emailService, authService: authService, siteInfoService: siteInfoService, userRoleService: userRoleService, userExternalLoginService: userExternalLoginService, userNotificationConfigRepo: userNotificationConfigRepo, userNotificationConfigService: userNotificationConfigService, questionService: questionService, eventQueueService: eventQueueService, fileRecordService: fileRecordService, } } // GetUserInfoByUserID get user info by user id func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) ( resp *schema.GetCurrentLoginUserInfoResp, err error) { userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } if userInfo.Status == entity.UserStatusDeleted { return nil, errors.Unauthorized(reason.UnauthorizedError) } resp = &schema.GetCurrentLoginUserInfoResp{} resp.ConvertFromUserEntity(userInfo) resp.RoleID, err = us.userRoleService.GetUserRole(ctx, userInfo.ID) if err != nil { log.Error(err) } resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status) resp.AccessToken = token resp.HavePassword = len(userInfo.Pass) > 0 return resp, nil } func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) ( resp *schema.GetOtherUserInfoByUsernameResp, err error) { userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username) if err != nil { return nil, err } if !exist { return nil, errors.NotFound(reason.UserNotFound) } resp = &schema.GetOtherUserInfoByUsernameResp{} resp.ConvertFromUserEntityWithLang(ctx, userInfo) resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() // Only the user himself and the administrator can see the hidden questions questionCount, err := us.questionService.GetPersonalUserQuestionCount(ctx, req.UserID, userInfo.ID, req.IsAdmin) if err != nil { return nil, err } resp.QuestionCount = int(questionCount) return resp, nil } // EmailLogin email login func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLoginReq) (resp *schema.UserLoginResp, err error) { siteLogin, err := us.siteInfoService.GetSiteLogin(ctx) if err != nil { return nil, err } if !siteLogin.AllowPasswordLogin { return nil, errors.BadRequest(reason.NotAllowedLoginViaPassword) } userInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email) if err != nil { return nil, err } if !exist || userInfo.Status == entity.UserStatusDeleted { return nil, errors.BadRequest(reason.EmailOrPasswordWrong) } if !us.verifyPassword(ctx, req.Pass, userInfo.Pass) { return nil, errors.BadRequest(reason.EmailOrPasswordWrong) } ok, externalID, err := us.userExternalLoginService.CheckUserStatusInUserCenter(ctx, userInfo.ID) if err != nil { return nil, err } if !ok { return nil, errors.BadRequest(reason.EmailOrPasswordWrong) } err = us.userRepo.UpdateLastLoginDate(ctx, userInfo.ID) if err != nil { log.Errorf("update last login data failed, err: %v", err) } roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID) if err != nil { log.Error(err) } resp = &schema.UserLoginResp{} resp.ConvertFromUserEntity(userInfo) resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() userCacheInfo := &entity.UserCacheInfo{ UserID: userInfo.ID, EmailStatus: userInfo.MailStatus, UserStatus: userInfo.Status, RoleID: roleID, ExternalID: externalID, } resp.AccessToken, resp.VisitToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) if err != nil { return nil, err } resp.RoleID = userCacheInfo.RoleID if resp.RoleID == role.RoleAdminID { err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, userCacheInfo) if err != nil { return nil, err } } return resp, nil } // RetrievePassWord . func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRetrievePassWordRequest) error { userInfo, has, err := us.userRepo.GetByEmail(ctx, req.Email) if err != nil { return err } if !has { return nil } // send email data := &schema.EmailCodeContent{ Email: req.Email, UserID: userInfo.ID, } code := token.GenerateToken() verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.getSiteUrl(ctx), code) title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL) if err != nil { return err } go us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString()) return nil } // UpdatePasswordWhenForgot update user password when user forgot password func (us *UserService) UpdatePasswordWhenForgot(ctx context.Context, req *schema.UserRePassWordRequest) (err error) { data := &schema.EmailCodeContent{} err = data.FromJSONString(req.Content) if err != nil { return errors.BadRequest(reason.EmailVerifyURLExpired) } userInfo, exist, err := us.userRepo.GetByEmail(ctx, data.Email) if err != nil { return err } if !exist { return errors.BadRequest(reason.UserNotFound) } enpass, err := us.encryptPassword(ctx, req.Pass) if err != nil { return err } err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass) if err != nil { return err } // When the user changes the password, all the current user's tokens are invalid. us.authService.RemoveUserAllTokens(ctx, userInfo.ID) return nil } func (us *UserService) UserModifyPassWordVerification(ctx context.Context, req *schema.UserModifyPasswordReq) (bool, error) { userInfo, has, err := us.userRepo.GetByUserID(ctx, req.UserID) if err != nil { return false, err } if !has { return false, errors.BadRequest(reason.UserNotFound) } isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass) if !isPass { return false, nil } return true, nil } // UserModifyPassword user modify password func (us *UserService) UserModifyPassword(ctx context.Context, req *schema.UserModifyPasswordReq) error { enpass, err := us.encryptPassword(ctx, req.Pass) if err != nil { return err } userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) if err != nil { return err } if !exist { return errors.BadRequest(reason.UserNotFound) } isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass) if !isPass { return errors.BadRequest(reason.OldPasswordVerificationFailed) } err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass) if err != nil { return err } us.authService.RemoveTokensExceptCurrentUser(ctx, userInfo.ID, req.AccessToken) return nil } // UpdateInfo update user info func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) ( errFields []*validator.FormErrorField, err error) { if len(req.Username) > 0 { if checker.IsInvalidUsername(req.Username) { return append(errFields, &validator.FormErrorField{ ErrorField: "username", ErrorMsg: reason.UsernameInvalid, }), errors.BadRequest(reason.UsernameInvalid) } // admin can use reserved username if !req.IsAdmin && checker.IsReservedUsername(req.Username) { return append(errFields, &validator.FormErrorField{ ErrorField: "username", ErrorMsg: reason.UsernameInvalid, }), errors.BadRequest(reason.UsernameInvalid) } else if req.IsAdmin && checker.IsUsersIgnorePath(req.Username) { return append(errFields, &validator.FormErrorField{ ErrorField: "username", ErrorMsg: reason.UsernameInvalid, }), errors.BadRequest(reason.UsernameInvalid) } userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username) if err != nil { return nil, err } if exist && userInfo.ID != req.UserID { return append(errFields, &validator.FormErrorField{ ErrorField: "username", ErrorMsg: reason.UsernameDuplicate, }), errors.BadRequest(reason.UsernameDuplicate) } } oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } cond := us.formatUserInfoForUpdateInfo(oldUserInfo, req) us.cleanUpRemovedAvatar(ctx, oldUserInfo.Avatar, cond.Avatar) err = us.userRepo.UpdateInfo(ctx, cond) if err != nil { return nil, err } us.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserUpdate, req.UserID)) return nil, err } func (us *UserService) cleanUpRemovedAvatar( ctx context.Context, oldAvatarJSON string, newAvatarJSON string, ) { if oldAvatarJSON == newAvatarJSON { return } var oldAvatar, newAvatar schema.AvatarInfo _ = json.Unmarshal([]byte(oldAvatarJSON), &oldAvatar) _ = json.Unmarshal([]byte(newAvatarJSON), &newAvatar) if len(oldAvatar.Custom) == 0 { return } // clean up if old is custom and it's either removed or replaced if oldAvatar.Custom != newAvatar.Custom { fileRecord, err := us.fileRecordService.GetFileRecordByURL(ctx, oldAvatar.Custom) if err != nil { log.Error(err) return } if fileRecord == nil { log.Warn("no file record found for old avatar url:", oldAvatar.Custom) return } if err := us.fileRecordService.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil { log.Error(err) } } } func (us *UserService) formatUserInfoForUpdateInfo( oldUserInfo *entity.User, req *schema.UpdateInfoRequest) *entity.User { avatar, _ := json.Marshal(req.Avatar) userInfo := &entity.User{} userInfo.DisplayName = oldUserInfo.DisplayName userInfo.Username = oldUserInfo.Username userInfo.Avatar = oldUserInfo.Avatar userInfo.Bio = oldUserInfo.Bio userInfo.BioHTML = oldUserInfo.BioHTML userInfo.Website = oldUserInfo.Website userInfo.Location = oldUserInfo.Location userInfo.ID = req.UserID if len(req.DisplayName) > 0 { userInfo.DisplayName = req.DisplayName } if len(req.Username) > 0 { userInfo.Username = req.Username } if len(avatar) > 0 { userInfo.Avatar = string(avatar) } userInfo.Bio = req.Bio userInfo.BioHTML = req.BioHTML userInfo.Website = req.Website userInfo.Location = req.Location return userInfo } // UserUpdateInterface update user interface func (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.UpdateUserInterfaceRequest) (err error) { return us.userRepo.UpdateUserInterface(ctx, req.UserId, req.Language, req.ColorScheme) } // UserRegisterByEmail user register func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) ( resp *schema.UserLoginResp, errFields []*validator.FormErrorField, err error, ) { _, has, err := us.userRepo.GetByEmail(ctx, registerUserInfo.Email) if err != nil { return nil, nil, err } if has { errFields = append(errFields, &validator.FormErrorField{ ErrorField: "e_mail", ErrorMsg: reason.EmailDuplicate, }) return nil, errFields, errors.BadRequest(reason.EmailDuplicate) } userInfo := &entity.User{} userInfo.EMail = registerUserInfo.Email userInfo.DisplayName = registerUserInfo.Name userInfo.Pass, err = us.encryptPassword(ctx, registerUserInfo.Pass) if err != nil { return nil, nil, err } userInfo.Username, err = us.userCommonService.MakeUsername(ctx, registerUserInfo.Name) if err != nil { errFields = append(errFields, &validator.FormErrorField{ ErrorField: "name", ErrorMsg: reason.UsernameInvalid, }) return nil, errFields, err } userInfo.IPInfo = registerUserInfo.IP userInfo.MailStatus = entity.EmailStatusToBeVerified userInfo.Status = entity.UserStatusAvailable userInfo.LastLoginDate = time.Now() err = us.userRepo.AddUser(ctx, userInfo) if err != nil { return nil, nil, err } if err := us.userNotificationConfigService.SetDefaultUserNotificationConfig(ctx, []string{userInfo.ID}); err != nil { log.Errorf("set default user notification config failed, err: %v", err) } // send email data := &schema.EmailCodeContent{ Email: registerUserInfo.Email, UserID: userInfo.ID, } code := token.GenerateToken() verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return nil, nil, err } go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID) if err != nil { log.Error(err) } // return user info and token resp = &schema.UserLoginResp{} resp.ConvertFromUserEntity(userInfo) resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() userCacheInfo := &entity.UserCacheInfo{ UserID: userInfo.ID, EmailStatus: userInfo.MailStatus, UserStatus: userInfo.Status, RoleID: roleID, } resp.AccessToken, resp.VisitToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) if err != nil { return nil, nil, err } resp.RoleID = userCacheInfo.RoleID if resp.RoleID == role.RoleAdminID { err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID}) if err != nil { return nil, nil, err } } return resp, nil, nil } func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) error { userInfo, has, err := us.userRepo.GetByUserID(ctx, userID) if err != nil { return err } if !has { return errors.BadRequest(reason.UserNotFound) } data := &schema.EmailCodeContent{ Email: userInfo.EMail, UserID: userInfo.ID, } code := token.GenerateToken() verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return err } go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) return nil } func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVerifyEmailReq) (resp *schema.UserLoginResp, err error) { data := &schema.EmailCodeContent{} err = data.FromJSONString(req.Content) if err != nil { return nil, errors.BadRequest(reason.EmailVerifyURLExpired) } userInfo, has, err := us.userRepo.GetByEmail(ctx, data.Email) if err != nil { return nil, err } if !has { return nil, errors.BadRequest(reason.UserNotFound) } if userInfo.MailStatus == entity.EmailStatusToBeVerified { userInfo.MailStatus = entity.EmailStatusAvailable err = us.userRepo.UpdateEmailStatus(ctx, userInfo.ID, userInfo.MailStatus) if err != nil { return nil, err } } if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil { log.Error(err) return nil, err } // In the case of three-party login, the associated users are bound if len(data.BindingKey) > 0 { err = us.userExternalLoginService.ExternalLoginBindingUser(ctx, data.BindingKey, userInfo) if err != nil { return nil, err } } accessToken, userCacheInfo, err := us.userCommonService.CacheLoginUserInfo( ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, "") if err != nil { return nil, err } resp = &schema.UserLoginResp{} resp.ConvertFromUserEntity(userInfo) resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() resp.AccessToken = accessToken // User verified email will update user email status. So user status cache should be updated. if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil { return nil, err } return resp, nil } // verifyPassword // Compare whether the password is correct func (us *UserService) verifyPassword(_ context.Context, loginPass, userPass string) bool { if len(loginPass) == 0 && len(userPass) == 0 { return true } err := bcrypt.CompareHashAndPassword([]byte(userPass), []byte(loginPass)) return err == nil } // encryptPassword // The password does irreversible encryption. func (us *UserService) encryptPassword(_ context.Context, pass string) (string, error) { hashPwd, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) // This encrypted string can be saved to the database and can be used as password matching verification return string(hashPwd), err } // UserChangeEmailSendCode user change email verification func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) ( resp []*validator.FormErrorField, err error) { userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } // If user's email already verified, then must verify password first. if userInfo.MailStatus == entity.EmailStatusAvailable && !us.verifyPassword(ctx, req.Pass, userInfo.Pass) { resp = append(resp, &validator.FormErrorField{ ErrorField: "pass", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.OldPasswordVerificationFailed), }) return resp, errors.BadRequest(reason.OldPasswordVerificationFailed) } _, exist, err = us.userRepo.GetByEmail(ctx, req.Email) if err != nil { return nil, err } if exist { resp = append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "e_mail", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailDuplicate), }) return resp, errors.BadRequest(reason.EmailDuplicate) } data := &schema.EmailCodeContent{ Email: req.Email, UserID: req.UserID, } code := token.GenerateToken() var title, body string verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.getSiteUrl(ctx), code) if userInfo.MailStatus == entity.EmailStatusToBeVerified { title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL) } else { title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL) } if err != nil { return nil, err } log.Infof("send email confirmation %s", verifyEmailURL) go us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString()) return nil, nil } // UserChangeEmailVerify user change email verify code func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string) (resp *schema.UserLoginResp, err error) { data := &schema.EmailCodeContent{} err = data.FromJSONString(content) if err != nil { return nil, errors.BadRequest(reason.EmailVerifyURLExpired) } _, exist, err := us.userRepo.GetByEmail(ctx, data.Email) if err != nil { return nil, err } if exist { return nil, errors.BadRequest(reason.EmailDuplicate) } userInfo, exist, err := us.userRepo.GetByUserID(ctx, data.UserID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } err = us.userRepo.UpdateEmail(ctx, data.UserID, data.Email) if err != nil { return nil, errors.BadRequest(reason.UserNotFound) } err = us.userRepo.UpdateEmailStatus(ctx, data.UserID, entity.EmailStatusAvailable) if err != nil { return nil, err } // if email status is to be verified, active user as well if userInfo.MailStatus == entity.EmailStatusToBeVerified { if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil { log.Error(err) return nil, err } } roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID) if err != nil { log.Error(err) } resp = &schema.UserLoginResp{} resp.ConvertFromUserEntity(userInfo) resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() userCacheInfo := &entity.UserCacheInfo{ UserID: userInfo.ID, EmailStatus: entity.EmailStatusAvailable, UserStatus: userInfo.Status, RoleID: roleID, } resp.AccessToken, resp.VisitToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) if err != nil { return nil, err } // User verified email will update user email status. So user status cache should be updated. if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil { return nil, err } resp.RoleID = userCacheInfo.RoleID if resp.RoleID == role.RoleAdminID { err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID}) if err != nil { return nil, err } } return resp, nil } // getSiteUrl get site url func (us *UserService) getSiteUrl(ctx context.Context) string { siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general failed: %s", err) return "" } return siteGeneral.SiteUrl } // UserRanking get user ranking func (us *UserService) UserRanking(ctx context.Context) (resp *schema.UserRankingResp, err error) { limit := 20 endTime := time.Now() startTime := endTime.AddDate(0, 0, -7) userIDs, userIDExist := make([]string, 0), make(map[string]bool, 0) // get most reputation users rankStat, rankStatUserIDs, err := us.getActivityUserRankStat(ctx, startTime, endTime, limit, userIDExist) if err != nil { return nil, err } userIDs = append(userIDs, rankStatUserIDs...) // get most vote users voteStat, voteStatUserIDs, err := us.getActivityUserVoteStat(ctx, startTime, endTime, limit, userIDExist) if err != nil { return nil, err } userIDs = append(userIDs, voteStatUserIDs...) // get all staff members userRoleRels, staffUserIDs, err := us.getStaff(ctx, userIDExist) if err != nil { return nil, err } userIDs = append(userIDs, staffUserIDs...) // get user information userInfoMapping, err := us.getUserInfoMapping(ctx, userIDs) if err != nil { return nil, err } return us.warpStatRankingResp(userInfoMapping, rankStat, voteStat, userRoleRels), nil } // GetUserStaff get user staff func (us *UserService) GetUserStaff(ctx context.Context, req *schema.GetUserStaffReq) ( resp []*schema.GetUserStaffResp, err error) { userList, err := us.userRepo.SearchUserListByName(ctx, req.Username, req.PageSize, true) if err != nil { return nil, err } avatarMapping := us.siteInfoService.FormatListAvatar(ctx, userList) for _, u := range userList { resp = append(resp, &schema.GetUserStaffResp{ Username: u.Username, DisplayName: u.DisplayName, Avatar: avatarMapping[u.ID].GetURL(), }) } return resp, nil } // UserUnsubscribeNotification user unsubscribe email notification func (us *UserService) UserUnsubscribeNotification( ctx context.Context, req *schema.UserUnsubscribeNotificationReq) (err error) { data := &schema.EmailCodeContent{} err = data.FromJSONString(req.Content) if err != nil || len(data.UserID) == 0 { return errors.BadRequest(reason.EmailVerifyURLExpired) } for _, source := range data.NotificationSources { notificationConfig, exist, err := us.userNotificationConfigRepo.GetByUserIDAndSource( ctx, data.UserID, source) if err != nil { return err } if !exist { continue } channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels) // unsubscribe email notification for _, channel := range channels { if channel.Key == constant.EmailChannel { channel.Enable = false } } notificationConfig.Channels = channels.ToJsonString() if err = us.userNotificationConfigRepo.Save(ctx, notificationConfig); err != nil { return err } } return nil } func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, endTime time.Time, limit int, userIDExist map[string]bool) (rankStat []*entity.ActivityUserRankStat, userIDs []string, err error) { if plugin.RankAgentEnabled() { return make([]*entity.ActivityUserRankStat, 0), make([]string, 0), nil } rankStat, err = us.activityRepo.GetUsersWhoHasGainedTheMostReputation(ctx, startTime, endTime, limit) if err != nil { return nil, nil, err } for _, stat := range rankStat { if stat.Rank <= 0 { continue } if userIDExist[stat.UserID] { continue } userIDs = append(userIDs, stat.UserID) userIDExist[stat.UserID] = true } return rankStat, userIDs, nil } func (us *UserService) getActivityUserVoteStat(ctx context.Context, startTime, endTime time.Time, limit int, userIDExist map[string]bool) (voteStat []*entity.ActivityUserVoteStat, userIDs []string, err error) { if plugin.RankAgentEnabled() { return make([]*entity.ActivityUserVoteStat, 0), make([]string, 0), nil } voteStat, err = us.activityRepo.GetUsersWhoHasVoteMost(ctx, startTime, endTime, limit) if err != nil { return nil, nil, err } for _, stat := range voteStat { if stat.VoteCount <= 0 { continue } if userIDExist[stat.UserID] { continue } userIDs = append(userIDs, stat.UserID) userIDExist[stat.UserID] = true } return voteStat, userIDs, nil } func (us *UserService) getStaff(ctx context.Context, userIDExist map[string]bool) ( userRoleRels []*entity.UserRoleRel, userIDs []string, err error) { userRoleRels, err = us.userRoleService.GetUserByRoleID(ctx, []int{role.RoleAdminID, role.RoleModeratorID}) if err != nil { return nil, nil, err } for _, rel := range userRoleRels { if userIDExist[rel.UserID] { continue } userIDs = append(userIDs, rel.UserID) userIDExist[rel.UserID] = true } return userRoleRels, userIDs, nil } func (us *UserService) getUserInfoMapping(ctx context.Context, userIDs []string) ( userInfoMapping map[string]*entity.User, err error) { userInfoMapping = make(map[string]*entity.User, 0) if len(userIDs) == 0 { return userInfoMapping, nil } userInfoList, err := us.userRepo.BatchGetByID(ctx, userIDs) if err != nil { return nil, err } avatarMapping := us.siteInfoService.FormatListAvatar(ctx, userInfoList) for _, user := range userInfoList { user.Avatar = avatarMapping[user.ID].GetURL() userInfoMapping[user.ID] = user } return userInfoMapping, nil } func (us *UserService) SearchUserListByName(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) ( resp []*schema.UserBasicInfo, err error) { resp = make([]*schema.UserBasicInfo, 0) if len(req.Username) == 0 { return resp, nil } userList, err := us.userRepo.SearchUserListByName(ctx, req.Username, 5, false) if err != nil { return resp, err } avatarMapping := us.siteInfoService.FormatListAvatar(ctx, userList) for _, u := range userList { if req.UserID == u.ID { continue } basicInfo := us.userCommonService.FormatUserBasicInfo(ctx, u) basicInfo.Avatar = avatarMapping[u.ID].GetURL() resp = append(resp, basicInfo) } return resp, nil } func (us *UserService) warpStatRankingResp( userInfoMapping map[string]*entity.User, rankStat []*entity.ActivityUserRankStat, voteStat []*entity.ActivityUserVoteStat, userRoleRels []*entity.UserRoleRel) (resp *schema.UserRankingResp) { resp = &schema.UserRankingResp{ UsersWithTheMostReputation: make([]*schema.UserRankingSimpleInfo, 0), UsersWithTheMostVote: make([]*schema.UserRankingSimpleInfo, 0), Staffs: make([]*schema.UserRankingSimpleInfo, 0), } for _, stat := range rankStat { if stat.Rank <= 0 { continue } if userInfo := userInfoMapping[stat.UserID]; userInfo != nil && userInfo.Status != entity.UserStatusDeleted { resp.UsersWithTheMostReputation = append(resp.UsersWithTheMostReputation, &schema.UserRankingSimpleInfo{ Username: userInfo.Username, Rank: stat.Rank, DisplayName: userInfo.DisplayName, Avatar: userInfo.Avatar, }) } } for _, stat := range voteStat { if stat.VoteCount <= 0 { continue } if userInfo := userInfoMapping[stat.UserID]; userInfo != nil && userInfo.Status != entity.UserStatusDeleted { resp.UsersWithTheMostVote = append(resp.UsersWithTheMostVote, &schema.UserRankingSimpleInfo{ Username: userInfo.Username, VoteCount: stat.VoteCount, DisplayName: userInfo.DisplayName, Avatar: userInfo.Avatar, }) } } for _, rel := range userRoleRels { if userInfo := userInfoMapping[rel.UserID]; userInfo != nil && userInfo.Status != entity.UserStatusDeleted { resp.Staffs = append(resp.Staffs, &schema.UserRankingSimpleInfo{ Username: userInfo.Username, Rank: userInfo.Rank, DisplayName: userInfo.DisplayName, Avatar: userInfo.Avatar, }) } } return resp } ================================================ FILE: internal/service/content/vote_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package content import ( "context" "fmt" "strings" "github.com/apache/answer/internal/service/eventqueue" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/activity_type" "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/object_info" "github.com/apache/answer/pkg/htmltext" "github.com/segmentfault/pacman/log" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/schema" answercommon "github.com/apache/answer/internal/service/answer_common" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/segmentfault/pacman/errors" ) // VoteRepo activity repository type VoteRepo interface { Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, err error) ListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) ( voteList []*entity.Activity, total int64, err error) } // VoteService user service type VoteService struct { voteRepo VoteRepo configService *config.ConfigService questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo commentCommonRepo comment_common.CommentCommonRepo objectService *object_info.ObjService eventQueueService eventqueue.Service } func NewVoteService( voteRepo VoteRepo, configService *config.ConfigService, questionRepo questioncommon.QuestionRepo, answerRepo answercommon.AnswerRepo, commentCommonRepo comment_common.CommentCommonRepo, objectService *object_info.ObjService, eventQueueService eventqueue.Service, ) *VoteService { return &VoteService{ voteRepo: voteRepo, configService: configService, questionRepo: questionRepo, answerRepo: answerRepo, commentCommonRepo: commentCommonRepo, objectService: objectService, eventQueueService: eventQueueService, } } // VoteUp vote up func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) { objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID) if err != nil { return nil, err } if objectInfo.IsDeleted() { return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted) } // make object id must be decoded objectInfo.ObjectID = req.ObjectID // check user is voting self or not if objectInfo.ObjectCreatorUserID == req.UserID { return nil, errors.BadRequest(reason.DisallowVoteYourSelf) } voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo) // vote operation if req.IsCancel { err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo) } else { // cancel vote down if exist voteOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo) err = vs.voteRepo.CancelVote(ctx, voteOperationInfo) if err != nil { return nil, err } err = vs.voteRepo.Vote(ctx, voteUpOperationInfo) if err != nil { return nil, err } } if err != nil { return nil, err } resp = &schema.VoteResp{} resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType) if err != nil { log.Error(err) } resp.Votes = resp.UpVotes - resp.DownVotes if !req.IsCancel { resp.VoteStatus = constant.ActVoteUp vs.sendEvent(ctx, req, objectInfo, resp) } return resp, nil } // VoteDown vote down func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) { objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID) if err != nil { return nil, err } if objectInfo.IsDeleted() { return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted) } // make object id must be decoded objectInfo.ObjectID = req.ObjectID // check user is voting self or not if objectInfo.ObjectCreatorUserID == req.UserID { return nil, errors.BadRequest(reason.DisallowVoteYourSelf) } // vote operation voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo) if req.IsCancel { err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo) if err != nil { return nil, err } } else { // cancel vote up if exist err = vs.voteRepo.CancelVote(ctx, vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)) if err != nil { return nil, err } err = vs.voteRepo.Vote(ctx, voteDownOperationInfo) if err != nil { return nil, err } } resp = &schema.VoteResp{} resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType) if err != nil { log.Error(err) } resp.Votes = resp.UpVotes - resp.DownVotes if !req.IsCancel { resp.VoteStatus = constant.ActVoteDown vs.sendEvent(ctx, req, objectInfo, resp) } return resp, nil } // ListUserVotes list user's votes func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (resp *pager.PageModel, err error) { typeKeys := []string{ activity_type.QuestionVoteUp, activity_type.QuestionVoteDown, activity_type.AnswerVoteUp, activity_type.AnswerVoteDown, } activityTypes := make([]int, 0) activityTypeMapping := make(map[int]string, 0) for _, typeKey := range typeKeys { cfg, err := vs.configService.GetConfigByKey(ctx, typeKey) if err != nil { continue } activityTypes = append(activityTypes, cfg.ID) activityTypeMapping[cfg.ID] = typeKey } voteList, total, err := vs.voteRepo.ListUserVotes(ctx, req.UserID, req.Page, req.PageSize, activityTypes) if err != nil { return nil, err } lang := handler.GetLangByCtx(ctx) votes := make([]*schema.GetVoteWithPageResp, 0) for _, voteInfo := range voteList { objInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID) if err != nil { log.Error(err) continue } item := &schema.GetVoteWithPageResp{ CreatedAt: voteInfo.CreatedAt.Unix(), ObjectID: objInfo.ObjectID, QuestionID: objInfo.QuestionID, AnswerID: objInfo.AnswerID, ObjectType: objInfo.ObjectType, Title: objInfo.Title, UrlTitle: htmltext.UrlTitle(objInfo.Title), Content: objInfo.Content, } item.VoteType = translator.Tr(lang, activity_type.ActivityTypeFlagMapping[activityTypeMapping[voteInfo.ActivityType]]) if objInfo.QuestionStatus == entity.QuestionStatusDeleted { item.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey) } votes = append(votes, item) } return pager.NewPageModel(total, votes), err } func (vs *VoteService) createVoteOperationInfo(ctx context.Context, userID string, voteUp bool, objectInfo *schema.SimpleObjectInfo) *schema.VoteOperationInfo { // warp vote operation voteOperationInfo := &schema.VoteOperationInfo{ ObjectID: objectInfo.ObjectID, ObjectType: objectInfo.ObjectType, ObjectCreatorUserID: objectInfo.ObjectCreatorUserID, OperatingUserID: userID, VoteUp: voteUp, VoteDown: !voteUp, } voteOperationInfo.Activities = vs.getActivities(ctx, voteOperationInfo) return voteOperationInfo } func (vs *VoteService) getActivities(ctx context.Context, op *schema.VoteOperationInfo) ( activities []*schema.VoteActivity) { activities = make([]*schema.VoteActivity, 0) var actions []string switch op.ObjectType { case constant.QuestionObjectType: if op.VoteUp { actions = []string{activity_type.QuestionVoteUp, activity_type.QuestionVotedUp} } else { actions = []string{activity_type.QuestionVoteDown, activity_type.QuestionVotedDown} } case constant.AnswerObjectType: if op.VoteUp { actions = []string{activity_type.AnswerVoteUp, activity_type.AnswerVotedUp} } else { actions = []string{activity_type.AnswerVoteDown, activity_type.AnswerVotedDown} } case constant.CommentObjectType: actions = []string{activity_type.CommentVoteUp} } for _, action := range actions { t := &schema.VoteActivity{} cfg, err := vs.configService.GetConfigByKey(ctx, action) if err != nil { log.Warnf("get config by key error: %v", err) continue } t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue() if strings.Contains(action, "voted") { t.ActivityUserID = op.ObjectCreatorUserID t.TriggerUserID = op.OperatingUserID } else { t.ActivityUserID = op.OperatingUserID t.TriggerUserID = "0" } activities = append(activities, t) } return activities } func (vs *VoteService) sendEvent(ctx context.Context, req *schema.VoteReq, objectInfo *schema.SimpleObjectInfo, resp *schema.VoteResp) { var event *schema.EventMsg switch objectInfo.ObjectType { case constant.QuestionObjectType: event = schema.NewEvent(constant.EventQuestionVote, req.UserID).TID(objectInfo.QuestionID). QID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID) case constant.AnswerObjectType: event = schema.NewEvent(constant.EventAnswerVote, req.UserID).TID(objectInfo.AnswerID). AID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID) case constant.CommentObjectType: event = schema.NewEvent(constant.EventCommentVote, req.UserID).TID(objectInfo.CommentID). CID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID) default: return } event.AddExtra("vote_up_amount", fmt.Sprintf("%d", resp.UpVotes)) event.AddExtra("vote_down_amount", fmt.Sprintf("%d", resp.DownVotes)) vs.eventQueueService.Send(ctx, event) } ================================================ FILE: internal/service/dashboard/dashboard_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package dashboard import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "time" "github.com/apache/answer/internal/service/review" "github.com/apache/answer/internal/service/revision" "github.com/apache/answer/pkg/converter" "xorm.io/xorm/schemas" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_common" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/export" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/report_common" "github.com/apache/answer/internal/service/service_config" "github.com/apache/answer/internal/service/siteinfo_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/dir" "github.com/segmentfault/pacman/log" ) type dashboardService struct { questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo commentRepo comment_common.CommentCommonRepo voteRepo activity_common.VoteRepo userRepo usercommon.UserRepo reportRepo report_common.ReportRepo configService *config.ConfigService siteInfoService siteinfo_common.SiteInfoCommonService serviceConfig *service_config.ServiceConfig reviewService *review.ReviewService revisionRepo revision.RevisionRepo data *data.Data } func NewDashboardService( questionRepo questioncommon.QuestionRepo, answerRepo answercommon.AnswerRepo, commentRepo comment_common.CommentCommonRepo, voteRepo activity_common.VoteRepo, userRepo usercommon.UserRepo, reportRepo report_common.ReportRepo, configService *config.ConfigService, siteInfoService siteinfo_common.SiteInfoCommonService, serviceConfig *service_config.ServiceConfig, reviewService *review.ReviewService, revisionRepo revision.RevisionRepo, data *data.Data, ) DashboardService { return &dashboardService{ questionRepo: questionRepo, answerRepo: answerRepo, commentRepo: commentRepo, voteRepo: voteRepo, userRepo: userRepo, reportRepo: reportRepo, configService: configService, siteInfoService: siteInfoService, serviceConfig: serviceConfig, reviewService: reviewService, revisionRepo: revisionRepo, data: data, } } type DashboardService interface { Statistical(ctx context.Context) (resp *schema.DashboardInfo, err error) } func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) { dashboardInfo := ds.getFromCache(ctx) security, err := ds.siteInfoService.GetSiteSecurity(ctx) if err != nil { log.Errorf("get general site info failed: %s", err) return dashboardInfo, nil } if dashboardInfo == nil { dashboardInfo = &schema.DashboardInfo{} dashboardInfo.AnswerCount = ds.answerCount(ctx) dashboardInfo.CommentCount = ds.commentCount(ctx) dashboardInfo.UserCount = ds.userCount(ctx) dashboardInfo.VoteCount = ds.voteCount(ctx) dashboardInfo.OccupyingStorageSpace = ds.calculateStorage() if security.CheckUpdate { dashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx) } dashboardInfo.DatabaseVersion = ds.getDatabaseInfo() dashboardInfo.DatabaseSize = ds.GetDatabaseSize() } dashboardInfo.QuestionCount = ds.questionCount(ctx) dashboardInfo.UnansweredCount = ds.unansweredQuestionCount(ctx) dashboardInfo.ResolvedCount = ds.resolvedQuestionCount(ctx) if dashboardInfo.QuestionCount == 0 { dashboardInfo.ResolvedRate = "0.00" dashboardInfo.UnansweredRate = "0.00" } else { dashboardInfo.ResolvedRate = fmt.Sprintf("%.2f", float64(dashboardInfo.ResolvedCount)/float64(dashboardInfo.QuestionCount)*100) dashboardInfo.UnansweredRate = fmt.Sprintf("%.2f", float64(dashboardInfo.UnansweredCount)/float64(dashboardInfo.QuestionCount)*100) } dashboardInfo.ReportCount = ds.reportCount(ctx) dashboardInfo.SMTP = ds.smtpStatus(ctx) dashboardInfo.HTTPS = ds.httpsStatus(ctx) dashboardInfo.TimeZone = ds.getTimezone(ctx) dashboardInfo.UploadingFiles = true dashboardInfo.AppStartTime = fmt.Sprintf("%d", time.Now().Unix()-schema.AppStartTime.Unix()) dashboardInfo.VersionInfo.Version = constant.Version dashboardInfo.VersionInfo.Revision = constant.Revision dashboardInfo.GoVersion = constant.GoVersion dashboardInfo.LoginRequired = security.LoginRequired ds.setCache(ctx, dashboardInfo) return dashboardInfo, nil } func (ds *dashboardService) getFromCache(ctx context.Context) (dashboardInfo *schema.DashboardInfo) { infoStr, exist, err := ds.data.Cache.GetString(ctx, schema.DashboardCacheKey) if err != nil { log.Errorf("get dashboard statistical from cache failed: %s", err) return nil } if !exist { return nil } dashboardInfo = &schema.DashboardInfo{} if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil { return nil } return dashboardInfo } func (ds *dashboardService) setCache(ctx context.Context, info *schema.DashboardInfo) { infoStr, _ := json.Marshal(info) err := ds.data.Cache.SetString(ctx, schema.DashboardCacheKey, string(infoStr), schema.DashboardCacheTime) if err != nil { log.Errorf("set dashboard statistical failed: %s", err) } } func (ds *dashboardService) questionCount(ctx context.Context) int64 { questionCount, err := ds.questionRepo.GetQuestionCount(ctx) if err != nil { log.Errorf("get question count failed: %s", err) } return questionCount } func (ds *dashboardService) unansweredQuestionCount(ctx context.Context) int64 { unansweredQuestionCount, err := ds.questionRepo.GetUnansweredQuestionCount(ctx) if err != nil { log.Errorf("get unanswered question count failed: %s", err) } return unansweredQuestionCount } func (ds *dashboardService) resolvedQuestionCount(ctx context.Context) int64 { resolvedQuestionCount, err := ds.questionRepo.GetResolvedQuestionCount(ctx) if err != nil { log.Errorf("get resolved question count failed: %s", err) } return resolvedQuestionCount } func (ds *dashboardService) answerCount(ctx context.Context) int64 { answerCount, err := ds.answerRepo.GetAnswerCount(ctx) if err != nil { log.Errorf("get answer count failed: %s", err) } return answerCount } func (ds *dashboardService) commentCount(ctx context.Context) int64 { commentCount, err := ds.commentRepo.GetCommentCount(ctx) if err != nil { log.Errorf("get comment count failed: %s", err) } return commentCount } func (ds *dashboardService) userCount(ctx context.Context) int64 { userCount, err := ds.userRepo.GetUserCount(ctx) if err != nil { log.Errorf("get user count failed: %s", err) } return userCount } func (ds *dashboardService) reportCount(ctx context.Context) int64 { reviewCount, err := ds.reviewService.GetReviewPendingCount(ctx) if err != nil { log.Errorf("get review count failed: %s", err) } reportCount, err := ds.reportRepo.GetReportCount(ctx) if err != nil { log.Errorf("get report count failed: %s", err) } countUnreviewedRevision, err := ds.revisionRepo.CountUnreviewedRevision(ctx, []int{ constant.ObjectTypeStrMapping[constant.AnswerObjectType], constant.ObjectTypeStrMapping[constant.QuestionObjectType], constant.ObjectTypeStrMapping[constant.TagObjectType], }) if err != nil { log.Errorf("get revision count failed: %s", err) } return reviewCount + reportCount + countUnreviewedRevision } // count vote func (ds *dashboardService) voteCount(ctx context.Context) int64 { typeKeys := []string{ "question.vote_up", "question.vote_down", "answer.vote_up", "answer.vote_down", } var activityTypes []int for _, typeKey := range typeKeys { cfg, err := ds.configService.GetConfigByKey(ctx, typeKey) if err != nil { continue } activityTypes = append(activityTypes, cfg.ID) } voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes) if err != nil { log.Errorf("get vote count failed: %s", err) } return voteCount } func (ds *dashboardService) remoteVersion(_ context.Context) string { req, _ := http.NewRequest("GET", "https://answer.apache.org/data/latest.json?from_version="+constant.Version, nil) req.Header.Set("User-Agent", "Answer/"+constant.Version) httpClient := &http.Client{} httpClient.Timeout = 15 * time.Second resp, err := httpClient.Do(req) if err != nil { log.Errorf("request remote version failed: %s", err) return "" } defer func() { _ = resp.Body.Close() }() respByte, err := io.ReadAll(resp.Body) if err != nil { log.Errorf("read response body failed: %s", err) return "" } remoteVersion := &schema.RemoteVersion{} if err := json.Unmarshal(respByte, remoteVersion); err != nil { log.Errorf("parsing response body failed: %s", err) return "" } return remoteVersion.Release.Version } func (ds *dashboardService) smtpStatus(ctx context.Context) (smtpStatus string) { smtpStatus = "not_configured" emailConf, err := ds.configService.GetStringValue(ctx, "email.config") if err != nil { log.Errorf("get email config failed: %s", err) return "disabled" } ec := &export.EmailConfig{} err = json.Unmarshal([]byte(emailConf), ec) if err != nil { log.Errorf("parsing email config failed: %s", err) return "disabled" } if ec.SMTPHost != "" { smtpStatus = "enabled" } return smtpStatus } func (ds *dashboardService) httpsStatus(ctx context.Context) (enabled bool) { siteGeneral, err := ds.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general failed: %s", err) return false } siteUrl, err := url.Parse(siteGeneral.SiteUrl) if err != nil { log.Errorf("parse site url failed: %s", err) return false } return siteUrl.Scheme == "https" } func (ds *dashboardService) getTimezone(ctx context.Context) string { siteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx) if err != nil { return "" } return siteInfoInterface.TimeZone } func (ds *dashboardService) calculateStorage() string { dirSize, err := dir.DirSize(ds.serviceConfig.UploadPath) if err != nil { log.Errorf("get upload dir size failed: %s", err) return "" } return dir.FormatFileSize(dirSize) } func (ds *dashboardService) getDatabaseInfo() (versionDesc string) { dbVersion, err := ds.data.DB.DBVersion() if err != nil { log.Errorf("get db version failed: %s", err) } else { versionDesc = fmt.Sprintf("%s %s", ds.data.DB.Dialect().URI().DBType, dbVersion.Number) } return versionDesc } func (ds *dashboardService) GetDatabaseSize() (dbSize string) { switch ds.data.DB.Dialect().URI().DBType { case schemas.MYSQL: sql := fmt.Sprintf("SELECT SUM(DATA_LENGTH) as db_size FROM information_schema.TABLES WHERE table_schema = '%s'", ds.data.DB.Dialect().URI().DBName) res, err := ds.data.DB.QueryInterface(sql) if err != nil { log.Warnf("get db size failed: %s", err) } else if len(res) > 0 && res[0]["db_size"] != nil { dbSizeStr, _ := res[0]["db_size"].(string) dbSize = dir.FormatFileSize(converter.StringToInt64(dbSizeStr)) } case schemas.POSTGRES: sql := fmt.Sprintf("SELECT pg_database_size('%s') AS db_size", ds.data.DB.Dialect().URI().DBName) res, err := ds.data.DB.QueryInterface(sql) if err != nil { log.Warnf("get db size failed: %s", err) } else if len(res) > 0 && res[0]["db_size"] != nil { dbSizeStr, _ := res[0]["db_size"].(int32) dbSize = dir.FormatFileSize(int64(dbSizeStr)) } case schemas.SQLITE: dirSize, err := dir.DirSize(ds.data.DB.DataSourceName()) if err != nil { log.Errorf("get upload dir size failed: %s", err) return "" } dbSize = dir.FormatFileSize(dirSize) } return dbSize } ================================================ FILE: internal/service/dashboard/dashboard_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package dashboard ================================================ FILE: internal/service/eventqueue/event_queue.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package eventqueue import ( "github.com/apache/answer/internal/base/queue" "github.com/apache/answer/internal/schema" ) type Service queue.Service[*schema.EventMsg] func NewService() Service { return queue.New[*schema.EventMsg]("event", 128) } ================================================ FILE: internal/service/export/email_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package export import ( "crypto/tls" "encoding/json" "fmt" "mime" "os" "strings" "time" "github.com/apache/answer/pkg/display" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "golang.org/x/net/context" "gopkg.in/gomail.v2" ) // EmailService kit service type EmailService struct { configService *config.ConfigService emailRepo EmailRepo siteInfoService siteinfo_common.SiteInfoCommonService } // EmailRepo email repository type EmailRepo interface { SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error VerifyCode(ctx context.Context, code string) (content string, err error) } // NewEmailService email service func NewEmailService( configService *config.ConfigService, emailRepo EmailRepo, siteInfoService siteinfo_common.SiteInfoCommonService, ) *EmailService { return &EmailService{ configService: configService, emailRepo: emailRepo, siteInfoService: siteInfoService, } } // EmailConfig email config type EmailConfig struct { FromEmail string `json:"from_email"` FromName string `json:"from_name"` SMTPHost string `json:"smtp_host"` SMTPPort int `json:"smtp_port"` Encryption string `json:"encryption"` // "" SSL TLS SMTPUsername string `json:"smtp_username"` SMTPPassword string `json:"smtp_password"` SMTPAuthentication bool `json:"smtp_authentication"` } func (e *EmailConfig) IsSSL() bool { return e.Encryption == "SSL" } func (e *EmailConfig) IsTLS() bool { return e.Encryption == "TLS" } // SaveCode save code func (es *EmailService) SaveCode(ctx context.Context, userID, code, codeContent string) { err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime) if err != nil { log.Error(err) } } // SendAndSaveCode send email and save code func (es *EmailService) SendAndSaveCode(ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string) { err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime) if err != nil { log.Error(err) return } es.Send(ctx, toEmailAddr, subject, body) } // SendAndSaveCodeWithTime send email and save code func (es *EmailService) SendAndSaveCodeWithTime( ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) { err := es.emailRepo.SetCode(ctx, userID, code, codeContent, duration) if err != nil { log.Error(err) return } es.Send(ctx, toEmailAddr, subject, body) } // Send email send func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body string) { log.Infof("try to send email to %s", toEmailAddr) ec, err := es.GetEmailConfig(ctx) if err != nil { log.Errorf("get email config failed: %s", err) return } if len(ec.SMTPHost) == 0 { log.Warnf("smtp host is empty, skip send email") return } m := gomail.NewMessage() fromName := mime.QEncoding.Encode("utf-8", ec.FromName) m.SetHeader("From", fmt.Sprintf("%s <%s>", fromName, ec.FromEmail)) m.SetHeader("To", toEmailAddr) m.SetHeader("Subject", subject) m.SetBody("text/html", body) d := gomail.NewDialer(ec.SMTPHost, ec.SMTPPort, ec.SMTPUsername, ec.SMTPPassword) if ec.IsSSL() { d.SSL = true } if ec.IsTLS() { d.SSL = false } if len(os.Getenv("SKIP_SMTP_TLS_VERIFY")) > 0 { d.TLSConfig = &tls.Config{ServerName: d.Host, InsecureSkipVerify: true} } if err := d.DialAndSend(m); err != nil { log.Errorf("send email to %s failed: %s", toEmailAddr, err) } else { log.Infof("send email to %s success", toEmailAddr) } } // VerifyUrlExpired email send func (es *EmailService) VerifyUrlExpired(ctx context.Context, code string) (content string) { content, err := es.emailRepo.VerifyCode(ctx, code) if err != nil { log.Error(err) } return content } func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string) (title, body string, err error) { siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } templateData := &schema.RegisterTemplateData{ SiteName: siteInfo.Name, RegisterUrl: registerUrl, } lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyRegisterTitle, templateData) body = translator.TrWithData(lang, constant.EmailTplKeyRegisterBody, templateData) return title, body, nil } func (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl string) (title, body string, err error) { siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } templateData := &schema.PassResetTemplateData{SiteName: siteInfo.Name, PassResetUrl: passResetUrl} lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyPassResetTitle, templateData) body = translator.TrWithData(lang, constant.EmailTplKeyPassResetBody, templateData) return title, body, nil } func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl string) (title, body string, err error) { siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } templateData := &schema.ChangeEmailTemplateData{ SiteName: siteInfo.Name, ChangeEmailUrl: changeEmailUrl, } lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyChangeEmailTitle, templateData) body = translator.TrWithData(lang, constant.EmailTplKeyChangeEmailBody, templateData) return title, body, nil } // TestTemplate send test email template parse func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) { siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } templateData := &schema.TestTemplateData{SiteName: siteInfo.Name} lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyTestTitle, templateData) body = translator.TrWithData(lang, constant.EmailTplKeyTestBody, templateData) return title, body, nil } // NewAnswerTemplate new answer template func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAnswerTemplateRawData) ( title, body string, err error) { siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) if err != nil { return } templateData := &schema.NewAnswerTemplateData{ SiteName: siteInfo.Name, DisplayName: raw.AnswerUserDisplayName, QuestionTitle: raw.QuestionTitle, AnswerUrl: display.AnswerURL(seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle, raw.AnswerID), AnswerSummary: raw.AnswerSummary, UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), } lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerTitle, templateData) body = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerBody, templateData) return title, body, nil } // NewInviteAnswerTemplate new invite answer template func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema.NewInviteAnswerTemplateRawData) ( title, body string, err error) { siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) if err != nil { return } templateData := &schema.NewInviteAnswerTemplateData{ SiteName: siteInfo.Name, DisplayName: raw.InviterDisplayName, QuestionTitle: raw.QuestionTitle, InviteUrl: display.QuestionURL(seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle), UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), } lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerTitle, templateData) body = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, templateData) return title, body, nil } // NewCommentTemplate new comment template func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) ( title, body string, err error) { siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) if err != nil { return } templateData := &schema.NewCommentTemplateData{ SiteName: siteInfo.Name, DisplayName: raw.CommentUserDisplayName, QuestionTitle: raw.QuestionTitle, CommentSummary: raw.CommentSummary, UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), } templateData.CommentUrl = display.CommentURL(seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle, raw.AnswerID, raw.CommentID) lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData) body = translator.TrWithData(lang, constant.EmailTplKeyNewCommentBody, templateData) return title, body, nil } // NewQuestionTemplate new question template func (es *EmailService) NewQuestionTemplate(ctx context.Context, raw *schema.NewQuestionTemplateRawData) ( title, body string, err error) { siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) if err != nil { return } templateData := &schema.NewQuestionTemplateData{ SiteName: siteInfo.Name, QuestionTitle: raw.QuestionTitle, Tags: strings.Join(raw.Tags, ", "), UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), } templateData.QuestionUrl = display.QuestionURL( seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle) lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionTitle, templateData) body = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionBody, templateData) return title, body, nil } func (es *EmailService) GetEmailConfig(ctx context.Context) (ec *EmailConfig, err error) { emailConf, err := es.configService.GetStringValue(ctx, constant.EmailConfigKey) if err != nil { return nil, err } ec = &EmailConfig{} err = json.Unmarshal([]byte(emailConf), ec) if err != nil { log.Errorf("old email config format is invalid, you need to update smtp config: %v", err) return nil, errors.BadRequest(reason.SiteInfoConfigNotFound) } return ec, nil } // SetEmailConfig set email config func (es *EmailService) SetEmailConfig(ctx context.Context, ec *EmailConfig) (err error) { data, _ := json.Marshal(ec) return es.configService.UpdateConfig(ctx, constant.EmailConfigKey, string(data)) } ================================================ FILE: internal/service/feature_toggle/feature_toggle_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package feature_toggle import ( "context" "encoding/json" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/segmentfault/pacman/errors" ) // Feature keys const ( FeatureBadge = "badge" FeatureCustomDomain = "custom_domain" FeatureMCP = "mcp" FeaturePrivateAPI = "private_api" FeatureAIChatbot = "ai_chatbot" FeatureArticle = "article" FeatureCategory = "category" ) type toggleConfig struct { Toggles map[string]bool `json:"toggles"` } // FeatureToggleService persist and query feature switches. type FeatureToggleService struct { siteInfoRepo siteinfo_common.SiteInfoRepo } // NewFeatureToggleService creates a new feature toggle service instance. func NewFeatureToggleService(siteInfoRepo siteinfo_common.SiteInfoRepo) *FeatureToggleService { return &FeatureToggleService{ siteInfoRepo: siteInfoRepo, } } // UpdateAll overwrites the feature toggle configuration. func (s *FeatureToggleService) UpdateAll(ctx context.Context, toggles map[string]bool) error { cfg := &toggleConfig{ Toggles: sanitizeToggleMap(toggles), } data, err := json.Marshal(cfg) if err != nil { return err } info := &entity.SiteInfo{ Type: constant.SiteTypeFeatureToggle, Content: string(data), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeFeatureToggle, info) } // GetAll returns all feature toggles. func (s *FeatureToggleService) GetAll(ctx context.Context) (map[string]bool, error) { siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeFeatureToggle, true) if err != nil { return nil, err } if !exist || siteInfo == nil || siteInfo.Content == "" { return map[string]bool{}, nil } cfg := &toggleConfig{} if err := json.Unmarshal([]byte(siteInfo.Content), cfg); err != nil { return map[string]bool{}, err } return sanitizeToggleMap(cfg.Toggles), nil } // IsEnabled returns whether a feature is enabled. Missing config defaults to true. func (s *FeatureToggleService) IsEnabled(ctx context.Context, feature string) (bool, error) { toggles, err := s.GetAll(ctx) if err != nil { return false, err } if len(toggles) == 0 { return true, nil } value, ok := toggles[feature] if !ok { return true, nil } return value, nil } // EnsureEnabled returns error if feature disabled. func (s *FeatureToggleService) EnsureEnabled(ctx context.Context, feature string) error { enabled, err := s.IsEnabled(ctx, feature) if err != nil { return err } if !enabled { return errors.BadRequest(reason.ErrFeatureDisabled) } return nil } func sanitizeToggleMap(in map[string]bool) map[string]bool { if in == nil { return map[string]bool{} } return in } ================================================ FILE: internal/service/file_record/file_record_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package file_record import ( "context" "fmt" "os" "path/filepath" "strings" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/revision" "github.com/apache/answer/internal/service/service_config" "github.com/apache/answer/internal/service/siteinfo_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/dir" "github.com/apache/answer/pkg/writer" "github.com/segmentfault/pacman/log" ) // FileRecordRepo file record repository type FileRecordRepo interface { AddFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) UpdateFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) GetFileRecordPage(ctx context.Context, page, pageSize int, cond *entity.FileRecord) ( fileRecordList []*entity.FileRecord, total int64, err error) DeleteFileRecord(ctx context.Context, id int) (err error) GetFileRecordByURL(ctx context.Context, fileURL string) (record *entity.FileRecord, err error) } // FileRecordService file record service type FileRecordService struct { fileRecordRepo FileRecordRepo revisionRepo revision.RevisionRepo serviceConfig *service_config.ServiceConfig siteInfoService siteinfo_common.SiteInfoCommonService userService *usercommon.UserCommon } // NewFileRecordService new file record service func NewFileRecordService( fileRecordRepo FileRecordRepo, revisionRepo revision.RevisionRepo, serviceConfig *service_config.ServiceConfig, siteInfoService siteinfo_common.SiteInfoCommonService, userService *usercommon.UserCommon, ) *FileRecordService { return &FileRecordService{ fileRecordRepo: fileRecordRepo, revisionRepo: revisionRepo, serviceConfig: serviceConfig, siteInfoService: siteInfoService, userService: userService, } } // AddFileRecord add file record func (fs *FileRecordService) AddFileRecord(ctx context.Context, userID, filePath, fileURL, source string) { record := &entity.FileRecord{ UserID: userID, FilePath: filePath, FileURL: fileURL, Source: source, Status: entity.FileRecordStatusAvailable, ObjectID: "0", } if err := fs.fileRecordRepo.AddFileRecord(ctx, record); err != nil { log.Errorf("add file record error: %v", err) } } // CleanOrphanUploadFiles clean orphan upload files func (fs *FileRecordService) CleanOrphanUploadFiles(ctx context.Context) { page, pageSize := 1, 1000 for { fileRecordList, total, err := fs.fileRecordRepo.GetFileRecordPage(ctx, page, pageSize, &entity.FileRecord{ Status: entity.FileRecordStatusAvailable, }) if err != nil { log.Errorf("get file record page error: %v", err) return } if len(fileRecordList) == 0 || total == 0 { break } for _, fileRecord := range fileRecordList { // If this file record created in 48 hours, no need to check if fileRecord.CreatedAt.AddDate(0, 0, 2).After(time.Now()) { continue } if isBrandingOrAvatarFile(fileRecord.FilePath) { if strings.Contains(fileRecord.FilePath, constant.BrandingSubPath+"/") { if fs.siteInfoService.IsBrandingFileUsed(ctx, fileRecord.FilePath) { continue } } else if strings.Contains(fileRecord.FilePath, constant.AvatarSubPath+"/") { if fs.userService.IsAvatarFileUsed(ctx, fileRecord.FilePath) { continue } } if err := fs.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil { log.Error(err) } continue } if checker.IsNotZeroString(fileRecord.ObjectID) { _, exist, err := fs.revisionRepo.GetLastRevisionByObjectID(ctx, fileRecord.ObjectID) if err != nil { log.Errorf("get last revision by object id error: %v", err) continue } if exist { continue } } else { lastRevision, exist, err := fs.revisionRepo.GetLastRevisionByFileURL(ctx, fileRecord.FileURL) if err != nil { log.Errorf("get last revision by file url error: %v", err) continue } if exist { // update the file record object id fileRecord.ObjectID = lastRevision.ObjectID if err := fs.fileRecordRepo.UpdateFileRecord(ctx, fileRecord); err != nil { log.Errorf("update file record object id error: %v", err) } continue } } // Delete and move the file record if err := fs.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil { log.Error(err) } } page++ } } func isBrandingOrAvatarFile(filePath string) bool { return strings.Contains(filePath, constant.BrandingSubPath+"/") || strings.Contains(filePath, constant.AvatarSubPath+"/") } func (fs *FileRecordService) PurgeDeletedFiles(ctx context.Context) { deletedPath := filepath.Join(fs.serviceConfig.UploadPath, constant.DeletedSubPath) log.Infof("purge deleted files: %s", deletedPath) err := os.RemoveAll(deletedPath) if err != nil { log.Errorf("purge deleted files error: %v", err) return } err = dir.CreateDirIfNotExist(deletedPath) if err != nil { log.Errorf("create deleted directory error: %v", err) } } func (fs *FileRecordService) DeleteAndMoveFileRecord(ctx context.Context, fileRecord *entity.FileRecord) error { // Delete the file record if err := fs.fileRecordRepo.DeleteFileRecord(ctx, fileRecord.ID); err != nil { return fmt.Errorf("delete file record error: %v", err) } // Move the file to the deleted directory oldFilename := filepath.Base(fileRecord.FilePath) oldFilePath := filepath.Join(fs.serviceConfig.UploadPath, fileRecord.FilePath) deletedPath := filepath.Join(fs.serviceConfig.UploadPath, constant.DeletedSubPath, oldFilename) if err := writer.MoveFile(oldFilePath, deletedPath); err != nil { return fmt.Errorf("move file error: %v", err) } log.Debugf("delete and move file: %s", fileRecord.FileURL) return nil } func (fs *FileRecordService) GetFileRecordByURL(ctx context.Context, fileURL string) (record *entity.FileRecord, err error) { record, err = fs.fileRecordRepo.GetFileRecordByURL(ctx, fileURL) if err != nil { log.Errorf("error retrieving file record by URL: %v", err) return } return } ================================================ FILE: internal/service/follow/follow_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package follow import ( "context" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_common" tagcommon "github.com/apache/answer/internal/service/tag_common" ) type FollowRepo interface { Follow(ctx context.Context, objectId, userId string) error FollowCancel(ctx context.Context, objectId, userId string) error } type FollowService struct { tagRepo tagcommon.TagCommonRepo followRepo FollowRepo followCommonRepo activity_common.FollowRepo } func NewFollowService( followRepo FollowRepo, followCommonRepo activity_common.FollowRepo, tagRepo tagcommon.TagCommonRepo, ) *FollowService { return &FollowService{ followRepo: followRepo, followCommonRepo: followCommonRepo, tagRepo: tagRepo, } } // Follow or cancel follow object func (fs *FollowService) Follow(ctx context.Context, dto *schema.FollowDTO) (resp schema.FollowResp, err error) { if dto.IsCancel { err = fs.followRepo.FollowCancel(ctx, dto.ObjectID, dto.UserID) } else { err = fs.followRepo.Follow(ctx, dto.ObjectID, dto.UserID) } if err != nil { return resp, err } follows, err := fs.followCommonRepo.GetFollowAmount(ctx, dto.ObjectID) if err != nil { return resp, err } resp.Follows = follows resp.IsFollowed = !dto.IsCancel return resp, nil } // UpdateFollowTags update user follow tags func (fs *FollowService) UpdateFollowTags(ctx context.Context, req *schema.UpdateFollowTagsReq) (err error) { objIDs, err := fs.followCommonRepo.GetFollowIDs(ctx, req.UserID, entity.Tag{}.TableName()) if err != nil { return } oldFollowTagList, err := fs.tagRepo.GetTagListByIDs(ctx, objIDs) if err != nil { return err } oldTagMapping := make(map[string]bool) for _, tag := range oldFollowTagList { oldTagMapping[tag.SlugName] = true } newTagMapping := make(map[string]bool) for _, tag := range req.SlugNameList { newTagMapping[tag] = true } // cancel follow for _, tag := range oldFollowTagList { if !newTagMapping[tag.SlugName] { err := fs.followRepo.FollowCancel(ctx, tag.ID, req.UserID) if err != nil { return err } } } // new follow for _, tagSlugName := range req.SlugNameList { if !oldTagMapping[tagSlugName] { tagInfo, exist, err := fs.tagRepo.GetTagBySlugName(ctx, tagSlugName) if err != nil { return err } if !exist { continue } err = fs.followRepo.Follow(ctx, tagInfo.ID, req.UserID) if err != nil { return err } } } return nil } ================================================ FILE: internal/service/importer/importer_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package importer import ( "context" "fmt" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/permission" "github.com/apache/answer/internal/service/rank" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // ImporterService importer service type ImporterService struct { questionService *content.QuestionService rankService *rank.RankService userCommon *usercommon.UserCommon } // NewRankService new rank service func NewImporterService( questionService *content.QuestionService, rankService *rank.RankService, userCommon *usercommon.UserCommon) *ImporterService { return &ImporterService{ questionService: questionService, rankService: rankService, userCommon: userCommon, } } type ImporterFunc struct { importerService *ImporterService } func (ipfunc *ImporterFunc) AddQuestion(ctx context.Context, questionInfo plugin.QuestionImporterInfo) (err error) { return ipfunc.importerService.ImportQuestion(ctx, questionInfo) } func (ip *ImporterService) NewImporterFunc() plugin.ImporterFunc { return &ImporterFunc{importerService: ip} } func (ip *ImporterService) ImportQuestion(ctx context.Context, questionInfo plugin.QuestionImporterInfo) (err error) { req := &schema.QuestionAdd{} errFields := make([]*validator.FormErrorField, 0) // To limit rate, remove the following code from comment: Part 1/2 // reject, rejectKey := ipc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) // if reject { // return // } userInfo, exist, err := ip.userCommon.GetByEmail(ctx, questionInfo.UserEmail) if err != nil { log.Errorf("error: %v", err) return err } if !exist { return fmt.Errorf("user not found") } // To limit rate, remove the following code from comment: Part 2/2 // defer func() { // // If status is not 200 means that the bad request has been returned, so the record should be cleared // if ctx.Writer.Status() != http.StatusOK { // ipc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey) // } // }() req.UserID = userInfo.ID req.Title = questionInfo.Title req.Content = questionInfo.Content req.HTML = "

" + questionInfo.Content + "

" req.Tags = make([]*schema.TagItem, len(questionInfo.Tags)) for i, tag := range questionInfo.Tags { req.Tags[i] = &schema.TagItem{ SlugName: tag, DisplayName: tag, } } canList, requireRanks, err := ip.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ permission.QuestionAdd, permission.QuestionEdit, permission.QuestionDelete, permission.QuestionClose, permission.QuestionReopen, permission.TagUseReservedTag, permission.TagAdd, permission.LinkUrlLimit, }) if err != nil { log.Errorf("error: %v", err) return err } req.CanAdd = canList[0] req.CanEdit = canList[1] req.CanDelete = canList[2] req.CanClose = canList[3] req.CanReopen = canList[4] req.CanUseReservedTag = canList[5] req.CanAddTag = canList[6] if !req.CanAdd { log.Errorf("error: %v", err) return err } hasNewTag, err := ip.questionService.HasNewTag(ctx.(*gin.Context), req.Tags) if err != nil { log.Errorf("error: %v", err) return err } if !req.CanAddTag && hasNewTag { lang := handler.GetLangByCtx(ctx.(*gin.Context)) msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[6]}) log.Errorf("error: %v", msg) return errors.BadRequest(msg) } errList, err := ip.questionService.CheckAddQuestion(ctx, req) if err != nil { errlist, ok := errList.([]*validator.FormErrorField) if ok { errFields = append(errFields, errlist...) } } if len(errFields) > 0 { return errors.BadRequest(reason.RequestFormatError) } ginCtx := ctx.(*gin.Context) req.UserAgent = ginCtx.GetHeader("User-Agent") req.IP = ginCtx.ClientIP() resp, err := ip.questionService.AddQuestion(ctx, req) if err != nil { errlist, ok := resp.([]*validator.FormErrorField) if ok { errFields = append(errFields, errlist...) } } if len(errFields) > 0 { log.Errorf("error: RequestFormatError") return errors.BadRequest(reason.RequestFormatError) } log.Info("Add Question Successfully") return nil } ================================================ FILE: internal/service/meta/meta_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package meta import ( "context" "encoding/json" "errors" "strconv" "strings" "github.com/apache/answer/internal/service/eventqueue" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" answercommon "github.com/apache/answer/internal/service/answer_common" metacommon "github.com/apache/answer/internal/service/meta_common" questioncommon "github.com/apache/answer/internal/service/question_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/obj" myErrors "github.com/segmentfault/pacman/errors" ) // MetaService user service type MetaService struct { metaCommonService *metacommon.MetaCommonService userCommon *usercommon.UserCommon questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo eventQueueService eventqueue.Service } func NewMetaService( metaCommonService *metacommon.MetaCommonService, userCommon *usercommon.UserCommon, answerRepo answercommon.AnswerRepo, questionRepo questioncommon.QuestionRepo, eventQueueService eventqueue.Service, ) *MetaService { return &MetaService{ metaCommonService: metaCommonService, questionRepo: questionRepo, userCommon: userCommon, answerRepo: answerRepo, eventQueueService: eventQueueService, } } // GetReactionByObjectId get reaction func (ms *MetaService) GetReactionByObjectId(ctx context.Context, req *schema.GetReactionReq) (resp *schema.GetReactionByObjectIdResp, err error) { reactionMeta, err := ms.metaCommonService.GetMetaByObjectIdAndKey(ctx, req.ObjectID, entity.ObjectReactSummaryKey) // if not exist, return nil if err != nil { var pacmanErr *myErrors.Error if errors.As(err, &pacmanErr) && pacmanErr.Reason == reason.MetaObjectNotFound { return nil, nil } else { return nil, err } } var reaction schema.ReactionsSummaryMeta err = json.Unmarshal([]byte(reactionMeta.Value), &reaction) if err != nil { return nil, err } return ms.convertToReactionResp(ctx, req.UserID, &reaction) } // AddOrUpdateReaction add or update reaction func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.UpdateReactionReq) (resp *schema.GetReactionByObjectIdResp, err error) { // check if object exist and it's answer or question objectType, err := obj.GetObjectTypeStrByObjectID(req.ObjectID) if err != nil { return nil, err } var event *schema.EventMsg switch objectType { case constant.AnswerObjectType: answerInfo, exist, err := ms.answerRepo.GetAnswer(ctx, req.ObjectID) if err != nil { return nil, err } if !exist { return nil, myErrors.BadRequest(reason.AnswerNotFound) } event = schema.NewEvent(constant.EventAnswerReact, req.UserID).TID(answerInfo.ID). AID(answerInfo.ID, answerInfo.UserID) case constant.QuestionObjectType: questionInfo, exist, err := ms.questionRepo.GetQuestion(ctx, req.ObjectID) if err != nil { return nil, err } if !exist { return nil, myErrors.BadRequest(reason.QuestionNotFound) } event = schema.NewEvent(constant.EventQuestionReact, req.UserID).TID(questionInfo.ID). QID(questionInfo.ID, questionInfo.UserID) default: return nil, myErrors.BadRequest(reason.ObjectNotFound) } // add or update reactions := &schema.ReactionsSummaryMeta{} err = ms.metaCommonService.AddOrUpdateMetaByObjectIdAndKey(ctx, req.ObjectID, entity.ObjectReactSummaryKey, func(meta *entity.Meta, exist bool) (*entity.Meta, error) { // if not exist, create new one if exist { _ = json.Unmarshal([]byte(meta.Value), reactions) } // update reaction ms.updateReaction(req, reactions) // write back to meta repo reactSumBytes, err := json.Marshal(reactions) if err != nil { return nil, err } return &entity.Meta{ ObjectID: req.ObjectID, Key: entity.ObjectReactSummaryKey, Value: string(reactSumBytes), }, nil }) if err != nil { return nil, myErrors.InternalServer(reason.DatabaseError).WithError(err) } resp, err = ms.convertToReactionResp(ctx, req.UserID, reactions) if err != nil { return nil, err } ms.eventQueueService.Send(ctx, event) return resp, nil } // updateReaction update reaction func (ms *MetaService) updateReaction(req *schema.UpdateReactionReq, reactions *schema.ReactionsSummaryMeta) { switch req.Reaction { case "activate": reactions.AddReactionSummary(req.Emoji, req.UserID) case "deactivate": reactions.RemoveReactionSummary(req.Emoji, req.UserID) } } func (ms *MetaService) convertToReactionResp(ctx context.Context, userId string, r *schema.ReactionsSummaryMeta) ( resp *schema.GetReactionByObjectIdResp, err error) { lang := handler.GetLangByCtx(ctx) resp = &schema.GetReactionByObjectIdResp{ ReactionSummary: make([]*schema.ReactionRespItem, 0), } // traverse map and convert to username for _, reaction := range r.Reactions { item := &schema.ReactionRespItem{ Emoji: reaction.Emoji, IsActive: r.CheckUserInReactionSummary(reaction.Emoji, userId), } usernames := make([]string, 0) userBasicInfos, err := ms.userCommon.BatchUserBasicInfoByID(ctx, reaction.UserIDs) item.Count = len(userBasicInfos) if err != nil { return resp, err } // get username for _, userBasicInfo := range userBasicInfos { usernames = append(usernames, userBasicInfo.Username) if len(usernames) == 5 && len(userBasicInfos) > 5 { item.Tooltip = translator.TrWithData(lang, constant.ReactionTooltipLabel, map[string]string{ "Count": strconv.Itoa(len(userBasicInfos) - 5), "Names": strings.Join(usernames, ", "), }) break } } if len(userBasicInfos) <= 5 { item.Tooltip = strings.Join(usernames, ", ") } resp.ReactionSummary = append(resp.ReactionSummary, item) } return resp, nil } ================================================ FILE: internal/service/meta_common/meta_common_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package metacommon import ( "context" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" myErrors "github.com/segmentfault/pacman/errors" ) // MetaRepo meta repository type MetaRepo interface { AddMeta(ctx context.Context, meta *entity.Meta) (err error) RemoveMeta(ctx context.Context, id int) (err error) UpdateMeta(ctx context.Context, meta *entity.Meta) (err error) AddOrUpdateMetaByObjectIdAndKey(ctx context.Context, objectId, key string, f func(*entity.Meta, bool) (*entity.Meta, error)) error GetMetaByObjectIdAndKey(ctx context.Context, objectId, key string) (meta *entity.Meta, exist bool, err error) GetMetaList(ctx context.Context, meta *entity.Meta) (metas []*entity.Meta, err error) } // MetaCommonService user service type MetaCommonService struct { metaRepo MetaRepo } func NewMetaCommonService(metaRepo MetaRepo) *MetaCommonService { return &MetaCommonService{ metaRepo: metaRepo, } } // AddMeta add meta func (ms *MetaCommonService) AddMeta(ctx context.Context, objID, key, value string) (err error) { meta := &entity.Meta{ ObjectID: objID, Key: key, Value: value, } return ms.metaRepo.AddMeta(ctx, meta) } // RemoveMeta delete meta func (ms *MetaCommonService) RemoveMeta(ctx context.Context, id int) (err error) { return ms.metaRepo.RemoveMeta(ctx, id) } // UpdateMeta update meta func (ms *MetaCommonService) UpdateMeta(ctx context.Context, metaID int, key, value string) (err error) { meta := &entity.Meta{ ID: metaID, Key: key, Value: value, } return ms.metaRepo.UpdateMeta(ctx, meta) } func (ms *MetaCommonService) AddOrUpdateMetaByObjectIdAndKey(ctx context.Context, objID, key string, f func(*entity.Meta, bool) (*entity.Meta, error)) (err error) { return ms.metaRepo.AddOrUpdateMetaByObjectIdAndKey(ctx, objID, key, f) } // GetMetaByObjectIdAndKey get meta one func (ms *MetaCommonService) GetMetaByObjectIdAndKey(ctx context.Context, objectID, key string) (meta *entity.Meta, err error) { meta, exist, err := ms.metaRepo.GetMetaByObjectIdAndKey(ctx, objectID, key) if err != nil { return nil, err } if !exist { return nil, myErrors.BadRequest(reason.MetaObjectNotFound) } return meta, nil } // GetMetaList get meta list all func (ms *MetaCommonService) GetMetaList(ctx context.Context, objID string) (metas []*entity.Meta, err error) { metas, err = ms.metaRepo.GetMetaList(ctx, &entity.Meta{ObjectID: objID}) if err != nil { return nil, err } return metas, err } ================================================ FILE: internal/service/mock/siteinfo_repo_mock.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Code generated by MockGen. DO NOT EDIT. // Source: ./siteinfo_service.go // // Generated by this command: // // mockgen -source=./siteinfo_service.go -destination=../mock/siteinfo_repo_mock.go -package=mock // // Package mock is a generated GoMock package. package mock import ( context "context" reflect "reflect" entity "github.com/apache/answer/internal/entity" schema "github.com/apache/answer/internal/schema" gomock "go.uber.org/mock/gomock" ) // MockSiteInfoRepo is a mock of SiteInfoRepo interface. type MockSiteInfoRepo struct { ctrl *gomock.Controller recorder *MockSiteInfoRepoMockRecorder isgomock struct{} } // MockSiteInfoRepoMockRecorder is the mock recorder for MockSiteInfoRepo. type MockSiteInfoRepoMockRecorder struct { mock *MockSiteInfoRepo } // NewMockSiteInfoRepo creates a new mock instance. func NewMockSiteInfoRepo(ctrl *gomock.Controller) *MockSiteInfoRepo { mock := &MockSiteInfoRepo{ctrl: ctrl} mock.recorder = &MockSiteInfoRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockSiteInfoRepo) EXPECT() *MockSiteInfoRepoMockRecorder { return m.recorder } // GetByType mocks base method. func (m *MockSiteInfoRepo) GetByType(ctx context.Context, siteType string, withoutCache ...bool) (*entity.SiteInfo, bool, error) { m.ctrl.T.Helper() varargs := []any{ctx, siteType} for _, a := range withoutCache { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "GetByType", varargs...) ret0, _ := ret[0].(*entity.SiteInfo) ret1, _ := ret[1].(bool) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // GetByType indicates an expected call of GetByType. func (mr *MockSiteInfoRepoMockRecorder) GetByType(ctx, siteType any, withoutCache ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{ctx, siteType}, withoutCache...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByType", reflect.TypeOf((*MockSiteInfoRepo)(nil).GetByType), varargs...) } // IsBrandingFileUsed mocks base method. func (m *MockSiteInfoRepo) IsBrandingFileUsed(ctx context.Context, filePath string) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsBrandingFileUsed", ctx, filePath) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // IsBrandingFileUsed indicates an expected call of IsBrandingFileUsed. func (mr *MockSiteInfoRepoMockRecorder) IsBrandingFileUsed(ctx, filePath any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBrandingFileUsed", reflect.TypeOf((*MockSiteInfoRepo)(nil).IsBrandingFileUsed), ctx, filePath) } // SaveByType mocks base method. func (m *MockSiteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveByType", ctx, siteType, data) ret0, _ := ret[0].(error) return ret0 } // SaveByType indicates an expected call of SaveByType. func (mr *MockSiteInfoRepoMockRecorder) SaveByType(ctx, siteType, data any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveByType", reflect.TypeOf((*MockSiteInfoRepo)(nil).SaveByType), ctx, siteType, data) } // MockSiteInfoCommonService is a mock of SiteInfoCommonService interface. type MockSiteInfoCommonService struct { ctrl *gomock.Controller recorder *MockSiteInfoCommonServiceMockRecorder isgomock struct{} } // MockSiteInfoCommonServiceMockRecorder is the mock recorder for MockSiteInfoCommonService. type MockSiteInfoCommonServiceMockRecorder struct { mock *MockSiteInfoCommonService } // NewMockSiteInfoCommonService creates a new mock instance. func NewMockSiteInfoCommonService(ctrl *gomock.Controller) *MockSiteInfoCommonService { mock := &MockSiteInfoCommonService{ctrl: ctrl} mock.recorder = &MockSiteInfoCommonServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockSiteInfoCommonService) EXPECT() *MockSiteInfoCommonServiceMockRecorder { return m.recorder } // FormatAvatar mocks base method. func (m *MockSiteInfoCommonService) FormatAvatar(ctx context.Context, originalAvatarData, email string, userStatus int) *schema.AvatarInfo { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FormatAvatar", ctx, originalAvatarData, email, userStatus) ret0, _ := ret[0].(*schema.AvatarInfo) return ret0 } // FormatAvatar indicates an expected call of FormatAvatar. func (mr *MockSiteInfoCommonServiceMockRecorder) FormatAvatar(ctx, originalAvatarData, email, userStatus any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatAvatar", reflect.TypeOf((*MockSiteInfoCommonService)(nil).FormatAvatar), ctx, originalAvatarData, email, userStatus) } // FormatListAvatar mocks base method. func (m *MockSiteInfoCommonService) FormatListAvatar(ctx context.Context, userList []*entity.User) map[string]*schema.AvatarInfo { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FormatListAvatar", ctx, userList) ret0, _ := ret[0].(map[string]*schema.AvatarInfo) return ret0 } // FormatListAvatar indicates an expected call of FormatListAvatar. func (mr *MockSiteInfoCommonServiceMockRecorder) FormatListAvatar(ctx, userList any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatListAvatar", reflect.TypeOf((*MockSiteInfoCommonService)(nil).FormatListAvatar), ctx, userList) } // GetSiteAI mocks base method. func (m *MockSiteInfoCommonService) GetSiteAI(ctx context.Context) (*schema.SiteAIResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteAI", ctx) ret0, _ := ret[0].(*schema.SiteAIResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteAI indicates an expected call of GetSiteAI. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteAI(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteAI", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteAI), ctx) } // GetSiteAdvanced mocks base method. func (m *MockSiteInfoCommonService) GetSiteAdvanced(ctx context.Context) (*schema.SiteAdvancedResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteAdvanced", ctx) ret0, _ := ret[0].(*schema.SiteAdvancedResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteAdvanced indicates an expected call of GetSiteAdvanced. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteAdvanced(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteAdvanced", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteAdvanced), ctx) } // GetSiteBranding mocks base method. func (m *MockSiteInfoCommonService) GetSiteBranding(ctx context.Context) (*schema.SiteBrandingResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteBranding", ctx) ret0, _ := ret[0].(*schema.SiteBrandingResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteBranding indicates an expected call of GetSiteBranding. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteBranding(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteBranding", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteBranding), ctx) } // GetSiteCustomCssHTML mocks base method. func (m *MockSiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (*schema.SiteCustomCssHTMLResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteCustomCssHTML", ctx) ret0, _ := ret[0].(*schema.SiteCustomCssHTMLResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteCustomCssHTML indicates an expected call of GetSiteCustomCssHTML. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteCustomCssHTML(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteCustomCssHTML", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteCustomCssHTML), ctx) } // GetSiteGeneral mocks base method. func (m *MockSiteInfoCommonService) GetSiteGeneral(ctx context.Context) (*schema.SiteGeneralResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteGeneral", ctx) ret0, _ := ret[0].(*schema.SiteGeneralResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteGeneral indicates an expected call of GetSiteGeneral. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteGeneral(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteGeneral", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteGeneral), ctx) } // GetSiteInfoByType mocks base method. func (m *MockSiteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp any) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteInfoByType", ctx, siteType, resp) ret0, _ := ret[0].(error) return ret0 } // GetSiteInfoByType indicates an expected call of GetSiteInfoByType. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInfoByType(ctx, siteType, resp any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteInfoByType", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteInfoByType), ctx, siteType, resp) } // GetSiteInterface mocks base method. func (m *MockSiteInfoCommonService) GetSiteInterface(ctx context.Context) (*schema.SiteInterfaceSettingsResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteInterface", ctx) ret0, _ := ret[0].(*schema.SiteInterfaceSettingsResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteInterface indicates an expected call of GetSiteInterface. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInterface(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteInterface", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteInterface), ctx) } // GetSiteLogin mocks base method. func (m *MockSiteInfoCommonService) GetSiteLogin(ctx context.Context) (*schema.SiteLoginResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteLogin", ctx) ret0, _ := ret[0].(*schema.SiteLoginResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteLogin indicates an expected call of GetSiteLogin. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLogin(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteLogin", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteLogin), ctx) } // GetSiteMCP mocks base method. func (m *MockSiteInfoCommonService) GetSiteMCP(ctx context.Context) (*schema.SiteMCPResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteMCP", ctx) ret0, _ := ret[0].(*schema.SiteMCPResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteMCP indicates an expected call of GetSiteMCP. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteMCP(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteMCP", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteMCP), ctx) } // GetSitePolicies mocks base method. func (m *MockSiteInfoCommonService) GetSitePolicies(ctx context.Context) (*schema.SitePoliciesResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSitePolicies", ctx) ret0, _ := ret[0].(*schema.SitePoliciesResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSitePolicies indicates an expected call of GetSitePolicies. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSitePolicies(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSitePolicies", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSitePolicies), ctx) } // GetSiteQuestion mocks base method. func (m *MockSiteInfoCommonService) GetSiteQuestion(ctx context.Context) (*schema.SiteQuestionsResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteQuestion", ctx) ret0, _ := ret[0].(*schema.SiteQuestionsResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteQuestion indicates an expected call of GetSiteQuestion. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteQuestion(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteQuestion", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteQuestion), ctx) } // GetSiteSecurity mocks base method. func (m *MockSiteInfoCommonService) GetSiteSecurity(ctx context.Context) (*schema.SiteSecurityResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteSecurity", ctx) ret0, _ := ret[0].(*schema.SiteSecurityResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteSecurity indicates an expected call of GetSiteSecurity. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteSecurity(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteSecurity", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteSecurity), ctx) } // GetSiteSeo mocks base method. func (m *MockSiteInfoCommonService) GetSiteSeo(ctx context.Context) (*schema.SiteSeoResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteSeo", ctx) ret0, _ := ret[0].(*schema.SiteSeoResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteSeo indicates an expected call of GetSiteSeo. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteSeo(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteSeo", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteSeo), ctx) } // GetSiteTag mocks base method. func (m *MockSiteInfoCommonService) GetSiteTag(ctx context.Context) (*schema.SiteTagsResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteTag", ctx) ret0, _ := ret[0].(*schema.SiteTagsResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteTag indicates an expected call of GetSiteTag. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteTag(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteTag", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteTag), ctx) } // GetSiteTheme mocks base method. func (m *MockSiteInfoCommonService) GetSiteTheme(ctx context.Context) (*schema.SiteThemeResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteTheme", ctx) ret0, _ := ret[0].(*schema.SiteThemeResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteTheme indicates an expected call of GetSiteTheme. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteTheme(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteTheme", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteTheme), ctx) } // GetSiteUsers mocks base method. func (m *MockSiteInfoCommonService) GetSiteUsers(ctx context.Context) (*schema.SiteUsersResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteUsers", ctx) ret0, _ := ret[0].(*schema.SiteUsersResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteUsers indicates an expected call of GetSiteUsers. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteUsers(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteUsers", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteUsers), ctx) } // GetSiteUsersSettings mocks base method. func (m *MockSiteInfoCommonService) GetSiteUsersSettings(ctx context.Context) (*schema.SiteUsersSettingsResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteUsersSettings", ctx) ret0, _ := ret[0].(*schema.SiteUsersSettingsResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteUsersSettings indicates an expected call of GetSiteUsersSettings. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteUsersSettings(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteUsersSettings", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteUsersSettings), ctx) } // GetSiteWrite mocks base method. func (m *MockSiteInfoCommonService) GetSiteWrite(ctx context.Context) (*schema.SiteWriteResp, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSiteWrite", ctx) ret0, _ := ret[0].(*schema.SiteWriteResp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSiteWrite indicates an expected call of GetSiteWrite. func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteWrite(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteWrite", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteWrite), ctx) } // IsBrandingFileUsed mocks base method. func (m *MockSiteInfoCommonService) IsBrandingFileUsed(ctx context.Context, filePath string) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsBrandingFileUsed", ctx, filePath) ret0, _ := ret[0].(bool) return ret0 } // IsBrandingFileUsed indicates an expected call of IsBrandingFileUsed. func (mr *MockSiteInfoCommonServiceMockRecorder) IsBrandingFileUsed(ctx, filePath any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBrandingFileUsed", reflect.TypeOf((*MockSiteInfoCommonService)(nil).IsBrandingFileUsed), ctx, filePath) } ================================================ FILE: internal/service/noticequeue/notice_queue.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package noticequeue import ( "github.com/apache/answer/internal/base/queue" "github.com/apache/answer/internal/schema" ) type Service queue.Service[*schema.NotificationMsg] func NewService() Service { return queue.New[*schema.NotificationMsg]("notification", 128) } type ExternalService queue.Service[*schema.ExternalNotificationMsg] func NewExternalService() ExternalService { return queue.New[*schema.ExternalNotificationMsg]("external_notification", 128) } ================================================ FILE: internal/service/notification/external_notification.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package notification import ( "context" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/siteinfo_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/internal/service/user_external_login" "github.com/apache/answer/internal/service/user_notification_config" "github.com/segmentfault/pacman/log" ) type ExternalNotificationService struct { data *data.Data userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo followRepo activity_common.FollowRepo emailService *export.EmailService userRepo usercommon.UserRepo notificationQueueService noticequeue.ExternalService userExternalLoginRepo user_external_login.UserExternalLoginRepo siteInfoService siteinfo_common.SiteInfoCommonService } func NewExternalNotificationService( data *data.Data, userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo, followRepo activity_common.FollowRepo, emailService *export.EmailService, userRepo usercommon.UserRepo, notificationQueueService noticequeue.ExternalService, userExternalLoginRepo user_external_login.UserExternalLoginRepo, siteInfoService siteinfo_common.SiteInfoCommonService, ) *ExternalNotificationService { n := &ExternalNotificationService{ data: data, userNotificationConfigRepo: userNotificationConfigRepo, followRepo: followRepo, emailService: emailService, userRepo: userRepo, notificationQueueService: notificationQueueService, userExternalLoginRepo: userExternalLoginRepo, siteInfoService: siteInfoService, } notificationQueueService.RegisterHandler(n.Handler) return n } func (ns *ExternalNotificationService) Handler(ctx context.Context, msg *schema.ExternalNotificationMsg) error { log.Debugf("try to send external notification %+v", msg) // If receiver not set language, use site default language. if len(msg.ReceiverLang) == 0 || msg.ReceiverLang == translator.DefaultLangOption { if interfaceInfo, _ := ns.siteInfoService.GetSiteInterface(ctx); interfaceInfo != nil { msg.ReceiverLang = interfaceInfo.Language } } if msg.NewQuestionTemplateRawData != nil { return ns.handleNewQuestionNotification(ctx, msg) } if msg.NewCommentTemplateRawData != nil { return ns.handleNewCommentNotification(ctx, msg) } if msg.NewAnswerTemplateRawData != nil { return ns.handleNewAnswerNotification(ctx, msg) } if msg.NewInviteAnswerTemplateRawData != nil { return ns.handleInviteAnswerNotification(ctx, msg) } log.Errorf("unknown notification message: %+v", msg) return nil } func (ns *ExternalNotificationService) checkUserStatusBeforeNotification(ctx context.Context, userID string) ( unavailable bool) { userInfo, exist, err := ns.userRepo.GetByUserID(ctx, userID) if err != nil { log.Errorf("get user %s info error: %v", userID, err) return true } if !exist || userInfo.Status != entity.UserStatusAvailable { return true } return false } ================================================ FILE: internal/service/notification/invite_answer_notification.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package notification import ( "context" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" ) func (ns *ExternalNotificationService) handleInviteAnswerNotification(ctx context.Context, msg *schema.ExternalNotificationMsg) error { log.Debugf("try to send invite answer notification %+v", msg) notificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource) if err != nil { return err } if !exist { return nil } channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels) for _, channel := range channels { if !channel.Enable { continue } if channel.Key == constant.EmailChannel { ns.sendInviteAnswerNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewInviteAnswerTemplateRawData) } } return nil } func (ns *ExternalNotificationService) sendInviteAnswerNotificationEmail(ctx context.Context, userID, email, lang string, rawData *schema.NewInviteAnswerTemplateRawData) { if unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable { return } codeContent := &schema.EmailCodeContent{ SourceType: schema.UnsubscribeSourceType, NotificationSources: []constant.NotificationSource{ constant.InboxSource, }, Email: email, UserID: userID, SkipValidationLatestCode: true, } // If receiver has set language, use it to send email. if len(lang) > 0 { ctx = context.WithValue(ctx, constant.AcceptLanguageContextKey, i18n.Language(lang)) } title, body, err := ns.emailService.NewInviteAnswerTemplate(ctx, rawData) if err != nil { log.Error(err) return } ns.emailService.SendAndSaveCodeWithTime( ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) } ================================================ FILE: internal/service/notification/new_answer_notification.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package notification import ( "context" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" ) func (ns *ExternalNotificationService) handleNewAnswerNotification(ctx context.Context, msg *schema.ExternalNotificationMsg) error { log.Debugf("try to send new comment notification %+v", msg) notificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource) if err != nil { return err } if !exist { return nil } channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels) for _, channel := range channels { if !channel.Enable { continue } if channel.Key == constant.EmailChannel { ns.sendNewAnswerNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewAnswerTemplateRawData) } } return nil } func (ns *ExternalNotificationService) sendNewAnswerNotificationEmail(ctx context.Context, userID, email, lang string, rawData *schema.NewAnswerTemplateRawData) { if unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable { return } codeContent := &schema.EmailCodeContent{ SourceType: schema.UnsubscribeSourceType, NotificationSources: []constant.NotificationSource{ constant.InboxSource, }, Email: email, UserID: userID, SkipValidationLatestCode: true, } // If receiver has set language, use it to send email. if len(lang) > 0 { ctx = context.WithValue(ctx, constant.AcceptLanguageContextKey, i18n.Language(lang)) } title, body, err := ns.emailService.NewAnswerTemplate(ctx, rawData) if err != nil { log.Error(err) return } ns.emailService.SendAndSaveCodeWithTime( ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) } ================================================ FILE: internal/service/notification/new_comment_notification.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package notification import ( "context" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" ) func (ns *ExternalNotificationService) handleNewCommentNotification(ctx context.Context, msg *schema.ExternalNotificationMsg) error { log.Debugf("try to send new comment notification %+v", msg) notificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource) if err != nil { return err } if !exist { return nil } channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels) for _, channel := range channels { if !channel.Enable { continue } if channel.Key == constant.EmailChannel { ns.sendNewCommentNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewCommentTemplateRawData) } } return nil } func (ns *ExternalNotificationService) sendNewCommentNotificationEmail(ctx context.Context, userID, email, lang string, rawData *schema.NewCommentTemplateRawData) { if unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable { return } codeContent := &schema.EmailCodeContent{ SourceType: schema.UnsubscribeSourceType, NotificationSources: []constant.NotificationSource{ constant.InboxSource, }, Email: email, UserID: userID, SkipValidationLatestCode: true, } // If receiver has set language, use it to send email. if len(lang) > 0 { ctx = context.WithValue(ctx, constant.AcceptLanguageContextKey, i18n.Language(lang)) } title, body, err := ns.emailService.NewCommentTemplate(ctx, rawData) if err != nil { log.Error(err) return } ns.emailService.SendAndSaveCodeWithTime( ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) } ================================================ FILE: internal/service/notification/new_question_notification.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package notification import ( "context" "strings" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/schema" "github.com/apache/answer/pkg/display" "github.com/apache/answer/pkg/token" "github.com/apache/answer/plugin" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" ) type NewQuestionSubscriber struct { UserID string `json:"user_id"` Channels schema.NotificationChannels `json:"channels"` NotificationSource constant.NotificationSource `json:"notification_source"` } func (ns *ExternalNotificationService) handleNewQuestionNotification(ctx context.Context, msg *schema.ExternalNotificationMsg) error { log.Debugf("try to send new question notification %+v", msg) subscribers, err := ns.getNewQuestionSubscribers(ctx, msg) if err != nil { return err } log.Debugf("get subscribers %d for question %s", len(subscribers), msg.NewQuestionTemplateRawData.QuestionID) for _, subscriber := range subscribers { for _, channel := range subscriber.Channels { if !channel.Enable { continue } if channel.Key == constant.EmailChannel { ns.sendNewQuestionNotificationEmail(ctx, subscriber.UserID, &schema.NewQuestionTemplateRawData{ QuestionTitle: msg.NewQuestionTemplateRawData.QuestionTitle, QuestionID: msg.NewQuestionTemplateRawData.QuestionID, UnsubscribeCode: token.GenerateToken(), Tags: msg.NewQuestionTemplateRawData.Tags, TagIDs: msg.NewQuestionTemplateRawData.TagIDs, }) } } } ns.syncNewQuestionNotificationToPlugin(ctx, msg) return nil } func (ns *ExternalNotificationService) getNewQuestionSubscribers(ctx context.Context, msg *schema.ExternalNotificationMsg) ( subscribers []*NewQuestionSubscriber, err error) { subscribersMapping := make(map[string]*NewQuestionSubscriber) // 1. get all this new question's tags followers tagsFollowerIDs := make([]string, 0) followerMapping := make(map[string]bool) for _, tagID := range msg.NewQuestionTemplateRawData.TagIDs { userIDs, err := ns.followRepo.GetFollowUserIDs(ctx, tagID) if err != nil { log.Error(err) continue } for _, userID := range userIDs { if _, ok := followerMapping[userID]; ok { continue } followerMapping[userID] = true tagsFollowerIDs = append(tagsFollowerIDs, userID) } } userNotificationConfigs, err := ns.userNotificationConfigRepo.GetByUsersAndSource( ctx, tagsFollowerIDs, constant.AllNewQuestionForFollowingTagsSource) if err != nil { return nil, err } for _, userNotificationConfig := range userNotificationConfigs { if _, ok := subscribersMapping[userNotificationConfig.UserID]; ok { continue } subscribersMapping[userNotificationConfig.UserID] = &NewQuestionSubscriber{ UserID: userNotificationConfig.UserID, Channels: schema.NewNotificationChannelsFormJson(userNotificationConfig.Channels), NotificationSource: constant.AllNewQuestionForFollowingTagsSource, } } log.Debugf("get %d subscribers from tags", len(subscribersMapping)) // 2. get all new question's followers notificationConfigs, err := ns.userNotificationConfigRepo.GetBySource(ctx, constant.AllNewQuestionSource) if err != nil { return nil, err } for _, notificationConfig := range notificationConfigs { if _, ok := subscribersMapping[notificationConfig.UserID]; ok { continue } if ns.checkSendNewQuestionNotificationEmailLimit(ctx, notificationConfig.UserID) { continue } subscribersMapping[notificationConfig.UserID] = &NewQuestionSubscriber{ UserID: notificationConfig.UserID, Channels: schema.NewNotificationChannelsFormJson(notificationConfig.Channels), NotificationSource: constant.AllNewQuestionSource, } } // 3. remove question owner delete(subscribersMapping, msg.NewQuestionTemplateRawData.QuestionAuthorUserID) for _, subscriber := range subscribersMapping { subscribers = append(subscribers, subscriber) } log.Debugf("get %d subscribers from all new question config", len(subscribers)) return subscribers, nil } func (ns *ExternalNotificationService) checkSendNewQuestionNotificationEmailLimit(ctx context.Context, userID string) bool { key := constant.NewQuestionNotificationLimitCacheKeyPrefix + userID old, exist, err := ns.data.Cache.GetInt64(ctx, key) if err != nil { log.Error(err) return false } if exist && old >= constant.NewQuestionNotificationLimitMax { log.Debugf("%s user reach new question notification limit", userID) return true } if !exist { err = ns.data.Cache.SetInt64(ctx, key, 1, constant.NewQuestionNotificationLimitCacheTime) } else { _, err = ns.data.Cache.Increase(ctx, key, 1) } if err != nil { log.Error(err) } return false } func (ns *ExternalNotificationService) sendNewQuestionNotificationEmail(ctx context.Context, userID string, rawData *schema.NewQuestionTemplateRawData) { if unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable { return } userInfo, exist, err := ns.userRepo.GetByUserID(ctx, userID) if err != nil { log.Error(err) return } if !exist { log.Errorf("user %s not exist", userID) return } // If receiver has set language, use it to send email. if len(userInfo.Language) > 0 { ctx = context.WithValue(ctx, constant.AcceptLanguageContextKey, i18n.Language(userInfo.Language)) } title, body, err := ns.emailService.NewQuestionTemplate(ctx, rawData) if err != nil { log.Error(err) return } codeContent := &schema.EmailCodeContent{ SourceType: schema.UnsubscribeSourceType, Email: userInfo.EMail, UserID: userID, NotificationSources: []constant.NotificationSource{ constant.AllNewQuestionSource, constant.AllNewQuestionForFollowingTagsSource, }, SkipValidationLatestCode: true, } ns.emailService.SendAndSaveCodeWithTime( ctx, userInfo.ID, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) } func (ns *ExternalNotificationService) syncNewQuestionNotificationToPlugin(ctx context.Context, msg *schema.ExternalNotificationMsg) { _ = plugin.CallNotification(func(fn plugin.Notification) error { // 1. get all this new question's tags followers subscribersMapping := make(map[string]plugin.NotificationType) for _, tagID := range msg.NewQuestionTemplateRawData.TagIDs { userIDs, err := ns.followRepo.GetFollowUserIDs(ctx, tagID) if err != nil { log.Error(err) continue } for _, userID := range userIDs { subscribersMapping[userID] = plugin.NotificationNewQuestionFollowedTag } } // 2. get all new question's followers questionSubscribers := fn.GetNewQuestionSubscribers() for _, subscriber := range questionSubscribers { subscribersMapping[subscriber] = plugin.NotificationNewQuestion } // 3. remove question owner delete(subscribersMapping, msg.NewQuestionTemplateRawData.QuestionAuthorUserID) pluginNotificationMsg := ns.newPluginQuestionNotification(ctx, msg) // 4. send notification for subscriberUserID, notificationType := range subscribersMapping { newMsg := plugin.NotificationMessage{} _ = copier.Copy(&newMsg, pluginNotificationMsg) newMsg.ReceiverUserID = subscriberUserID newMsg.Type = notificationType if len(subscriberUserID) > 0 { userInfo, _, _ := ns.userRepo.GetByUserID(ctx, subscriberUserID) if userInfo != nil && len(userInfo.Language) > 0 && userInfo.Language != translator.DefaultLangOption { newMsg.ReceiverLang = userInfo.Language } } // Get all external logins as fallback externalLogins, err := ns.userExternalLoginRepo.GetUserExternalLoginList(ctx, subscriberUserID) if err != nil { log.Errorf("get user external login list failed for user %s: %v", subscriberUserID, err) } else if len(externalLogins) > 0 { newMsg.ReceiverExternalID = externalLogins[0].ExternalID if len(externalLogins) > 1 { log.Debugf("user %s has %d SSO logins, using most recent: provider=%s", subscriberUserID, len(externalLogins), externalLogins[0].Provider) } } // Try to get external login specific to this plugin (takes precedence over fallback) userInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, subscriberUserID) if err != nil { log.Errorf("get user external login info failed: %v", err) return nil } if exist { newMsg.ReceiverExternalID = userInfo.ExternalID } fn.Notify(newMsg) } return nil }) } func (ns *ExternalNotificationService) newPluginQuestionNotification( ctx context.Context, msg *schema.ExternalNotificationMsg) (raw *plugin.NotificationMessage) { raw = &plugin.NotificationMessage{ ReceiverUserID: msg.ReceiverUserID, ReceiverLang: msg.ReceiverLang, QuestionTitle: msg.NewQuestionTemplateRawData.QuestionTitle, QuestionTags: strings.Join(msg.NewQuestionTemplateRawData.Tags, ","), } siteInfo, err := ns.siteInfoService.GetSiteGeneral(ctx) if err != nil { return raw } seoInfo, err := ns.siteInfoService.GetSiteSeo(ctx) if err != nil { return raw } interfaceInfo, err := ns.siteInfoService.GetSiteInterface(ctx) if err != nil { return raw } if len(raw.ReceiverLang) == 0 || raw.ReceiverLang == translator.DefaultLangOption { raw.ReceiverLang = interfaceInfo.Language } raw.QuestionUrl = display.QuestionURL( seoInfo.Permalink, siteInfo.SiteUrl, msg.NewQuestionTemplateRawData.QuestionID, msg.NewQuestionTemplateRawData.QuestionTitle) if len(msg.NewQuestionTemplateRawData.QuestionAuthorUserID) > 0 { triggerUser, exist, err := ns.userRepo.GetByUserID(ctx, msg.NewQuestionTemplateRawData.QuestionAuthorUserID) if err != nil { log.Errorf("get trigger user basic info failed: %v", err) return } if exist { raw.TriggerUserID = triggerUser.ID raw.TriggerUserDisplayName = triggerUser.DisplayName raw.TriggerUserUrl = display.UserURL(siteInfo.SiteUrl, triggerUser.Username) } } return raw } ================================================ FILE: internal/service/notification/notification_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package notification import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/badge" notficationcommon "github.com/apache/answer/internal/service/notification_common" "github.com/apache/answer/internal/service/report_common" "github.com/apache/answer/internal/service/review" "github.com/apache/answer/internal/service/revision_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/log" ) // NotificationService user service type NotificationService struct { data *data.Data notificationRepo notficationcommon.NotificationRepo notificationCommon *notficationcommon.NotificationCommon revisionService *revision_common.RevisionService reportRepo report_common.ReportRepo reviewService *review.ReviewService userRepo usercommon.UserRepo badgeRepo badge.BadgeRepo } func NewNotificationService( data *data.Data, notificationRepo notficationcommon.NotificationRepo, notificationCommon *notficationcommon.NotificationCommon, revisionService *revision_common.RevisionService, userRepo usercommon.UserRepo, reportRepo report_common.ReportRepo, reviewService *review.ReviewService, badgeRepo badge.BadgeRepo, ) *NotificationService { return &NotificationService{ data: data, notificationRepo: notificationRepo, notificationCommon: notificationCommon, revisionService: revisionService, userRepo: userRepo, reportRepo: reportRepo, reviewService: reviewService, badgeRepo: badgeRepo, } } func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRedDot) (resp *schema.RedDot, err error) { inboxKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, req.UserID) achievementKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, req.UserID) redBot := &schema.RedDot{} redBot.Inbox, _, _ = ns.data.Cache.GetInt64(ctx, inboxKey) redBot.Achievement, _, _ = ns.data.Cache.GetInt64(ctx, achievementKey) // get review amount if req.CanReviewAnswer || req.CanReviewQuestion || req.CanReviewTag { redBot.CanRevision = true redBot.Revision = ns.countAllReviewAmount(ctx, req) } // get badge award redBot.BadgeAward = ns.getBadgeAward(ctx, req.UserID) return redBot, nil } func (ns *NotificationService) getBadgeAward(ctx context.Context, userID string) (badgeAward *schema.RedDotBadgeAward) { key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) cacheData, exist, err := ns.data.Cache.GetString(ctx, key) if err != nil { log.Errorf("get badge award failed: %v", err) return nil } if !exist { return nil } c := schema.NewRedDotBadgeAwardCache() c.FromJSON(cacheData) award := c.GetBadgeAward() if award == nil { return nil } badgeInfo, exists, err := ns.badgeRepo.GetByID(ctx, award.BadgeID) if err != nil { log.Errorf("get badge info failed: %v", err) return nil } if !exists { return nil } award.Name = translator.Tr(handler.GetLangByCtx(ctx), badgeInfo.Name) award.Icon = badgeInfo.Icon award.Level = badgeInfo.Level return award } func (ns *NotificationService) countAllReviewAmount(ctx context.Context, req *schema.GetRedDot) (amount int64) { // get queue amount if req.IsAdmin { reviewCount, err := ns.reviewService.GetReviewPendingCount(ctx) if err != nil { log.Errorf("get report count failed: %v", err) } else { amount += reviewCount } } // get flag amount if req.IsAdmin { reportCount, err := ns.reportRepo.GetReportCount(ctx) if err != nil { log.Errorf("get report count failed: %v", err) } else { amount += reportCount } } // get suggestion amount countUnreviewedRevision, err := ns.revisionService.GetUnreviewedRevisionCount(ctx, &schema.RevisionSearch{ CanReviewQuestion: req.CanReviewQuestion, CanReviewAnswer: req.CanReviewAnswer, CanReviewTag: req.CanReviewTag, UserID: req.UserID, }) if err != nil { log.Errorf("get unreviewed revision count failed: %v", err) } else { amount += countUnreviewedRevision } return amount } func (ns *NotificationService) ClearRedDot(ctx context.Context, req *schema.NotificationClearRequest) (*schema.RedDot, error) { _ = ns.notificationCommon.DeleteRedDot(ctx, req.UserID, schema.NotificationType[req.NotificationType]) resp := &schema.GetRedDot{} _ = copier.Copy(resp, req) return ns.GetRedDot(ctx, resp) } func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, notificationType string) error { botType, ok := schema.NotificationType[notificationType] if ok { err := ns.notificationRepo.ClearUnRead(ctx, userID, botType) if err != nil { return err } } return nil } func (ns *NotificationService) ClearIDUnRead(ctx context.Context, userID string, id string) error { notificationInfo, exist, err := ns.notificationRepo.GetById(ctx, id) if err != nil { log.Errorf("get notification failed: %v", err) return nil } if !exist || notificationInfo.UserID != userID { return nil } if notificationInfo.IsRead == schema.NotificationNotRead { err := ns.notificationRepo.ClearIDUnRead(ctx, userID, id) if err != nil { return err } } err = ns.notificationCommon.RemoveBadgeAwardAlertCache(ctx, userID, id) if err != nil { log.Errorf("remove badge award alert cache failed: %v", err) } _ = ns.notificationCommon.DecreaseRedDot(ctx, userID, notificationInfo.Type) return nil } func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCond *schema.NotificationSearch) ( pageModel *pager.PageModel, err error) { resp := make([]*schema.NotificationContent, 0) searchType, ok := schema.NotificationType[searchCond.TypeStr] if !ok { return pager.NewPageModel(0, resp), nil } searchInboxType := schema.NotificationInboxTypeAll if searchType == schema.NotificationTypeInbox { _, ok = schema.NotificationInboxType[searchCond.InboxTypeStr] if ok { searchInboxType = schema.NotificationInboxType[searchCond.InboxTypeStr] } } searchCond.Type = searchType searchCond.InboxType = searchInboxType notifications, total, err := ns.notificationRepo.GetNotificationPage(ctx, searchCond) if err != nil { return nil, err } resp = ns.formatNotificationPage(ctx, notifications) return pager.NewPageModel(total, resp), nil } func (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) ( resp []*schema.NotificationContent) { lang := handler.GetLangByCtx(ctx) enableShortID := handler.GetEnableShortID(ctx) userIDs := make([]string, 0) userMapping := make(map[string]bool) for _, notificationInfo := range notifications { item := &schema.NotificationContent{} if err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil { log.Error("NotificationContent Unmarshal Error", err.Error()) continue } // If notification is downvote, the user info is not needed. if item.NotificationAction == constant.NotificationDownVotedTheQuestion || item.NotificationAction == constant.NotificationDownVotedTheAnswer { item.UserInfo = nil } // If notification is badge, the user info is not needed and the title need to be translated. if item.ObjectInfo.ObjectType == constant.BadgeAwardObjectType { badgeName := translator.Tr(lang, item.ObjectInfo.Title) item.ObjectInfo.Title = translator.TrWithData(lang, constant.NotificationEarnedBadge, struct { BadgeName string }{BadgeName: badgeName}) item.UserInfo = nil } item.ID = notificationInfo.ID item.NotificationAction = translator.Tr(lang, item.NotificationAction) item.UpdateTime = notificationInfo.UpdatedAt.Unix() item.IsRead = notificationInfo.IsRead == schema.NotificationRead if enableShortID { if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok { if item.ObjectInfo.ObjectID == answerID { item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) } item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) } if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok { if item.ObjectInfo.ObjectID == questionID { item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"]) } item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"]) } } if item.UserInfo != nil && !userMapping[item.UserInfo.ID] { userIDs = append(userIDs, item.UserInfo.ID) userMapping[item.UserInfo.ID] = true } resp = append(resp, item) } if len(userIDs) == 0 { return resp } users, err := ns.userRepo.BatchGetByID(ctx, userIDs) if err != nil { log.Error(err) return resp } userIDMapping := make(map[string]*entity.User, len(users)) for _, user := range users { userIDMapping[user.ID] = user } for _, item := range resp { if item.UserInfo == nil { continue } userInfo, ok := userIDMapping[item.UserInfo.ID] if !ok { continue } if userInfo.Status == entity.UserStatusDeleted { item.UserInfo = &schema.UserBasicInfo{ DisplayName: "user" + converter.DeleteUserDisplay(userInfo.ID), Status: constant.UserDeleted, } } } return resp } ================================================ FILE: internal/service/notification_common/notification.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package notificationcommon import ( "context" "fmt" "time" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/internal/service/user_external_login" "github.com/apache/answer/pkg/display" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/object_info" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/plugin" "github.com/goccy/go-json" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) type NotificationRepo interface { AddNotification(ctx context.Context, notification *entity.Notification) (err error) GetNotificationPage(ctx context.Context, search *schema.NotificationSearch) ([]*entity.Notification, int64, error) ClearUnRead(ctx context.Context, userID string, notificationType int) (err error) ClearIDUnRead(ctx context.Context, userID string, id string) (err error) GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error) UpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error) GetById(ctx context.Context, id string) (*entity.Notification, bool, error) CountNotificationByUser(ctx context.Context, cond *entity.Notification) (int64, error) DeleteNotification(ctx context.Context, userID string) (err error) DeleteUserNotificationConfig(ctx context.Context, userID string) (err error) } type NotificationCommon struct { data *data.Data notificationRepo NotificationRepo activityRepo activity_common.ActivityRepo followRepo activity_common.FollowRepo userCommon *usercommon.UserCommon objectInfoService *object_info.ObjService notificationQueueService noticequeue.Service userExternalLoginRepo user_external_login.UserExternalLoginRepo siteInfoService siteinfo_common.SiteInfoCommonService } func NewNotificationCommon( data *data.Data, notificationRepo NotificationRepo, userCommon *usercommon.UserCommon, activityRepo activity_common.ActivityRepo, followRepo activity_common.FollowRepo, objectInfoService *object_info.ObjService, notificationQueueService noticequeue.Service, userExternalLoginRepo user_external_login.UserExternalLoginRepo, siteInfoService siteinfo_common.SiteInfoCommonService, ) *NotificationCommon { notification := &NotificationCommon{ data: data, notificationRepo: notificationRepo, activityRepo: activityRepo, followRepo: followRepo, userCommon: userCommon, objectInfoService: objectInfoService, notificationQueueService: notificationQueueService, userExternalLoginRepo: userExternalLoginRepo, siteInfoService: siteInfoService, } notificationQueueService.RegisterHandler(notification.AddNotification) return notification } // AddNotification // need set // LoginUserID // Type 1 inbox 2 achievement // [inbox] Activity // [achievement] Rank // ObjectInfo.Title // ObjectInfo.ObjectID // ObjectInfo.ObjectType func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) (err error) { if msg.Type == schema.NotificationTypeAchievement && plugin.RankAgentEnabled() { return nil } req := &schema.NotificationContent{ TriggerUserID: msg.TriggerUserID, ReceiverUserID: msg.ReceiverUserID, ObjectInfo: schema.ObjectInfo{ Title: msg.Title, ObjectID: uid.DeShortID(msg.ObjectID), ObjectType: msg.ObjectType, }, NotificationAction: msg.NotificationAction, Type: msg.Type, } var questionID string // just for notify all followers var objInfo *schema.SimpleObjectInfo if msg.ObjectType == constant.BadgeAwardObjectType { req.ObjectInfo.Title = msg.Title objectMap := make(map[string]string) objectMap["badge_id"] = msg.ExtraInfo["badge_id"] req.ObjectInfo.ObjectMap = objectMap } else { objInfo, err = ns.objectInfoService.GetInfo(ctx, req.ObjectInfo.ObjectID) if err != nil { log.Error(err) return err } else { req.ObjectInfo.Title = objInfo.Title questionID = objInfo.QuestionID objectMap := make(map[string]string) objectMap["question"] = uid.DeShortID(objInfo.QuestionID) objectMap["answer"] = uid.DeShortID(objInfo.AnswerID) objectMap["comment"] = objInfo.CommentID req.ObjectInfo.ObjectMap = objectMap } } if msg.Type == schema.NotificationTypeAchievement { notificationInfo, exist, err := ns.notificationRepo.GetByUserIdObjectIdTypeId(ctx, req.ReceiverUserID, req.ObjectInfo.ObjectID, req.Type) if err != nil { return fmt.Errorf("get by user id object id type id error: %w", err) } rank, err := ns.activityRepo.GetUserIDObjectIDActivitySum(ctx, req.ReceiverUserID, req.ObjectInfo.ObjectID) if err != nil { return fmt.Errorf("get user id object id activity sum error: %w", err) } req.Rank = rank if exist { // modify notification updateContent := &schema.NotificationContent{} err := json.Unmarshal([]byte(notificationInfo.Content), updateContent) if err != nil { return fmt.Errorf("unmarshal notification content error: %w", err) } updateContent.Rank = rank content, _ := json.Marshal(updateContent) notificationInfo.Content = string(content) err = ns.notificationRepo.UpdateNotificationContent(ctx, notificationInfo) if err != nil { return fmt.Errorf("update notification content error: %w", err) } return nil } } info := &entity.Notification{} now := time.Now() info.UserID = req.ReceiverUserID info.Type = req.Type info.IsRead = schema.NotificationNotRead info.Status = schema.NotificationStatusNormal info.CreatedAt = now info.UpdatedAt = now info.ObjectID = req.ObjectInfo.ObjectID userBasicInfo, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, req.TriggerUserID) if err != nil { return fmt.Errorf("get user basic info error: %w", err) } if !exist { return fmt.Errorf("user not exist: %s", req.TriggerUserID) } req.UserInfo = userBasicInfo content, _ := json.Marshal(req) _, ok := constant.NotificationMsgTypeMapping[req.NotificationAction] if ok { info.MsgType = constant.NotificationMsgTypeMapping[req.NotificationAction] } info.Content = string(content) err = ns.notificationRepo.AddNotification(ctx, info) if err != nil { return fmt.Errorf("add notification error: %w", err) } err = ns.addRedDot(ctx, info.UserID, msg.Type) if err != nil { log.Error("addRedDot Error", err.Error()) } if req.ObjectInfo.ObjectType == constant.BadgeAwardObjectType { err = ns.AddBadgeAwardAlertCache(ctx, info.UserID, info.ID, req.ObjectInfo.ObjectMap["badge_id"]) if err != nil { log.Error("AddBadgeAwardAlertCache Error", err.Error()) } } go ns.SendNotificationToAllFollower(ctx, msg, questionID) if msg.Type == schema.NotificationTypeInbox { ns.syncNotificationToPlugin(ctx, objInfo, msg) } return nil } func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, noticeType int) error { var key string if noticeType == schema.NotificationTypeInbox { key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID) } else { key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) } _, exist, err := ns.data.Cache.GetInt64(ctx, key) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } if exist { if _, err := ns.data.Cache.Increase(ctx, key, 1); err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } return nil } err = ns.data.Cache.SetInt64(ctx, key, 1, constant.RedDotCacheTime) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } return nil } func (ns *NotificationCommon) DecreaseRedDot(ctx context.Context, userID string, notificationType int) error { var key string if notificationType == schema.NotificationTypeInbox { key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID) } else { key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) } _, exist, err := ns.data.Cache.GetInt64(ctx, key) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } if !exist { return nil } res, err := ns.data.Cache.Decrease(ctx, key, 1) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } if res <= 0 { return ns.DeleteRedDot(ctx, userID, notificationType) } return nil } func (ns *NotificationCommon) DeleteRedDot(ctx context.Context, userID string, notificationType int) error { var key string if notificationType == schema.NotificationTypeInbox { key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID) } else { key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) } err := ns.data.Cache.Del(ctx, key) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } return nil } // AddBadgeAwardAlertCache add badge award alert cache func (ns *NotificationCommon) AddBadgeAwardAlertCache(ctx context.Context, userID, notificationID, badgeID string) (err error) { key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) cacheData, exist, err := ns.data.Cache.GetString(ctx, key) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } if !exist { c := schema.NewRedDotBadgeAwardCache() c.AddBadgeAward(&schema.RedDotBadgeAward{ NotificationID: notificationID, BadgeID: badgeID, }) return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) } c := schema.NewRedDotBadgeAwardCache() c.FromJSON(cacheData) c.AddBadgeAward(&schema.RedDotBadgeAward{ NotificationID: notificationID, BadgeID: badgeID, }) return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) } // RemoveBadgeAwardAlertCache remove badge award alert cache func (ns *NotificationCommon) RemoveBadgeAwardAlertCache(ctx context.Context, userID, notificationID string) (err error) { key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) cacheData, exist, err := ns.data.Cache.GetString(ctx, key) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } if !exist { return nil } c := schema.NewRedDotBadgeAwardCache() c.FromJSON(cacheData) c.RemoveBadgeAward(notificationID) if len(c.BadgeAwardList) == 0 { return ns.data.Cache.Del(ctx, key) } return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) } // SendNotificationToAllFollower send notification to all followers func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context, msg *schema.NotificationMsg, questionID string) { if msg.NoNeedPushAllFollow || len(questionID) == 0 { return } if msg.NotificationAction != constant.NotificationUpdateQuestion && msg.NotificationAction != constant.NotificationAnswerTheQuestion && msg.NotificationAction != constant.NotificationUpdateAnswer && msg.NotificationAction != constant.NotificationAcceptAnswer { return } condObjectID := msg.ObjectID if len(questionID) > 0 { condObjectID = uid.DeShortID(questionID) } userIDs, err := ns.followRepo.GetFollowUserIDs(ctx, condObjectID) if err != nil { log.Error(err) return } log.Infof("send notification to all followers: %s %d", condObjectID, len(userIDs)) for _, userID := range userIDs { t := &schema.NotificationMsg{} _ = copier.Copy(t, msg) t.ReceiverUserID = userID t.TriggerUserID = msg.TriggerUserID t.NoNeedPushAllFollow = true ns.notificationQueueService.Send(ctx, t) } } func (ns *NotificationCommon) syncNotificationToPlugin(ctx context.Context, objInfo *schema.SimpleObjectInfo, msg *schema.NotificationMsg) { if objInfo == nil { return } siteInfo, err := ns.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general info failed: %v", err) return } seoInfo, err := ns.siteInfoService.GetSiteSeo(ctx) if err != nil { log.Errorf("get site seo info failed: %v", err) return } interfaceInfo, err := ns.siteInfoService.GetSiteInterface(ctx) if err != nil { log.Errorf("get site interface info failed: %v", err) return } objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID) objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID) pluginNotificationMsg := plugin.NotificationMessage{ Type: plugin.NotificationType(msg.NotificationAction), ReceiverUserID: msg.ReceiverUserID, TriggerUserID: msg.TriggerUserID, QuestionTitle: objInfo.Title, } if len(objInfo.QuestionID) > 0 { pluginNotificationMsg.QuestionUrl = display.QuestionURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title) } if len(objInfo.AnswerID) > 0 { pluginNotificationMsg.AnswerUrl = display.AnswerURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID) } if len(objInfo.CommentID) > 0 { pluginNotificationMsg.CommentUrl = display.CommentURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID, objInfo.CommentID) } if len(msg.TriggerUserID) > 0 { triggerUser, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, msg.TriggerUserID) if err != nil { log.Errorf("get trigger user basic info failed: %v", err) return } if exist { pluginNotificationMsg.TriggerUserID = triggerUser.ID pluginNotificationMsg.TriggerUserDisplayName = triggerUser.DisplayName pluginNotificationMsg.TriggerUserUrl = display.UserURL(siteInfo.SiteUrl, triggerUser.Username) } } if len(pluginNotificationMsg.ReceiverLang) == 0 && len(msg.ReceiverUserID) > 0 { userInfo, _, _ := ns.userCommon.GetUserBasicInfoByID(ctx, msg.ReceiverUserID) if userInfo != nil { pluginNotificationMsg.ReceiverLang = userInfo.Language } // If receiver not set language, use site default language. if len(pluginNotificationMsg.ReceiverLang) == 0 || pluginNotificationMsg.ReceiverLang == translator.DefaultLangOption { pluginNotificationMsg.ReceiverLang = interfaceInfo.Language } } externalLogins, err := ns.userExternalLoginRepo.GetUserExternalLoginList(ctx, msg.ReceiverUserID) if err != nil { log.Errorf("get user external login list failed for user %s: %v", msg.ReceiverUserID, err) } else if len(externalLogins) > 0 { pluginNotificationMsg.ReceiverExternalID = externalLogins[0].ExternalID } _ = plugin.CallNotification(func(fn plugin.Notification) error { userInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, msg.ReceiverUserID) if err != nil { log.Errorf("get user external login info failed: %v", err) return nil } if exist { pluginNotificationMsg.ReceiverExternalID = userInfo.ExternalID } fn.Notify(pluginNotificationMsg) return nil }) } ================================================ FILE: internal/service/object_info/object_info.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package object_info import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/schema" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/comment_common" questioncommon "github.com/apache/answer/internal/service/question_common" tagcommon "github.com/apache/answer/internal/service/tag_common" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/obj" "github.com/segmentfault/pacman/errors" ) // ObjService user service type ObjService struct { answerRepo answercommon.AnswerRepo questionRepo questioncommon.QuestionRepo commentRepo comment_common.CommentCommonRepo tagRepo tagcommon.TagCommonRepo tagCommon *tagcommon.TagCommonService } // NewObjService new object service func NewObjService( answerRepo answercommon.AnswerRepo, questionRepo questioncommon.QuestionRepo, commentRepo comment_common.CommentCommonRepo, tagRepo tagcommon.TagCommonRepo, tagCommon *tagcommon.TagCommonService, ) *ObjService { return &ObjService{ answerRepo: answerRepo, questionRepo: questionRepo, commentRepo: commentRepo, tagRepo: tagRepo, tagCommon: tagCommon, } } func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID string) (objInfo *schema.UnreviewedRevisionInfoInfo, err error) { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return nil, err } switch objectType { case constant.QuestionObjectType: questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, objectID) if err != nil { return nil, err } if !exist { break } taglist, err := os.tagCommon.GetObjectEntityTag(ctx, objectID) if err != nil { return nil, err } os.tagCommon.TagsFormatRecommendAndReserved(ctx, taglist) tags, err := os.tagCommon.TagFormat(ctx, taglist) if err != nil { return nil, err } objInfo = &schema.UnreviewedRevisionInfoInfo{ CreatedAt: questionInfo.CreatedAt.Unix(), ObjectID: questionInfo.ID, QuestionID: questionInfo.ID, ObjectType: objectType, ObjectCreatorUserID: questionInfo.UserID, Title: questionInfo.Title, Content: questionInfo.OriginalText, Html: questionInfo.ParsedText, AnswerCount: questionInfo.AnswerCount, AnswerAccepted: !checker.IsNotZeroString(questionInfo.AcceptedAnswerID), Tags: tags, Status: questionInfo.Status, ShowStatus: questionInfo.Show, } case constant.AnswerObjectType: answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID) if err != nil { return nil, err } if !exist { break } questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) if err != nil { return nil, err } if !exist { break } objInfo = &schema.UnreviewedRevisionInfoInfo{ CreatedAt: answerInfo.CreatedAt.Unix(), ObjectID: answerInfo.ID, QuestionID: answerInfo.QuestionID, AnswerID: answerInfo.ID, ObjectType: objectType, ObjectCreatorUserID: answerInfo.UserID, Title: questionInfo.Title, Content: answerInfo.OriginalText, Html: answerInfo.ParsedText, Status: answerInfo.Status, AnswerAccepted: questionInfo.AcceptedAnswerID == answerInfo.ID, } case constant.TagObjectType: tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true) if err != nil { return nil, err } if !exist { break } objInfo = &schema.UnreviewedRevisionInfoInfo{ CreatedAt: tagInfo.CreatedAt.Unix(), ObjectID: tagInfo.ID, ObjectType: objectType, Title: tagInfo.SlugName, Content: tagInfo.OriginalText, Html: tagInfo.ParsedText, Status: tagInfo.Status, } case constant.CommentObjectType: commentInfo, exist, err := os.commentRepo.GetCommentWithoutStatus(ctx, objectID) if err != nil { return nil, err } if !exist { break } objInfo = &schema.UnreviewedRevisionInfoInfo{ CreatedAt: commentInfo.CreatedAt.Unix(), ObjectID: commentInfo.ID, CommentID: commentInfo.ID, ObjectType: objectType, ObjectCreatorUserID: commentInfo.UserID, Content: commentInfo.OriginalText, Html: commentInfo.ParsedText, Status: commentInfo.Status, } if len(commentInfo.QuestionID) > 0 { questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, commentInfo.QuestionID) if err != nil { return nil, err } if exist { objInfo.QuestionID = questionInfo.ID } answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, commentInfo.ObjectID) if err != nil { return nil, err } if exist { objInfo.AnswerID = answerInfo.ID } } } if objInfo == nil { err = errors.BadRequest(reason.ObjectNotFound) } return objInfo, err } // GetInfo get object simple information func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *schema.SimpleObjectInfo, err error) { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return nil, err } switch objectType { case constant.QuestionObjectType: questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, objectID) if err != nil { return nil, err } if !exist { break } objInfo = &schema.SimpleObjectInfo{ ObjectID: questionInfo.ID, ObjectCreatorUserID: questionInfo.UserID, QuestionID: questionInfo.ID, QuestionStatus: questionInfo.Status, ObjectType: objectType, Title: questionInfo.Title, Content: questionInfo.ParsedText, // todo trim } case constant.AnswerObjectType: answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID) if err != nil { return nil, err } if !exist { break } questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) if err != nil { return nil, err } if !exist { break } objInfo = &schema.SimpleObjectInfo{ ObjectID: answerInfo.ID, ObjectCreatorUserID: answerInfo.UserID, QuestionID: answerInfo.QuestionID, QuestionStatus: questionInfo.Status, AnswerStatus: answerInfo.Status, AnswerID: answerInfo.ID, ObjectType: objectType, Title: questionInfo.Title, // this should be question title Content: answerInfo.ParsedText, // todo trim } case constant.CommentObjectType: commentInfo, exist, err := os.commentRepo.GetComment(ctx, objectID) if err != nil { return nil, err } if !exist { break } objInfo = &schema.SimpleObjectInfo{ ObjectID: commentInfo.ID, ObjectCreatorUserID: commentInfo.UserID, ObjectType: objectType, Content: commentInfo.ParsedText, // todo trim CommentID: commentInfo.ID, CommentStatus: commentInfo.Status, } if len(commentInfo.QuestionID) > 0 { questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, commentInfo.QuestionID) if err != nil { return nil, err } if exist { objInfo.QuestionID = questionInfo.ID objInfo.QuestionStatus = questionInfo.Status objInfo.Title = questionInfo.Title } answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, commentInfo.ObjectID) if err != nil { return nil, err } if exist { objInfo.AnswerID = answerInfo.ID } } case constant.TagObjectType: tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true) if err != nil { return nil, err } if !exist { break } objInfo = &schema.SimpleObjectInfo{ ObjectID: tagInfo.ID, ObjectCreatorUserID: tagInfo.UserID, TagID: tagInfo.ID, TagStatus: tagInfo.Status, ObjectType: objectType, Title: tagInfo.SlugName, Content: tagInfo.ParsedText, // todo trim } } if objInfo == nil { err = errors.BadRequest(reason.ObjectNotFound) } return objInfo, err } ================================================ FILE: internal/service/permission/answer_permission.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package permission import ( "context" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/schema" ) // GetAnswerPermission get answer permission func GetAnswerPermission(ctx context.Context, userID, creatorUserID string, status int, canEdit, canDelete, canRecover bool) ( actions []*schema.PermissionMemberAction) { lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if len(userID) > 0 { actions = append(actions, &schema.PermissionMemberAction{ Action: "report", Name: translator.Tr(lang, reportActionName), Type: "reason", }) } if canEdit || userID == creatorUserID { actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", Name: translator.Tr(lang, editActionName), Type: "edit", }) } if (canDelete || userID == creatorUserID) && status != entity.AnswerStatusDeleted { actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", Name: translator.Tr(lang, deleteActionName), Type: "confirm", }) } if canRecover && status == entity.AnswerStatusDeleted { actions = append(actions, &schema.PermissionMemberAction{ Action: "undelete", Name: translator.Tr(lang, undeleteActionName), Type: "confirm", }) } return actions } ================================================ FILE: internal/service/permission/comment_permission.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package permission import ( "context" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/schema" ) // GetCommentPermission get comment permission func GetCommentPermission(ctx context.Context, userID string, creatorUserID string, createdAt time.Time, canEdit, canDelete bool) (actions []*schema.PermissionMemberAction) { lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if len(userID) > 0 { actions = append(actions, &schema.PermissionMemberAction{ Action: "report", Name: translator.Tr(lang, reportActionName), Type: "reason", }) } deadline := createdAt.Add(constant.CommentEditDeadline) if canEdit || (userID == creatorUserID && time.Now().Before(deadline)) { actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", Name: translator.Tr(lang, editActionName), Type: "edit", }) } if canDelete || userID == creatorUserID { actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", Name: translator.Tr(lang, deleteActionName), Type: "reason", }) } return actions } ================================================ FILE: internal/service/permission/permission_name.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package permission const ( AdminAccess = "admin.access" QuestionAdd = "question.add" QuestionEdit = "question.edit" QuestionEditWithoutReview = "question.edit_without_review" QuestionDelete = "question.delete" QuestionClose = "question.close" QuestionReopen = "question.reopen" QuestionVoteUp = "question.vote_up" QuestionVoteDown = "question.vote_down" QuestionPin = "question.pin" QuestionUnPin = "question.unpin" QuestionHide = "question.hide" QuestionShow = "question.show" AnswerAdd = "answer.add" AnswerEdit = "answer.edit" AnswerEditWithoutReview = "answer.edit_without_review" AnswerDelete = "answer.delete" AnswerAccept = "answer.accept" AnswerVoteUp = "answer.vote_up" AnswerVoteDown = "answer.vote_down" AnswerInviteSomeoneToAnswer = "answer.invite_someone_to_answer" CommentAdd = "comment.add" CommentEdit = "comment.edit" CommentDelete = "comment.delete" CommentVoteUp = "comment.vote_up" CommentVoteDown = "comment.vote_down" ReportAdd = "report.add" TagAdd = "tag.add" TagEdit = "tag.edit" TagEditSlugName = "tag.edit_slug_name" TagEditWithoutReview = "tag.edit_without_review" TagDelete = "tag.delete" TagMerge = "tag.merge" TagSynonym = "tag.synonym" LinkUrlLimit = "link.url_limit" VoteDetail = "vote.detail" AnswerAudit = "answer.audit" QuestionAudit = "question.audit" TagAudit = "tag.audit" TagUseReservedTag = "tag.use_reserved_tag" AnswerUnDelete = "answer.undeleted" QuestionUnDelete = "question.undeleted" TagUnDelete = "tag.undeleted" ) const ( reportActionName = "action.report" editActionName = "action.edit" deleteActionName = "action.delete" mergeActionName = "action.merge" undeleteActionName = "action.undelete" closeActionName = "action.close" reopenActionName = "action.reopen" pinActionName = "action.pin" unpinActionName = "action.unpin" hideActionName = "action.hide" showActionName = "action.show" inviteSomeoneToAnswerActionName = "action.invite_someone_to_answer" ) ================================================ FILE: internal/service/permission/question_permission.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package permission import ( "context" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/schema" ) // GetQuestionPermission get question permission func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string, status int, canEdit, canDelete, canClose, canReopen, canPin, canHide, canUnPin, canShow, canRecover bool) ( actions []*schema.PermissionMemberAction) { lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if len(userID) > 0 { actions = append(actions, &schema.PermissionMemberAction{ Action: "report", Name: translator.Tr(lang, reportActionName), Type: "reason", }) } if (canEdit || userID == creatorUserID) && status != entity.QuestionStatusDeleted { actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", Name: translator.Tr(lang, editActionName), Type: "edit", }) } if canClose && status == entity.QuestionStatusAvailable { actions = append(actions, &schema.PermissionMemberAction{ Action: "close", Name: translator.Tr(lang, closeActionName), Type: "confirm", }) } if canReopen { actions = append(actions, &schema.PermissionMemberAction{ Action: "reopen", Name: translator.Tr(lang, reopenActionName), Type: "confirm", }) } if canPin { actions = append(actions, &schema.PermissionMemberAction{ Action: "pin", Name: translator.Tr(lang, pinActionName), Type: "confirm", }) } if canHide { actions = append(actions, &schema.PermissionMemberAction{ Action: "hide", Name: translator.Tr(lang, hideActionName), Type: "confirm", }) } if canUnPin { actions = append(actions, &schema.PermissionMemberAction{ Action: "unpin", Name: translator.Tr(lang, unpinActionName), Type: "confirm", }) } if canShow { actions = append(actions, &schema.PermissionMemberAction{ Action: "show", Name: translator.Tr(lang, showActionName), Type: "confirm", }) } if (canDelete || userID == creatorUserID) && status != entity.QuestionStatusDeleted { actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", Name: translator.Tr(lang, deleteActionName), Type: "confirm", }) } if canRecover && status == entity.QuestionStatusDeleted { actions = append(actions, &schema.PermissionMemberAction{ Action: "undelete", Name: translator.Tr(lang, undeleteActionName), Type: "confirm", }) } return actions } // GetQuestionExtendsPermission get question extends permission func GetQuestionExtendsPermission(ctx context.Context, canInviteOtherToAnswer bool) ( actions []*schema.PermissionMemberAction) { lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if canInviteOtherToAnswer { actions = append(actions, &schema.PermissionMemberAction{ Action: "invite_other_to_answer", Name: translator.Tr(lang, inviteSomeoneToAnswerActionName), Type: "confirm", }) } return actions } ================================================ FILE: internal/service/permission/tag_permission.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package permission import ( "context" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/schema" ) // GetTagPermission get tag permission func GetTagPermission(ctx context.Context, status int, canEdit, canDelete, canMerge, canRecover bool) ( actions []*schema.PermissionMemberAction) { lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if canEdit { actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", Name: translator.Tr(lang, editActionName), Type: "edit", }) } if canDelete && status != entity.TagStatusDeleted { actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", Name: translator.Tr(lang, deleteActionName), Type: "reason", }) } if canMerge && status != entity.TagStatusDeleted { actions = append(actions, &schema.PermissionMemberAction{ Action: "merge", Name: translator.Tr(lang, mergeActionName), Type: "edit", }) } if canRecover && status == entity.QuestionStatusDeleted { actions = append(actions, &schema.PermissionMemberAction{ Action: "undelete", Name: translator.Tr(lang, undeleteActionName), Type: "confirm", }) } return actions } // GetTagSynonymPermission get tag synonym permission func GetTagSynonymPermission(ctx context.Context, canEdit bool) ( actions []*schema.PermissionMemberAction) { lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if canEdit { actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", Name: translator.Tr(lang, editActionName), Type: "edit", }) } return actions } ================================================ FILE: internal/service/plugin_common/plugin_common_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin_common import ( "context" "encoding/json" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/repo/search_sync" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/importer" "github.com/apache/answer/plugin" ) type PluginConfigRepo interface { SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error) GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error) } type PluginUserConfigRepo interface { SaveUserPluginConfig(ctx context.Context, userID string, pluginSlugName, configValue string) (err error) GetPluginUserConfig(ctx context.Context, userID, pluginSlugName string) ( pluginUserConfig *entity.PluginUserConfig, exist bool, err error) GetPluginUserConfigPage(ctx context.Context, page, pageSize int) ( pluginUserConfigs []*entity.PluginUserConfig, total int64, err error) DeleteUserPluginConfig(ctx context.Context, userID string) (err error) } // PluginCommonService user service type PluginCommonService struct { configService *config.ConfigService pluginConfigRepo PluginConfigRepo pluginUserConfigRepo PluginUserConfigRepo data *data.Data importerService *importer.ImporterService } // NewPluginCommonService new report service func NewPluginCommonService( pluginConfigRepo PluginConfigRepo, pluginUserConfigRepo PluginUserConfigRepo, configService *config.ConfigService, data *data.Data, importerService *importer.ImporterService, ) *PluginCommonService { p := &PluginCommonService{ configService: configService, pluginConfigRepo: pluginConfigRepo, pluginUserConfigRepo: pluginUserConfigRepo, data: data, importerService: importerService, } p.initPluginData() return p } // UpdatePluginStatus update plugin status func (ps *PluginCommonService) UpdatePluginStatus(ctx context.Context) (err error) { content, err := plugin.StatusManager.MarshalJSON() if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err) } return ps.configService.UpdateConfig(ctx, constant.PluginStatus, string(content)) } // UpdatePluginConfig update plugin config func (ps *PluginCommonService) UpdatePluginConfig(ctx context.Context, req *schema.UpdatePluginConfigReq) (err error) { configValue, _ := json.Marshal(req.ConfigFields) err = ps.pluginConfigRepo.SavePluginConfig(ctx, req.PluginSlugName, string(configValue)) if err != nil { return err } _ = plugin.CallSearch(func(search plugin.Search) error { if search.Info().SlugName == req.PluginSlugName { search.RegisterSyncer(ctx, search_sync.NewPluginSyncer(ps.data)) } return nil }) _ = plugin.CallImporter(func(importer plugin.Importer) error { importer.RegisterImporterFunc(ctx, ps.importerService.NewImporterFunc()) return nil }) return nil } // UpdatePluginUserConfig update plugin config func (ps *PluginCommonService) UpdatePluginUserConfig(ctx context.Context, req *schema.UpdateUserPluginConfigReq) (err error) { configValue, _ := json.Marshal(req.ConfigFields) err = ps.pluginUserConfigRepo.SaveUserPluginConfig(ctx, req.UserID, req.PluginSlugName, string(configValue)) if err != nil { return err } return nil } // GetUserPluginConfig get user plugin config func (ps *PluginCommonService) GetUserPluginConfig(ctx context.Context, req *schema.GetUserPluginConfigReq) ( configValue string, err error) { pluginUserConfig, exist, err := ps.pluginUserConfigRepo.GetPluginUserConfig(ctx, req.UserID, req.PluginSlugName) if err != nil { return "", err } if !exist { return "", nil } return pluginUserConfig.Value, nil } func (ps *PluginCommonService) initPluginData() { _ = plugin.CallKVStorage(func(k plugin.KVStorage) error { k.SetOperator(plugin.NewKVOperator( ps.data.DB, ps.data.Cache, k.Info().SlugName, )) return nil }) // init plugin status pluginStatus, err := ps.configService.GetStringValueFromDB(context.TODO(), constant.PluginStatus) if err != nil { log.Error(err) } else { if err := plugin.StatusManager.UnmarshalJSON([]byte(pluginStatus)); err != nil { log.Error(err) } } // init plugin config pluginConfigs, err := ps.pluginConfigRepo.GetPluginConfigAll(context.Background()) if err != nil { log.Error(err) } else { for _, pluginConfig := range pluginConfigs { err := plugin.CallConfig(func(fn plugin.Config) error { if fn.Info().SlugName == pluginConfig.PluginSlugName { return fn.ConfigReceiver([]byte(pluginConfig.Value)) } return nil }) if err != nil { log.Errorf("parse plugin config failed: %s %v", pluginConfig.PluginSlugName, err) } } _ = plugin.CallCache(func(cache plugin.Cache) error { ps.data.Cache = cache return nil }) } // init plugin user config plugin.RegisterGetPluginUserConfigFunc(func(userID, pluginSlugName string) []byte { pluginUserConfig, exist, err := ps.pluginUserConfigRepo.GetPluginUserConfig(context.Background(), userID, pluginSlugName) if err != nil { log.Error(err) return nil } if !exist { return nil } return []byte(pluginUserConfig.Value) }) // init plugin user config data go func() { page, pageSize := 1, 1000 for { userConfigs, _, err := ps.pluginUserConfigRepo.GetPluginUserConfigPage(context.Background(), page, pageSize) if err != nil { log.Error(err) return } if len(userConfigs) == 0 { return } for _, userConfig := range userConfigs { err := plugin.CallUserConfig(func(fn plugin.UserConfig) error { if fn.Info().SlugName == userConfig.PluginSlugName { return fn.UserConfigReceiver(userConfig.UserID, []byte(userConfig.Value)) } return nil }) if err != nil { log.Errorf("parse plugin user config failed: %s %v", userConfig.PluginSlugName, err) } } page++ } }() } ================================================ FILE: internal/service/provider.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package service import ( "github.com/apache/answer/internal/service/action" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/activityqueue" "github.com/apache/answer/internal/service/ai_conversation" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/apikey" "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/internal/service/badge" "github.com/apache/answer/internal/service/collection" collectioncommon "github.com/apache/answer/internal/service/collection_common" "github.com/apache/answer/internal/service/comment" "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/dashboard" "github.com/apache/answer/internal/service/eventqueue" "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/feature_toggle" "github.com/apache/answer/internal/service/file_record" "github.com/apache/answer/internal/service/follow" "github.com/apache/answer/internal/service/importer" "github.com/apache/answer/internal/service/meta" metacommon "github.com/apache/answer/internal/service/meta_common" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/notification" notficationcommon "github.com/apache/answer/internal/service/notification_common" "github.com/apache/answer/internal/service/object_info" "github.com/apache/answer/internal/service/plugin_common" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/internal/service/reason" "github.com/apache/answer/internal/service/report" "github.com/apache/answer/internal/service/report_handle" "github.com/apache/answer/internal/service/review" "github.com/apache/answer/internal/service/revision_common" "github.com/apache/answer/internal/service/role" "github.com/apache/answer/internal/service/search_parser" "github.com/apache/answer/internal/service/siteinfo" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/internal/service/tag" tagcommon "github.com/apache/answer/internal/service/tag_common" "github.com/apache/answer/internal/service/uploader" "github.com/apache/answer/internal/service/user_admin" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/internal/service/user_external_login" "github.com/apache/answer/internal/service/user_notification_config" "github.com/google/wire" ) // ProviderSetService is providers. var ProviderSetService = wire.NewSet( comment.NewCommentService, comment_common.NewCommentCommonService, report.NewReportService, content.NewVoteService, tag.NewTagService, follow.NewFollowService, collection.NewCollectionGroupService, collection.NewCollectionService, action.NewCaptchaService, auth.NewAuthService, content.NewUserService, content.NewQuestionService, content.NewAnswerService, export.NewEmailService, tagcommon.NewTagCommonService, usercommon.NewUserCommon, questioncommon.NewQuestionCommon, answercommon.NewAnswerCommon, uploader.NewUploaderService, collectioncommon.NewCollectionCommon, revision_common.NewRevisionService, content.NewRevisionService, rank.NewRankService, search_parser.NewSearchParser, content.NewSearchService, metacommon.NewMetaCommonService, object_info.NewObjService, report_handle.NewReportHandle, user_admin.NewUserAdminService, reason.NewReasonService, siteinfo_common.NewSiteInfoCommonService, siteinfo.NewSiteInfoService, notficationcommon.NewNotificationCommon, notification.NewNotificationService, activity.NewAnswerActivityService, dashboard.NewDashboardService, activity_common.NewActivityCommon, activity.NewActivityService, role.NewRoleService, role.NewUserRoleRelService, role.NewRolePowerRelService, user_external_login.NewUserExternalLoginService, user_external_login.NewUserCenterLoginService, plugin_common.NewPluginCommonService, config.NewConfigService, noticequeue.NewService, activityqueue.NewService, user_notification_config.NewUserNotificationConfigService, notification.NewExternalNotificationService, noticequeue.NewExternalService, review.NewReviewService, meta.NewMetaService, eventqueue.NewService, badge.NewBadgeService, badge.NewBadgeEventService, badge.NewBadgeAwardService, badge.NewBadgeGroupService, importer.NewImporterService, file_record.NewFileRecordService, apikey.NewAPIKeyService, ai_conversation.NewAIConversationService, feature_toggle.NewFeatureToggleService, ) ================================================ FILE: internal/service/question_common/question.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package questioncommon import ( "context" "encoding/json" "fmt" "math" "strings" "time" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/activityqueue" "github.com/apache/answer/internal/service/config" metacommon "github.com/apache/answer/internal/service/meta_common" "github.com/apache/answer/internal/service/revision" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" answercommon "github.com/apache/answer/internal/service/answer_common" collectioncommon "github.com/apache/answer/internal/service/collection_common" tagcommon "github.com/apache/answer/internal/service/tag_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/segmentfault/pacman/log" ) // QuestionRepo question repository type QuestionRepo interface { AddQuestion(ctx context.Context, question *entity.Question) (err error) RemoveQuestion(ctx context.Context, id string) (err error) UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error) GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error) GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error) GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) ( questionList []*entity.Question, total int64, err error) GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) (questionList []*entity.Question, total int64, err error) UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) DeletePermanentlyQuestions(ctx context.Context) (err error) RecoverQuestion(ctx context.Context, questionID string) (err error) UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error) GetQuestionsByTitle(ctx context.Context, title string, pageSize int) (questionList []*entity.Question, err error) UpdatePvCount(ctx context.Context, questionID string) (err error) UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error) UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) GetQuestionCount(ctx context.Context) (count int64, err error) GetUnansweredQuestionCount(ctx context.Context) (count int64, err error) GetResolvedQuestionCount(ctx context.Context) (count int64, err error) GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error) SitemapQuestions(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) RemoveAllUserQuestion(ctx context.Context, userID string) (err error) UpdateSearch(ctx context.Context, questionID string) (err error) LinkQuestion(ctx context.Context, link ...*entity.QuestionLink) (err error) GetLinkedQuestionIDs(ctx context.Context, questionID string, status int) (questionIDs []string, err error) UpdateQuestionLinkCount(ctx context.Context, questionID string) (err error) RemoveQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error) RecoverQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error) UpdateQuestionLinkStatus(ctx context.Context, status int, links ...*entity.QuestionLink) (err error) GetQuestionLink(ctx context.Context, page, pageSize int, questionID string, orderCond string, inDays int) (questions []*entity.Question, total int64, err error) } // QuestionCommon user service type QuestionCommon struct { questionRepo QuestionRepo answerRepo answercommon.AnswerRepo voteRepo activity_common.VoteRepo followCommon activity_common.FollowRepo tagCommon *tagcommon.TagCommonService userCommon *usercommon.UserCommon collectionCommon *collectioncommon.CollectionCommon AnswerCommon *answercommon.AnswerCommon metaCommonService *metacommon.MetaCommonService configService *config.ConfigService activityQueueService activityqueue.Service revisionRepo revision.RevisionRepo siteInfoService siteinfo_common.SiteInfoCommonService data *data.Data } func NewQuestionCommon(questionRepo QuestionRepo, answerRepo answercommon.AnswerRepo, voteRepo activity_common.VoteRepo, followCommon activity_common.FollowRepo, tagCommon *tagcommon.TagCommonService, userCommon *usercommon.UserCommon, collectionCommon *collectioncommon.CollectionCommon, answerCommon *answercommon.AnswerCommon, metaCommonService *metacommon.MetaCommonService, configService *config.ConfigService, activityQueueService activityqueue.Service, revisionRepo revision.RevisionRepo, siteInfoService siteinfo_common.SiteInfoCommonService, data *data.Data, ) *QuestionCommon { return &QuestionCommon{ questionRepo: questionRepo, answerRepo: answerRepo, voteRepo: voteRepo, followCommon: followCommon, tagCommon: tagCommon, userCommon: userCommon, collectionCommon: collectionCommon, AnswerCommon: answerCommon, metaCommonService: metaCommonService, configService: configService, activityQueueService: activityQueueService, revisionRepo: revisionRepo, siteInfoService: siteInfoService, data: data, } } func (qs *QuestionCommon) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) { return qs.questionRepo.GetUserQuestionCount(ctx, userID, 0) } func (qs *QuestionCommon) GetPersonalUserQuestionCount(ctx context.Context, loginUserID, userID string, isAdmin bool) (count int64, err error) { show := entity.QuestionShow if loginUserID == userID || isAdmin { show = 0 } return qs.questionRepo.GetUserQuestionCount(ctx, userID, show) } func (qs *QuestionCommon) UpdatePv(ctx context.Context, questionID string) error { return qs.questionRepo.UpdatePvCount(ctx, questionID) } func (qs *QuestionCommon) UpdateAnswerCount(ctx context.Context, questionID string) error { count, err := qs.answerRepo.GetCountByQuestionID(ctx, questionID) if err != nil { return err } if count == 0 { err = qs.questionRepo.UpdateLastAnswer(ctx, &entity.Question{ ID: questionID, LastAnswerID: "0", }) if err != nil { return err } } return qs.questionRepo.UpdateAnswerCount(ctx, questionID, int(count)) } func (qs *QuestionCommon) UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) { return qs.questionRepo.UpdateCollectionCount(ctx, questionID) } func (qs *QuestionCommon) UpdateAccepted(ctx context.Context, questionID, answerID string) error { question := &entity.Question{} question.ID = questionID question.AcceptedAnswerID = answerID return qs.questionRepo.UpdateAccepted(ctx, question) } func (qs *QuestionCommon) UpdateLastAnswer(ctx context.Context, questionID, answerID string) error { question := &entity.Question{} question.ID = questionID question.LastAnswerID = answerID return qs.questionRepo.UpdateLastAnswer(ctx, question) } func (qs *QuestionCommon) UpdatePostTime(ctx context.Context, questionID string) error { questioninfo := &entity.Question{} now := time.Now() questioninfo.ID = questionID questioninfo.PostUpdateTime = now return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"}) } func (qs *QuestionCommon) UpdatePostSetTime(ctx context.Context, questionID string, setTime time.Time) error { questioninfo := &entity.Question{} questioninfo.ID = questionID questioninfo.PostUpdateTime = setTime return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"}) } func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfoResp, error) { list := make(map[string]*schema.QuestionInfoResp) questionList, err := qs.questionRepo.FindByID(ctx, questionIDs) if err != nil { return list, err } questions, err := qs.FormatQuestions(ctx, questionList, loginUserID) if err != nil { return list, err } for _, item := range questions { list[item.ID] = item } return list, nil } func (qs *QuestionCommon) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) { InviteUserInfo := make([]*schema.UserBasicInfo, 0) dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID) if err != nil { return InviteUserInfo, err } if !has { return InviteUserInfo, errors.NotFound(reason.QuestionNotFound) } // InviteUser if dbinfo.InviteUserID != "" { InviteUserIDs := make([]string, 0) err := json.Unmarshal([]byte(dbinfo.InviteUserID), &InviteUserIDs) if err == nil { inviteUserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, InviteUserIDs) if err == nil { for _, userid := range InviteUserIDs { _, ok := inviteUserInfoMap[userid] if ok { InviteUserInfo = append(InviteUserInfo, inviteUserInfoMap[userid]) } } } } } return InviteUserInfo, nil } func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (resp *schema.QuestionInfoResp, err error) { questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID) if err != nil { return resp, err } questionInfo.ID = uid.DeShortID(questionInfo.ID) if !has { return resp, errors.NotFound(reason.QuestionNotFound) } resp = qs.ShowFormat(ctx, questionInfo) if resp.Status == entity.QuestionStatusClosed { metaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey) if err != nil { log.Error(err) } else { closeMsg := &schema.CloseQuestionMeta{} err = json.Unmarshal([]byte(metaInfo.Value), closeMsg) if err != nil { log.Error("json.Unmarshal CloseQuestionMeta error", err.Error()) } else { cfg, err := qs.configService.GetConfigByID(ctx, closeMsg.CloseType) if err != nil { log.Error("json.Unmarshal QuestionCloseJson error", err.Error()) } else { reasonItem := &schema.ReasonItem{} _ = json.Unmarshal(cfg.GetByteValue(), reasonItem) reasonItem.Translate(cfg.Key, handler.GetLangByCtx(ctx)) operation := &schema.Operation{} operation.Type = reasonItem.Name operation.Description = reasonItem.Description operation.Msg = closeMsg.CloseMsg operation.Time = metaInfo.CreatedAt.Unix() operation.Level = schema.OperationLevelInfo resp.Operation = operation } } } } if resp.Status != entity.QuestionStatusDeleted { if resp.Tags, err = qs.tagCommon.GetObjectTag(ctx, questionID); err != nil { return resp, err } } else { revisionInfo, exist, err := qs.revisionRepo.GetLastRevisionByObjectID(ctx, questionID) if err != nil { log.Errorf("get revision error %s", err) } if exist { questionWithTagsRevision := &entity.QuestionWithTagsRevision{} if err = json.Unmarshal([]byte(revisionInfo.Content), questionWithTagsRevision); err != nil { log.Errorf("revision parsing error %s", err) return resp, nil } for _, tag := range questionWithTagsRevision.Tags { resp.Tags = append(resp.Tags, &schema.TagResp{ ID: tag.ID, SlugName: tag.SlugName, DisplayName: tag.DisplayName, MainTagSlugName: tag.MainTagSlugName, Recommend: tag.Recommend, Reserved: tag.Reserved, }) } } } userIds := make([]string, 0) if checker.IsNotZeroString(questionInfo.UserID) { userIds = append(userIds, questionInfo.UserID) } if checker.IsNotZeroString(questionInfo.LastEditUserID) { userIds = append(userIds, questionInfo.LastEditUserID) } if checker.IsNotZeroString(resp.LastAnsweredUserID) { userIds = append(userIds, resp.LastAnsweredUserID) } userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) if err != nil { return resp, err } resp.UserInfo = userInfoMap[questionInfo.UserID] resp.UpdateUserInfo = userInfoMap[questionInfo.LastEditUserID] resp.LastAnsweredUserInfo = userInfoMap[resp.LastAnsweredUserID] if len(loginUserID) == 0 { return resp, nil } resp.VoteStatus = qs.voteRepo.GetVoteStatus(ctx, questionID, loginUserID) resp.IsFollowed, _ = qs.followCommon.IsFollowed(ctx, loginUserID, questionID) ids, err := qs.AnswerCommon.SearchAnswerIDs(ctx, loginUserID, questionInfo.ID) if err != nil { log.Error("AnswerFunc.SearchAnswerIDs", err) } resp.Answered = len(ids) > 0 if resp.Answered { resp.FirstAnswerId = ids[0] } collectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{questionInfo.ID}) if err != nil { return nil, err } if len(collectedMap) > 0 { resp.Collected = true } return resp, nil } func (qs *QuestionCommon) FormatQuestionsPage( ctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) ( formattedQuestions []*schema.QuestionPageResp, err error) { formattedQuestions = make([]*schema.QuestionPageResp, 0) questionIDs := make([]string, 0) userIDs := make([]string, 0) for _, questionInfo := range questionList { t := &schema.QuestionPageResp{ ID: questionInfo.ID, CreatedAt: questionInfo.CreatedAt.Unix(), Title: questionInfo.Title, UrlTitle: htmltext.UrlTitle(questionInfo.Title), Description: htmltext.FetchExcerpt(questionInfo.ParsedText, "...", 240), Status: questionInfo.Status, ViewCount: questionInfo.ViewCount, UniqueViewCount: questionInfo.UniqueViewCount, VoteCount: questionInfo.VoteCount, AnswerCount: questionInfo.AnswerCount, CollectionCount: questionInfo.CollectionCount, FollowCount: questionInfo.FollowCount, AcceptedAnswerID: questionInfo.AcceptedAnswerID, LastAnswerID: questionInfo.LastAnswerID, Pin: questionInfo.Pin, Show: questionInfo.Show, Operator: &schema.QuestionPageRespOperator{ID: questionInfo.UserID}, } questionIDs = append(questionIDs, questionInfo.ID) userIDs = append(userIDs, questionInfo.UserID) haveEdited, haveAnswered := false, false if checker.IsNotZeroString(questionInfo.LastEditUserID) { haveEdited = true userIDs = append(userIDs, questionInfo.LastEditUserID) } if checker.IsNotZeroString(questionInfo.LastAnswerID) { haveAnswered = true answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, questionInfo.LastAnswerID) if err == nil && exist { if answerInfo.LastEditUserID != "0" { t.LastAnsweredUserID = answerInfo.LastEditUserID } else { t.LastAnsweredUserID = answerInfo.UserID } t.LastAnsweredAt = answerInfo.CreatedAt userIDs = append(userIDs, t.LastAnsweredUserID) } } // The default operation is to ask questions t.OperationType = schema.QuestionPageRespOperationTypeAsked t.OperatedAt = questionInfo.CreatedAt.Unix() t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID} // If the order is active, the last operation time is the last edit or answer time if it exists if orderCond == schema.QuestionOrderCondActive { if haveEdited { t.OperationType = schema.QuestionPageRespOperationTypeModified t.OperatedAt = questionInfo.UpdatedAt.Unix() t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID} } if haveAnswered { if t.LastAnsweredAt.Unix() > t.OperatedAt { t.OperationType = schema.QuestionPageRespOperationTypeAnswered t.OperatedAt = t.LastAnsweredAt.Unix() t.Operator = &schema.QuestionPageRespOperator{ID: t.LastAnsweredUserID} } } } formattedQuestions = append(formattedQuestions, t) } tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, questionIDs) if err != nil { return formattedQuestions, err } userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIDs) if err != nil { return formattedQuestions, err } for _, item := range formattedQuestions { tags, ok := tagsMap[item.ID] if ok { item.Tags = tags } else { item.Tags = make([]*schema.TagResp, 0) } userInfo, ok := userInfoMap[item.Operator.ID] if ok { if userInfo != nil { item.Operator.DisplayName = userInfo.DisplayName item.Operator.Username = userInfo.Username item.Operator.Rank = userInfo.Rank item.Operator.Status = userInfo.Status item.Operator.Avatar = userInfo.Avatar } } } return formattedQuestions, nil } func (qs *QuestionCommon) FormatQuestions(ctx context.Context, questionList []*entity.Question, loginUserID string) ([]*schema.QuestionInfoResp, error) { list := make([]*schema.QuestionInfoResp, 0) objectIds := make([]string, 0) userIds := make([]string, 0) for _, questionInfo := range questionList { item := qs.ShowFormat(ctx, questionInfo) list = append(list, item) objectIds = append(objectIds, item.ID) userIds = append(userIds, item.UserID, item.LastEditUserID, item.LastAnsweredUserID) } tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, objectIds) if err != nil { return list, err } userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) if err != nil { return list, err } for _, item := range list { item.Tags = tagsMap[item.ID] item.UserInfo = userInfoMap[item.UserID] item.UpdateUserInfo = userInfoMap[item.LastEditUserID] item.LastAnsweredUserInfo = userInfoMap[item.LastAnsweredUserID] } if loginUserID == "" { return list, nil } collectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, objectIds) if err != nil { return nil, err } for _, item := range list { item.Collected = collectedMap[item.ID] } return list, nil } // RemoveQuestion delete question func (qs *QuestionCommon) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) { questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) if err != nil { return err } if !has { return nil } if questionInfo.Status == entity.QuestionStatusDeleted { return nil } questionInfo.Status = entity.QuestionStatusDeleted err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status) if err != nil { return err } userQuestionCount, err := qs.GetUserQuestionCount(ctx, questionInfo.UserID) if err != nil { log.Error("user GetUserQuestionCount error", err.Error()) } else { err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount) if err != nil { log.Error("user IncreaseQuestionCount error", err.Error()) } } return nil } func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQuestionReq) error { questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) if err != nil { return err } if !has { return nil } questionInfo.Status = entity.QuestionStatusClosed err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status) if err != nil { return err } closeMeta, _ := json.Marshal(schema.CloseQuestionMeta{ CloseType: req.CloseType, CloseMsg: req.CloseMsg, }) err = qs.metaCommonService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta)) if err != nil { return err } qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: questionInfo.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionClosed, }) return nil } // RemoveAnswer delete answer func (qs *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err error) { answerinfo, has, err := qs.answerRepo.GetByID(ctx, id) if err != nil { return err } if !has { return nil } // user add question count err = qs.UpdateAnswerCount(ctx, answerinfo.QuestionID) if err != nil { log.Error("UpdateAnswerCount error", err.Error()) } userAnswerCount, err := qs.answerRepo.GetCountByUserID(ctx, answerinfo.UserID) if err != nil { log.Error("GetCountByUserID error", err.Error()) } err = qs.userCommon.UpdateAnswerCount(ctx, answerinfo.UserID, int(userAnswerCount)) if err != nil { log.Error("user UpdateAnswerCount error", err.Error()) } return qs.answerRepo.RemoveAnswer(ctx, id) } func (qs *QuestionCommon) SitemapCron(ctx context.Context) { questionNum, err := qs.questionRepo.GetQuestionCount(ctx) if err != nil { log.Error(err) return } if questionNum <= constant.SitemapMaxSize { _, err = qs.questionRepo.SitemapQuestions(ctx, 1, int(questionNum)) if err != nil { log.Errorf("get site map question error: %v", err) } return } totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize))) for i := 1; i <= totalPages; i++ { _, err = qs.questionRepo.SitemapQuestions(ctx, i, constant.SitemapMaxSize) if err != nil { log.Errorf("get site map question error: %v", err) return } } } func (qs *QuestionCommon) SetCache(ctx context.Context, cachekey string, info any) error { infoStr, err := json.Marshal(info) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashboardCacheTime) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } return nil } func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp { return qs.ShowFormat(ctx, data) } func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp { info := schema.QuestionInfoResp{} info.ID = data.ID if handler.GetEnableShortID(ctx) { info.ID = uid.EnShortID(data.ID) } info.Title = data.Title info.UrlTitle = htmltext.UrlTitle(data.Title) info.Content = data.OriginalText info.HTML = data.ParsedText info.ViewCount = data.ViewCount info.UniqueViewCount = data.UniqueViewCount info.VoteCount = data.VoteCount info.AnswerCount = data.AnswerCount info.CollectionCount = data.CollectionCount info.FollowCount = data.FollowCount info.AcceptedAnswerID = data.AcceptedAnswerID info.LastAnswerID = data.LastAnswerID info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() info.PostUpdateTime = data.PostUpdateTime.Unix() if data.PostUpdateTime.Unix() < 1 { info.PostUpdateTime = 0 } info.QuestionUpdateTime = data.UpdatedAt.Unix() if data.UpdatedAt.Unix() < 1 { info.QuestionUpdateTime = 0 } info.Status = data.Status info.Pin = data.Pin info.Show = data.Show info.UserID = data.UserID info.LastEditUserID = data.LastEditUserID if data.LastAnswerID != "0" { answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, data.LastAnswerID) if err == nil && exist { if answerInfo.LastEditUserID != "0" { info.LastAnsweredUserID = answerInfo.LastEditUserID } else { info.LastAnsweredUserID = answerInfo.UserID } } } info.Tags = make([]*schema.TagResp, 0) return &info } func (qs *QuestionCommon) ShowFormatWithTag(ctx context.Context, data *entity.QuestionWithTagsRevision) *schema.QuestionInfoResp { info := qs.ShowFormat(ctx, &data.Question) Tags := make([]*schema.TagResp, 0) for _, tag := range data.Tags { item := &schema.TagResp{} item.SlugName = tag.SlugName item.DisplayName = tag.DisplayName item.Recommend = tag.Recommend item.Reserved = tag.Reserved Tags = append(Tags, item) } info.Tags = Tags return info } func (qs *QuestionCommon) UpdateQuestionLink(ctx context.Context, questionID, answerID, parsedText, originalText string) (string, error) { err := qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{ FromQuestionID: uid.DeShortID(questionID), FromAnswerID: uid.DeShortID(answerID), }) if err != nil { return parsedText, err } // Update the number of question links that have been removed linkedQuestionIDs, err := qs.questionRepo.GetLinkedQuestionIDs(ctx, uid.DeShortID(questionID), entity.QuestionLinkStatusDeleted) if err != nil { log.Errorf("get linked question ids error %v", err) } else { for _, id := range linkedQuestionIDs { if err := qs.questionRepo.UpdateQuestionLinkCount(ctx, id); err != nil { log.Errorf("update question link count error %v", err) } } } links := checker.GetQuestionLink(originalText) if len(links) == 0 { return parsedText, nil } // get answer ids and question ids answerIDs := make([]string, 0, len(links)) questionIDs := make([]string, 0, len(links)) for _, link := range links { if link.AnswerID != "" { answerIDs = append(answerIDs, link.AnswerID) } if link.QuestionID != "" { questionIDs = append(questionIDs, link.QuestionID) } } // get answer info and build cache answerInfoList, err := qs.answerRepo.GetByIDs(ctx, answerIDs...) if err != nil { return parsedText, err } answerCache := make(map[string]string, len(answerInfoList)) for _, ans := range answerInfoList { answerID := uid.DeShortID(ans.ID) questionID := ans.QuestionID answerCache[answerID] = questionID } // get question info and build cache questionInfoList, err := qs.questionRepo.FindByID(ctx, questionIDs) if err != nil { return parsedText, err } questionCache := make(map[string]struct{}, len(questionInfoList)) for _, q := range questionInfoList { questionID := uid.DeShortID(q.ID) questionCache[questionID] = struct{}{} } // process links and generate new QuestionLink validLinks := make([]*entity.QuestionLink, 0, len(links)) for _, link := range links { linkQuestionID := uid.DeShortID(link.QuestionID) linkAnswerID := uid.DeShortID(link.AnswerID) // validate question id if _, exists := questionCache[linkQuestionID]; linkQuestionID != "0" && !exists { continue } // validate answer id if linkAnswerID != "0" { linkedQuestionID, exists := answerCache[linkAnswerID] if !exists { continue } // if question id is empty, get it from answer cache if link.QuestionID == "" { link.QuestionID = linkedQuestionID } } // build new link newLink := &entity.QuestionLink{ FromQuestionID: uid.DeShortID(questionID), FromAnswerID: uid.DeShortID(answerID), ToQuestionID: uid.DeShortID(link.QuestionID), ToAnswerID: uid.DeShortID(link.AnswerID), } // replace link in parsed text if link.QuestionID != "" { htmlLink := fmt.Sprintf("#%s", link.QuestionID, link.QuestionID) parsedText = strings.ReplaceAll(parsedText, "#"+link.QuestionID, htmlLink) } if link.AnswerID != "" { linkedQuestionID := answerCache[linkAnswerID] htmlLink := fmt.Sprintf("#%s", linkedQuestionID, link.AnswerID, link.AnswerID) parsedText = strings.ReplaceAll(parsedText, "#"+link.AnswerID, htmlLink) newLink.ToQuestionID = uid.DeShortID(linkedQuestionID) } // avoid link to self if newLink.FromQuestionID != newLink.ToQuestionID { validLinks = append(validLinks, newLink) } } // add new links to repo if len(validLinks) > 0 { err = qs.questionRepo.LinkQuestion(ctx, validLinks...) if err != nil { return parsedText, err } } // update question linked count for _, link := range validLinks { if len(link.ToQuestionID) == 0 { continue } if err := qs.questionRepo.UpdateQuestionLinkCount(ctx, link.ToQuestionID); err != nil { log.Errorf("update question link count error %v", err) } } return parsedText, nil } // AddQuestionLinkForCloseReason When the reason about close question is a question link, add the link to the question func (qs *QuestionCommon) AddQuestionLinkForCloseReason(ctx context.Context, questionInfo *entity.Question, closeMsg string) { questionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsg) if len(questionID) == 0 { return } linkedQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, questionID) if err != nil { log.Errorf("get question error %s", err) return } if !exist { return } err = qs.questionRepo.LinkQuestion(ctx, &entity.QuestionLink{ FromQuestionID: questionInfo.ID, ToQuestionID: linkedQuestion.ID, Status: entity.QuestionLinkStatusAvailable, }) if err != nil { log.Errorf("link question error %s", err) } } func (qs *QuestionCommon) RemoveQuestionLinkForReopen(ctx context.Context, questionInfo *entity.Question) { questionInfo.ID = uid.DeShortID(questionInfo.ID) metaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey) if err != nil { return } closeMsgMeta := &schema.CloseQuestionMeta{} _ = json.Unmarshal([]byte(metaInfo.Value), closeMsgMeta) linkedQuestionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsgMeta.CloseMsg) if len(linkedQuestionID) == 0 { return } err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{ FromQuestionID: questionInfo.ID, ToQuestionID: linkedQuestionID, }) if err != nil { log.Errorf("remove question link error %s", err) } } func (qs *QuestionCommon) tryToGetQuestionIDFromMsg(ctx context.Context, closeMsg string) (questionID string) { siteGeneral, err := qs.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Errorf("get site general error %s", err) return } if !strings.HasPrefix(closeMsg, siteGeneral.SiteUrl) { return } // get question id from url // the url may like: https://xxx.com/questions/D1401/xxx // the D1401 is question id questionID = strings.TrimPrefix(closeMsg, siteGeneral.SiteUrl) questionID = strings.TrimPrefix(questionID, "/questions/") t := strings.Split(questionID, "/") if len(t) < 1 { return "" } questionID = t[0] questionID = uid.DeShortID(questionID) return questionID } func (qs *QuestionCommon) GetMinimumContentLength(ctx context.Context) (int, error) { siteInfo, err := qs.siteInfoService.GetSiteQuestion(ctx) if err != nil { return 6, err } return siteInfo.MinimumContent, nil } ================================================ FILE: internal/service/rank/rank_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package rank import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_type" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/object_info" "github.com/apache/answer/internal/service/permission" "github.com/apache/answer/internal/service/role" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) const ( PermissionPrefix = "rank." ) type UserRankRepo interface { GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) CheckReachLimit(ctx context.Context, session *xorm.Session, userID string, maxDailyRank int) (reach bool, err error) ChangeUserRank(ctx context.Context, session *xorm.Session, userID string, userCurrentScore, deltaRank int) (err error) TriggerUserRank(ctx context.Context, session *xorm.Session, userId string, rank int, activityType int) (isReachStandard bool, err error) UserRankPage(ctx context.Context, userId string, page, pageSize int) (rankPage []*entity.Activity, total int64, err error) } // RankService rank service type RankService struct { userCommon *usercommon.UserCommon configService *config.ConfigService userRankRepo UserRankRepo objectInfoService *object_info.ObjService roleService *role.UserRoleRelService rolePowerService *role.RolePowerRelService } // NewRankService new rank service func NewRankService( userCommon *usercommon.UserCommon, userRankRepo UserRankRepo, objectInfoService *object_info.ObjService, roleService *role.UserRoleRelService, rolePowerService *role.RolePowerRelService, configService *config.ConfigService) *RankService { return &RankService{ userCommon: userCommon, configService: configService, userRankRepo: userRankRepo, objectInfoService: objectInfoService, roleService: roleService, rolePowerService: rolePowerService, } } // CheckOperationPermission verify that the user has permission func (rs *RankService) CheckOperationPermission(ctx context.Context, userID string, action string, objectID string) ( can bool, err error) { if len(userID) == 0 { return false, nil } // get the rank of the current user userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID) if err != nil { return false, err } if !exist { return false, nil } powerMapping := rs.getUserPowerMapping(ctx, userID) if powerMapping[action] { return true, nil } if len(objectID) > 0 { objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID) if err != nil { return can, err } // if the user is this object creator, the user can operate this object. if objectInfo != nil && objectInfo.ObjectCreatorUserID == userID { return true, nil } } can, _ = rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action) return can, nil } // CheckOperationPermissionsForRanks verify that the user has permission func (rs *RankService) CheckOperationPermissionsForRanks(ctx context.Context, userID string, actions []string) ( can []bool, requireRanks []int, err error) { can = make([]bool, len(actions)) requireRanks = make([]int, len(actions)) if len(userID) == 0 { return can, requireRanks, nil } // get the rank of the current user userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID) if err != nil { return can, requireRanks, err } if !exist { return can, requireRanks, nil } powerMapping := rs.getUserPowerMapping(ctx, userID) for idx, action := range actions { if powerMapping[action] { can[idx] = true continue } meetRank, requireRank := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action) can[idx] = meetRank requireRanks[idx] = requireRank } return can, requireRanks, nil } // CheckOperationPermissions verify that the user has permission func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID string, actions []string) ( can []bool, err error) { can, _, err = rs.CheckOperationPermissionsForRanks(ctx, userID, actions) return can, err } // CheckOperationObjectOwner check operation object owner func (rs *RankService) CheckOperationObjectOwner(ctx context.Context, userID, objectID string) bool { objectID = uid.DeShortID(objectID) objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID) if err != nil { log.Error(err) return false } // if the user is this object creator, the user can operate this object. if objectInfo != nil && objectInfo.ObjectCreatorUserID == userID { return true } return false } // CheckVotePermission verify that the user has vote permission func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID string, voteUp bool) ( can bool, needRank int, err error) { if len(userID) == 0 || len(objectID) == 0 { return false, 0, nil } // get the rank of the current user userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID) if err != nil { return can, 0, err } if !exist { return can, 0, nil } objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID) if err != nil { return can, 0, err } action := "" switch objectInfo.ObjectType { case constant.QuestionObjectType: if voteUp { action = permission.QuestionVoteUp } else { action = permission.QuestionVoteDown } case constant.AnswerObjectType: if voteUp { action = permission.AnswerVoteUp } else { action = permission.AnswerVoteDown } case constant.CommentObjectType: if voteUp { action = permission.CommentVoteUp } else { action = permission.CommentVoteDown } } powerMapping := rs.getUserPowerMapping(ctx, userID) if powerMapping[action] { return true, 0, nil } can, needRank = rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action) return can, needRank, nil } // getUserPowerMapping get user power mapping func (rs *RankService) getUserPowerMapping(ctx context.Context, userID string) (powerMapping map[string]bool) { powerMapping = make(map[string]bool, 0) userRole, err := rs.roleService.GetUserRole(ctx, userID) if err != nil { log.Error(err) return powerMapping } powers, err := rs.rolePowerService.GetRolePowerList(ctx, userRole) if err != nil { log.Error(err) return powerMapping } for _, power := range powers { powerMapping[power] = true } return powerMapping } // checkUserRank verify that the user meets the prestige criteria func (rs *RankService) checkUserRank(ctx context.Context, userID string, userRank int, action string) ( can bool, rank int) { // get the amount of rank required for the current operation requireRank, err := rs.configService.GetIntValue(ctx, action) if err != nil { log.Error(err) return false, requireRank } if userRank < requireRank || requireRank < 0 { log.Debugf("user %s want to do action %s, but rank %d < %d", userID, action, userRank, requireRank) return false, requireRank } return true, requireRank } // GetRankPersonalPage get personal comment list page func (rs *RankService) GetRankPersonalPage(ctx context.Context, req *schema.GetRankPersonalWithPageReq) ( pageModel *pager.PageModel, err error) { if plugin.RankAgentEnabled() { return pager.NewPageModel(0, []string{}), nil } if len(req.Username) > 0 { userInfo, exist, err := rs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } req.UserID = userInfo.ID } if len(req.UserID) == 0 { return nil, errors.BadRequest(reason.UserNotFound) } userRankPage, total, err := rs.userRankRepo.UserRankPage(ctx, req.UserID, req.Page, req.PageSize) if err != nil { return nil, err } resp := rs.decorateRankPersonalPageResp(ctx, userRankPage) return pager.NewPageModel(total, resp), nil } func (rs *RankService) decorateRankPersonalPageResp( ctx context.Context, userRankPage []*entity.Activity) []*schema.GetRankPersonalPageResp { resp := make([]*schema.GetRankPersonalPageResp, 0) lang := handler.GetLangByCtx(ctx) for _, userRankInfo := range userRankPage { if len(userRankInfo.ObjectID) == 0 || userRankInfo.ObjectID == "0" { continue } objInfo, err := rs.objectInfoService.GetInfo(ctx, userRankInfo.ObjectID) if err != nil { log.Error(err) continue } commentResp := &schema.GetRankPersonalPageResp{ CreatedAt: userRankInfo.CreatedAt.Unix(), ObjectID: userRankInfo.ObjectID, Reputation: userRankInfo.Rank, } cfg, err := rs.configService.GetConfigByID(ctx, userRankInfo.ActivityType) if err != nil { log.Error(err) continue } commentResp.RankType = translator.Tr(lang, activity_type.ActivityTypeFlagMapping[cfg.Key]) commentResp.ObjectType = objInfo.ObjectType commentResp.Title = objInfo.Title commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title) commentResp.Content = objInfo.Content if objInfo.QuestionStatus == entity.QuestionStatusDeleted { commentResp.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey) } commentResp.QuestionID = objInfo.QuestionID commentResp.AnswerID = objInfo.AnswerID resp = append(resp, commentResp) } return resp } ================================================ FILE: internal/service/reason/reason_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package reason import ( "context" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/reason_common" ) type ReasonService struct { reasonRepo reason_common.ReasonRepo } func NewReasonService(reasonRepo reason_common.ReasonRepo) *ReasonService { return &ReasonService{ reasonRepo: reasonRepo, } } func (rs ReasonService) GetReasons(ctx context.Context, req schema.ReasonReq) (resp []*schema.ReasonItem, err error) { return rs.reasonRepo.ListReasons(ctx, req.ObjectType, req.Action) } ================================================ FILE: internal/service/reason_common/reason.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package reason_common import ( "context" "github.com/apache/answer/internal/schema" ) type ReasonRepo interface { ListReasons(ctx context.Context, objectType, action string) (resp []*schema.ReasonItem, err error) } ================================================ FILE: internal/service/report/report_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package report import ( "encoding/json" "github.com/apache/answer/internal/service/eventqueue" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/object_info" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/report_common" "github.com/apache/answer/internal/service/report_handle" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/obj" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "golang.org/x/net/context" ) // ReportService user service type ReportService struct { reportRepo report_common.ReportRepo objectInfoService *object_info.ObjService commonUser *usercommon.UserCommon answerRepo answercommon.AnswerRepo questionRepo questioncommon.QuestionRepo commentCommonRepo comment_common.CommentCommonRepo reportHandle *report_handle.ReportHandle configService *config.ConfigService eventQueueService eventqueue.Service } // NewReportService new report service func NewReportService( reportRepo report_common.ReportRepo, objectInfoService *object_info.ObjService, commonUser *usercommon.UserCommon, answerRepo answercommon.AnswerRepo, questionRepo questioncommon.QuestionRepo, commentCommonRepo comment_common.CommentCommonRepo, reportHandle *report_handle.ReportHandle, configService *config.ConfigService, eventQueueService eventqueue.Service, ) *ReportService { return &ReportService{ reportRepo: reportRepo, objectInfoService: objectInfoService, commonUser: commonUser, answerRepo: answerRepo, questionRepo: questionRepo, commentCommonRepo: commentCommonRepo, reportHandle: reportHandle, configService: configService, eventQueueService: eventQueueService, } } // AddReport add report func (rs *ReportService) AddReport(ctx context.Context, req *schema.AddReportReq) (err error) { objectTypeNumber, err := obj.GetObjectTypeNumberByObjectID(req.ObjectID) if err != nil { return err } objInfo, err := rs.objectInfoService.GetInfo(ctx, req.ObjectID) if err != nil { return err } if objInfo.IsDeleted() { return errors.BadRequest(reason.NewObjectAlreadyDeleted) } cf, err := rs.configService.GetConfigByID(ctx, req.ReportType) if err != nil || cf == nil { return errors.BadRequest(reason.ReportNotFound) } if cf.Key == constant.ReasonADuplicate && !checker.IsURL(req.Content) { return errors.BadRequest(reason.InvalidURLError) } report := &entity.Report{ UserID: req.UserID, ReportedUserID: objInfo.ObjectCreatorUserID, ObjectID: req.ObjectID, ObjectType: objectTypeNumber, ReportType: req.ReportType, Content: req.Content, Status: entity.ReportStatusPending, } err = rs.reportRepo.AddReport(ctx, report) if err != nil { return err } rs.sendEvent(ctx, report, objInfo) return nil } // GetUnreviewedReportPostPage get unreviewed report post page func (rs *ReportService) GetUnreviewedReportPostPage(ctx context.Context, req *schema.GetUnreviewedReportPostPageReq) ( pageModel *pager.PageModel, err error) { if !req.IsAdmin { return pager.NewPageModel(0, make([]*schema.GetReportListPageResp, 0)), nil } lang := handler.GetLangByCtx(ctx) reports, total, err := rs.reportRepo.GetReportListPage(ctx, &schema.GetReportListPageDTO{ Page: req.Page, PageSize: 1, Status: entity.ReportStatusPending, }) if err != nil { return } resp := make([]*schema.GetReportListPageResp, 0) for _, report := range reports { info, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, report.ObjectID) if err != nil { log.Errorf("GetUnreviewedRevisionInfo failed, err: %v", err) continue } r := &schema.GetReportListPageResp{ FlagID: report.ID, CreatedAt: info.CreatedAt, ObjectID: info.ObjectID, ObjectType: info.ObjectType, QuestionID: info.QuestionID, AnswerID: info.AnswerID, CommentID: info.CommentID, Title: info.Title, UrlTitle: htmltext.UrlTitle(info.Title), OriginalText: info.Content, ParsedText: info.Html, AnswerCount: info.AnswerCount, AnswerAccepted: info.AnswerAccepted, Tags: info.Tags, SubmitAt: report.CreatedAt.Unix(), ObjectStatus: info.Status, ObjectShowStatus: info.ShowStatus, ReasonContent: report.Content, } // get user info userInfo, exists, e := rs.commonUser.GetUserBasicInfoByID(ctx, info.ObjectCreatorUserID) if e != nil { log.Errorf("user not found by id: %s, err: %v", info.ObjectCreatorUserID, e) } if exists { _ = copier.Copy(&r.AuthorUserInfo, userInfo) } // get submitter info submitter, exists, e := rs.commonUser.GetUserBasicInfoByID(ctx, report.ReportedUserID) if e != nil { log.Errorf("user not found by id: %s, err: %v", info.ObjectCreatorUserID, e) } if exists { _ = copier.Copy(&r.SubmitterUser, submitter) } if report.ReportType > 0 { r.Reason = &schema.ReasonItem{ReasonType: report.ReportType} cf, err := rs.configService.GetConfigByID(ctx, report.ReportType) if err != nil { log.Error(err) } else { _ = json.Unmarshal([]byte(cf.Value), r.Reason) r.Reason.Translate(cf.Key, lang) } } resp = append(resp, r) } return pager.NewPageModel(total, resp), nil } // ReviewReport review report func (rs *ReportService) ReviewReport(ctx context.Context, req *schema.ReviewReportReq) (err error) { report, exist, err := rs.reportRepo.GetByID(ctx, req.FlagID) if err != nil { return err } if !exist { return errors.NotFound(reason.ReportNotFound) } // check if handle or not if report.Status != entity.ReportStatusPending { return nil } // ignore this report if req.OperationType == constant.ReportOperationIgnoreReport { return rs.reportRepo.UpdateStatus(ctx, report.ID, entity.ReportStatusIgnore) } if err = rs.reportHandle.UpdateReportedObject(ctx, report, req); err != nil { return } return rs.reportRepo.UpdateStatus(ctx, report.ID, entity.ReportStatusCompleted) } func (rs *ReportService) sendEvent(ctx context.Context, report *entity.Report, objectInfo *schema.SimpleObjectInfo) { var event *schema.EventMsg switch objectInfo.ObjectType { case constant.QuestionObjectType: event = schema.NewEvent(constant.EventQuestionFlag, report.UserID).TID(objectInfo.QuestionID). QID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID) case constant.AnswerObjectType: event = schema.NewEvent(constant.EventAnswerFlag, report.UserID).TID(objectInfo.AnswerID). AID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID) case constant.CommentObjectType: event = schema.NewEvent(constant.EventCommentFlag, report.UserID).TID(objectInfo.CommentID). CID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID) default: return } rs.eventQueueService.Send(ctx, event) } ================================================ FILE: internal/service/report_common/report_common.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package report_common import ( "context" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" ) // ReportRepo report repository type ReportRepo interface { AddReport(ctx context.Context, report *entity.Report) (err error) GetReportListPage(ctx context.Context, query *schema.GetReportListPageDTO) ( reports []*entity.Report, total int64, err error) GetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error) UpdateStatus(ctx context.Context, id string, status int) (err error) GetReportCount(ctx context.Context) (count int64, err error) } ================================================ FILE: internal/service/report_handle/report_handle.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package report_handle import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/comment" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/obj" ) type ReportHandle struct { questionService *content.QuestionService answerService *content.AnswerService commentService *comment.CommentService } func NewReportHandle( questionService *content.QuestionService, answerService *content.AnswerService, commentService *comment.CommentService, ) *ReportHandle { return &ReportHandle{ questionService: questionService, answerService: answerService, commentService: commentService, } } // UpdateReportedObject this handle object status func (rh *ReportHandle) UpdateReportedObject(ctx context.Context, report *entity.Report, req *schema.ReviewReportReq) (err error) { objectKey, err := obj.GetObjectTypeStrByObjectID(report.ObjectID) if err != nil { return err } switch objectKey { case constant.QuestionObjectType: err = rh.updateReportedQuestionReport(ctx, report, req) case constant.AnswerObjectType: err = rh.updateReportedAnswerReport(ctx, report, req) case constant.CommentObjectType: err = rh.updateReportedCommentReport(ctx, report, req) } return } func (rh *ReportHandle) updateReportedQuestionReport(ctx context.Context, report *entity.Report, req *schema.ReviewReportReq) (err error) { switch req.OperationType { case constant.ReportOperationUnlistPost: err = rh.questionService.OperationQuestion(ctx, &schema.OperationQuestionReq{ ID: report.ObjectID, Operation: schema.QuestionOperationHide, UserID: req.UserID}) case constant.ReportOperationDeletePost: err = rh.questionService.RemoveQuestion(ctx, &schema.RemoveQuestionReq{ ID: report.ObjectID, UserID: req.UserID, IsAdmin: true}) case constant.ReportOperationClosePost: err = rh.questionService.CloseQuestion(ctx, &schema.CloseQuestionReq{ ID: report.ObjectID, CloseType: req.CloseType, CloseMsg: req.CloseMsg, UserID: req.UserID, }) case constant.ReportOperationEditPost: _, err = rh.questionService.UpdateQuestion(ctx, &schema.QuestionUpdate{ ID: report.ObjectID, Title: req.Title, Content: req.Content, HTML: converter.Markdown2HTML(req.Content), Tags: req.Tags, UserID: req.UserID, NoNeedReview: true, }) } return } func (rh *ReportHandle) updateReportedAnswerReport(ctx context.Context, report *entity.Report, req *schema.ReviewReportReq) (err error) { switch req.OperationType { case constant.ReportOperationDeletePost: err = rh.answerService.RemoveAnswer(ctx, &schema.RemoveAnswerReq{ ID: report.ObjectID, UserID: req.UserID}) case constant.ReportOperationEditPost: _, err = rh.answerService.Update(ctx, &schema.AnswerUpdateReq{ ID: report.ObjectID, Title: req.Title, Content: req.Content, HTML: converter.Markdown2HTML(req.Content), UserID: req.UserID, NoNeedReview: true, }) } if err != nil { return err } return nil } func (rh *ReportHandle) updateReportedCommentReport(ctx context.Context, report *entity.Report, req *schema.ReviewReportReq) (err error) { switch req.OperationType { case constant.ReportOperationDeletePost: err = rh.commentService.RemoveComment(ctx, &schema.RemoveCommentReq{ CommentID: report.ObjectID, UserID: req.UserID}) case constant.ReportOperationEditPost: _, err = rh.commentService.UpdateComment(ctx, &schema.UpdateCommentReq{ CommentID: report.ObjectID, OriginalText: req.Content, ParsedText: converter.Markdown2HTML(req.Content), UserID: req.UserID, }) } if err != nil { return err } return nil } ================================================ FILE: internal/service/review/review_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package review import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" answercommon "github.com/apache/answer/internal/service/answer_common" commentcommon "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/noticequeue" "github.com/apache/answer/internal/service/object_info" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/role" "github.com/apache/answer/internal/service/siteinfo_common" tagcommon "github.com/apache/answer/internal/service/tag_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/token" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/plugin" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // ReviewRepo review repository type ReviewRepo interface { AddReview(ctx context.Context, review *entity.Review) (err error) UpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error) GetReview(ctx context.Context, reviewID int) (review *entity.Review, exist bool, err error) GetReviewByObject(ctx context.Context, objectID string) (review *entity.Review, exist bool, err error) GetReviewCount(ctx context.Context, status int) (count int64, err error) GetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) (reviewList []*entity.Review, total int64, err error) } // ReviewService user service type ReviewService struct { reviewRepo ReviewRepo objectInfoService *object_info.ObjService userCommon *usercommon.UserCommon userRepo usercommon.UserRepo questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo userRoleService *role.UserRoleRelService tagCommon *tagcommon.TagCommonService questionCommon *questioncommon.QuestionCommon externalNotificationQueueService noticequeue.ExternalService notificationQueueService noticequeue.Service siteInfoService siteinfo_common.SiteInfoCommonService commentCommonRepo commentcommon.CommentCommonRepo } // NewReviewService new review service func NewReviewService( reviewRepo ReviewRepo, objectInfoService *object_info.ObjService, userCommon *usercommon.UserCommon, userRepo usercommon.UserRepo, questionRepo questioncommon.QuestionRepo, answerRepo answercommon.AnswerRepo, userRoleService *role.UserRoleRelService, externalNotificationQueueService noticequeue.ExternalService, tagCommon *tagcommon.TagCommonService, questionCommon *questioncommon.QuestionCommon, notificationQueueService noticequeue.Service, siteInfoService siteinfo_common.SiteInfoCommonService, commentCommonRepo commentcommon.CommentCommonRepo, ) *ReviewService { return &ReviewService{ reviewRepo: reviewRepo, objectInfoService: objectInfoService, userCommon: userCommon, userRepo: userRepo, questionRepo: questionRepo, answerRepo: answerRepo, userRoleService: userRoleService, externalNotificationQueueService: externalNotificationQueueService, tagCommon: tagCommon, questionCommon: questionCommon, notificationQueueService: notificationQueueService, siteInfoService: siteInfoService, commentCommonRepo: commentCommonRepo, } } // AddQuestionReview add review for question if needed func (cs *ReviewService) AddQuestionReview(ctx context.Context, question *entity.Question, tags []*schema.TagItem, ip, ua string) (questionStatus int) { reviewContent := &plugin.ReviewContent{ ObjectType: constant.QuestionObjectType, Title: question.Title, Content: question.ParsedText, IP: ip, UserAgent: ua, } for _, tag := range tags { reviewContent.Tags = append(reviewContent.Tags, tag.SlugName) } reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, question.UserID) reviewStatus := cs.callPluginToReview(ctx, question.UserID, question.ID, reviewContent) switch reviewStatus { case plugin.ReviewStatusApproved: questionStatus = entity.QuestionStatusAvailable case plugin.ReviewStatusNeedReview: questionStatus = entity.QuestionStatusPending case plugin.ReviewStatusDeleteDirectly: questionStatus = entity.QuestionStatusDeleted default: questionStatus = entity.QuestionStatusAvailable } return questionStatus } // AddAnswerReview add review for answer if needed func (cs *ReviewService) AddAnswerReview(ctx context.Context, answer *entity.Answer, ip, ua string) (answerStatus int) { reviewContent := &plugin.ReviewContent{ ObjectType: constant.AnswerObjectType, Content: answer.ParsedText, IP: ip, UserAgent: ua, } reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, answer.UserID) reviewStatus := cs.callPluginToReview(ctx, answer.UserID, answer.ID, reviewContent) switch reviewStatus { case plugin.ReviewStatusApproved: answerStatus = entity.AnswerStatusAvailable case plugin.ReviewStatusNeedReview: answerStatus = entity.AnswerStatusPending case plugin.ReviewStatusDeleteDirectly: answerStatus = entity.AnswerStatusDeleted default: answerStatus = entity.AnswerStatusAvailable } return answerStatus } // AddCommentReview add review for comment if needed func (cs *ReviewService) AddCommentReview(ctx context.Context, comment *entity.Comment, ip, ua string) (commentStatus int) { reviewContent := &plugin.ReviewContent{ ObjectType: constant.CommentObjectType, Content: comment.ParsedText, IP: ip, UserAgent: ua, } reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, comment.UserID) reviewStatus := cs.callPluginToReview(ctx, comment.UserID, comment.ID, reviewContent) switch reviewStatus { case plugin.ReviewStatusApproved: commentStatus = entity.CommentStatusAvailable case plugin.ReviewStatusNeedReview: commentStatus = entity.CommentStatusPending case plugin.ReviewStatusDeleteDirectly: commentStatus = entity.CommentStatusDeleted default: commentStatus = entity.CommentStatusAvailable } return commentStatus } // get review content author info func (cs *ReviewService) getReviewContentAuthorInfo(ctx context.Context, userID string) (author plugin.ReviewContentAuthor) { user, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, userID) if err != nil { log.Errorf("get user info failed, err: %v", err) return } if !exist { log.Errorf("user not found by id: %s", userID) return } author.Rank = user.Rank author.ApprovedQuestionAmount, _ = cs.questionRepo.GetUserQuestionCount(ctx, userID, 0) author.ApprovedAnswerAmount, _ = cs.answerRepo.GetCountByUserID(ctx, userID) author.Role, _ = cs.userRoleService.GetUserRole(ctx, userID) return } // call plugin to review func (cs *ReviewService) callPluginToReview(ctx context.Context, userID, objectID string, reviewContent *plugin.ReviewContent) (reviewStatus plugin.ReviewStatus) { // As default, no need review reviewStatus = plugin.ReviewStatusApproved objectID = uid.DeShortID(objectID) r := &entity.Review{ UserID: userID, ObjectID: objectID, ObjectType: constant.ObjectTypeStrMapping[reviewContent.ObjectType], ReviewerUserID: "0", Status: entity.ReviewStatusPending, } if siteInterface, _ := cs.siteInfoService.GetSiteInterface(ctx); siteInterface != nil { reviewContent.Language = siteInterface.Language } _ = plugin.CallReviewer(func(reviewer plugin.Reviewer) error { // If one of the reviewer plugin return false, then the review is not approved if reviewStatus != plugin.ReviewStatusApproved { return nil } if result := reviewer.Review(reviewContent); !result.Approved { reviewStatus = result.ReviewStatus r.Reason = result.Reason r.Submitter = reviewer.Info().SlugName } return nil }) if reviewStatus == plugin.ReviewStatusNeedReview { if err := cs.reviewRepo.AddReview(ctx, r); err != nil { log.Errorf("add review failed, err: %v", err) } } return reviewStatus } // UpdateReview update review func (cs *ReviewService) UpdateReview(ctx context.Context, req *schema.UpdateReviewReq) (err error) { review, exist, err := cs.reviewRepo.GetReview(ctx, req.ReviewID) if err != nil { return err } if !exist { return errors.BadRequest(reason.ObjectNotFound) } if review.Status != entity.ReviewStatusPending { return nil } if err = cs.updateObjectStatus(ctx, review, req.IsApprove()); err != nil { return err } if req.IsApprove() { err = cs.reviewRepo.UpdateReviewStatus(ctx, req.ReviewID, req.UserID, entity.ReviewStatusApproved) } else { err = cs.reviewRepo.UpdateReviewStatus(ctx, req.ReviewID, req.UserID, entity.ReviewStatusRejected) } return } // update object status func (cs *ReviewService) updateObjectStatus(ctx context.Context, review *entity.Review, isApprove bool) (err error) { objectType := constant.ObjectTypeNumberMapping[review.ObjectType] switch objectType { case constant.QuestionObjectType: questionInfo, exist, err := cs.questionRepo.GetQuestion(ctx, review.ObjectID) if err != nil { return err } if !exist { return errors.BadRequest(reason.ObjectNotFound) } if isApprove { questionInfo.Status = entity.QuestionStatusAvailable } else { questionInfo.Status = entity.QuestionStatusDeleted } if err := cs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status); err != nil { return err } if isApprove { tags, err := cs.tagCommon.GetObjectEntityTag(ctx, questionInfo.ID) if err != nil { log.Errorf("get question tags failed, err: %v", err) } cs.externalNotificationQueueService.Send(ctx, schema.CreateNewQuestionNotificationMsg(questionInfo.ID, questionInfo.Title, questionInfo.UserID, tags)) } userQuestionCount, err := cs.questionRepo.GetUserQuestionCount(ctx, questionInfo.UserID, 0) if err != nil { log.Errorf("get user question count failed, err: %v", err) } else { err = cs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount) if err != nil { log.Errorf("update user question count failed, err: %v", err) } } case constant.AnswerObjectType: answerInfo, exist, err := cs.answerRepo.GetAnswer(ctx, review.ObjectID) if err != nil { return err } if !exist { return errors.BadRequest(reason.ObjectNotFound) } if isApprove { answerInfo.Status = entity.AnswerStatusAvailable } else { answerInfo.Status = entity.AnswerStatusDeleted } if err := cs.answerRepo.UpdateAnswerStatus(ctx, answerInfo.ID, answerInfo.Status); err != nil { return err } questionInfo, exist, err := cs.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) if err != nil { return err } if !exist { return errors.BadRequest(reason.ObjectNotFound) } if isApprove { cs.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, answerInfo.ID, answerInfo.UserID, questionInfo.Title, answerInfo.OriginalText) } if err := cs.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID); err != nil { log.Errorf("update question answer count failed, err: %v", err) } if err := cs.questionCommon.UpdateLastAnswer(ctx, answerInfo.QuestionID, uid.DeShortID(answerInfo.ID)); err != nil { log.Errorf("update question last answer failed, err: %v", err) } userAnswerCount, err := cs.answerRepo.GetCountByUserID(ctx, answerInfo.UserID) if err != nil { log.Errorf("get user answer count failed, err: %v", err) } else { err = cs.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount)) if err != nil { log.Errorf("update user answer count failed, err: %v", err) } } case constant.CommentObjectType: commentInfo, exist, err := cs.commentCommonRepo.GetCommentWithoutStatus(ctx, review.ObjectID) if err != nil { return err } if !exist { return errors.BadRequest(reason.ObjectNotFound) } if isApprove { commentInfo.Status = entity.CommentStatusAvailable } else { commentInfo.Status = entity.CommentStatusDeleted } if err := cs.commentCommonRepo.UpdateCommentStatus(ctx, commentInfo.ID, commentInfo.Status); err != nil { return err } _, exist, err = cs.questionRepo.GetQuestion(ctx, commentInfo.QuestionID) if err != nil { return err } if !exist { return errors.BadRequest(reason.ObjectNotFound) } if isApprove { cs.notificationCommentOnTheQuestion(ctx, commentInfo) } } return } func (cs *ReviewService) notificationAnswerTheQuestion(ctx context.Context, questionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) { // If the question is answered by me, there is no notification for myself. if questionUserID == answerUserID { return } msg := &schema.NotificationMsg{ TriggerUserID: answerUserID, ReceiverUserID: questionUserID, Type: schema.NotificationTypeInbox, ObjectID: answerID, } msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationAnswerTheQuestion cs.notificationQueueService.Send(ctx, msg) receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID) if err != nil { log.Error(err) return } if !exist { log.Warnf("user %s not found", questionUserID) return } externalNotificationMsg := &schema.ExternalNotificationMsg{ ReceiverUserID: receiverUserInfo.ID, ReceiverEmail: receiverUserInfo.EMail, ReceiverLang: receiverUserInfo.Language, } rawData := &schema.NewAnswerTemplateRawData{ QuestionTitle: questionTitle, QuestionID: questionID, AnswerID: answerID, AnswerSummary: answerSummary, UnsubscribeCode: token.GenerateToken(), } answerUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, answerUserID) if answerUser != nil { rawData.AnswerUserDisplayName = answerUser.DisplayName } externalNotificationMsg.NewAnswerTemplateRawData = rawData cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } func (cs *ReviewService) notificationCommentOnTheQuestion(ctx context.Context, comment *entity.Comment) { objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID) if err != nil { log.Error(err) return } if objInfo.IsDeleted() { log.Error("object already deleted") return } objInfo.ObjectID = uid.DeShortID(objInfo.ObjectID) objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID) objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID) // The priority of the notification // 1. reply to user // 2. comment mention to user // 3. answer or question was commented alreadyNotifiedUserID := make(map[string]bool) // get reply user info replyUserID := comment.GetReplyUserID() if len(replyUserID) > 0 && replyUserID != comment.UserID { replyUser, _, err := cs.userCommon.GetUserBasicInfoByID(ctx, replyUserID) if err != nil { log.Error(err) return } cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, comment.UserID, objInfo.QuestionID, objInfo.Title, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) alreadyNotifiedUserID[replyUser.ID] = true return } mentionUsernameList := comment.GetMentionUsernameList() if len(mentionUsernameList) > 0 { alreadyNotifiedUserIDs := cs.notificationMention( ctx, mentionUsernameList, comment.ID, comment.UserID, alreadyNotifiedUserID) for _, userID := range alreadyNotifiedUserIDs { alreadyNotifiedUserID[userID] = true } return } if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] { cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID, objInfo.QuestionID, objInfo.Title, comment.ID, comment.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) } else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] { cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID, objInfo.ObjectCreatorUserID, comment.ID, comment.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) } } func (cs *ReviewService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID, questionID, questionTitle, commentSummary string) { msg := &schema.NotificationMsg{ ReceiverUserID: replyUserID, TriggerUserID: commentUserID, Type: schema.NotificationTypeInbox, ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationReplyToYou cs.notificationQueueService.Send(ctx, msg) // Send external notification. receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, replyUserID) if err != nil { log.Error(err) return } if !exist { log.Warnf("user %s not found", replyUserID) return } externalNotificationMsg := &schema.ExternalNotificationMsg{ ReceiverUserID: receiverUserInfo.ID, ReceiverEmail: receiverUserInfo.EMail, ReceiverLang: receiverUserInfo.Language, } rawData := &schema.NewCommentTemplateRawData{ QuestionTitle: questionTitle, QuestionID: questionID, CommentID: commentID, CommentSummary: commentSummary, UnsubscribeCode: token.GenerateToken(), } commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID) if commentUser != nil { rawData.CommentUserDisplayName = commentUser.DisplayName } externalNotificationMsg.NewCommentTemplateRawData = rawData cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } func (cs *ReviewService) notificationMention( ctx context.Context, mentionUsernameList []string, commentID, commentUserID string, alreadyNotifiedUserID map[string]bool) (alreadyNotifiedUserIDs []string) { for _, username := range mentionUsernameList { userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username) if err != nil { log.Error(err) continue } if exist && !alreadyNotifiedUserID[userInfo.ID] { msg := &schema.NotificationMsg{ ReceiverUserID: userInfo.ID, TriggerUserID: commentUserID, Type: schema.NotificationTypeInbox, ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationMentionYou cs.notificationQueueService.Send(ctx, msg) alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID) } } return alreadyNotifiedUserIDs } func (cs *ReviewService) notificationQuestionComment(ctx context.Context, questionUserID, questionID, questionTitle, commentID, commentUserID, commentSummary string) { if questionUserID == commentUserID { return } // send internal notification msg := &schema.NotificationMsg{ ReceiverUserID: questionUserID, TriggerUserID: commentUserID, Type: schema.NotificationTypeInbox, ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationCommentQuestion cs.notificationQueueService.Send(ctx, msg) // send external notification receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID) if err != nil { log.Error(err) return } if !exist { log.Warnf("user %s not found", questionUserID) return } externalNotificationMsg := &schema.ExternalNotificationMsg{ ReceiverUserID: receiverUserInfo.ID, ReceiverEmail: receiverUserInfo.EMail, ReceiverLang: receiverUserInfo.Language, } rawData := &schema.NewCommentTemplateRawData{ QuestionTitle: questionTitle, QuestionID: questionID, CommentID: commentID, CommentSummary: commentSummary, UnsubscribeCode: token.GenerateToken(), } commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID) if commentUser != nil { rawData.CommentUserDisplayName = commentUser.DisplayName } externalNotificationMsg.NewCommentTemplateRawData = rawData cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } func (cs *ReviewService) notificationAnswerComment(ctx context.Context, questionID, questionTitle, answerID, answerUserID, commentID, commentUserID, commentSummary string) { if answerUserID == commentUserID { return } // Send internal notification. msg := &schema.NotificationMsg{ ReceiverUserID: answerUserID, TriggerUserID: commentUserID, Type: schema.NotificationTypeInbox, ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationCommentAnswer cs.notificationQueueService.Send(ctx, msg) // Send external notification. receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID) if err != nil { log.Error(err) return } if !exist { log.Warnf("user %s not found", answerUserID) return } externalNotificationMsg := &schema.ExternalNotificationMsg{ ReceiverUserID: receiverUserInfo.ID, ReceiverEmail: receiverUserInfo.EMail, ReceiverLang: receiverUserInfo.Language, } rawData := &schema.NewCommentTemplateRawData{ QuestionTitle: questionTitle, QuestionID: questionID, AnswerID: answerID, CommentID: commentID, CommentSummary: commentSummary, UnsubscribeCode: token.GenerateToken(), } commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID) if commentUser != nil { rawData.CommentUserDisplayName = commentUser.DisplayName } externalNotificationMsg.NewCommentTemplateRawData = rawData cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } // GetReviewPendingCount get review pending count func (cs *ReviewService) GetReviewPendingCount(ctx context.Context) (count int64, err error) { return cs.reviewRepo.GetReviewCount(ctx, entity.ReviewStatusPending) } // GetUnreviewedPostPage get review page func (cs *ReviewService) GetUnreviewedPostPage(ctx context.Context, req *schema.GetUnreviewedPostPageReq) ( pageModel *pager.PageModel, err error) { if !req.IsAdmin { return pager.NewPageModel(0, make([]*schema.GetUnreviewedPostPageResp, 0)), nil } cond := &entity.Review{ ObjectID: req.ObjectID, Status: entity.ReviewStatusPending, } reviewList, total, err := cs.reviewRepo.GetReviewPage(ctx, req.Page, 1, cond) if err != nil { return } resp := make([]*schema.GetUnreviewedPostPageResp, 0) for _, review := range reviewList { info, err := cs.objectInfoService.GetUnreviewedRevisionInfo(ctx, review.ObjectID) if err != nil { log.Errorf("GetUnreviewedRevisionInfo failed, err: %v", err) continue } r := &schema.GetUnreviewedPostPageResp{ ReviewID: review.ID, CreatedAt: info.CreatedAt, ObjectID: info.ObjectID, QuestionID: info.QuestionID, AnswerID: info.AnswerID, CommentID: info.CommentID, ObjectType: info.ObjectType, Title: info.Title, UrlTitle: htmltext.UrlTitle(info.Title), OriginalText: info.Content, ParsedText: info.Html, Tags: info.Tags, ObjectStatus: info.Status, ObjectShowStatus: info.ShowStatus, SubmitAt: review.CreatedAt.Unix(), SubmitterDisplayName: req.ReviewerMapping[review.Submitter], Reason: review.Reason, } // get user info userInfo, exists, e := cs.userCommon.GetUserBasicInfoByID(ctx, info.ObjectCreatorUserID) if e != nil { log.Errorf("user not found by id: %s, err: %v", info.ObjectCreatorUserID, e) } if exists { _ = copier.Copy(&r.AuthorUserInfo, userInfo) } resp = append(resp, r) } return pager.NewPageModel(total, resp), nil } ================================================ FILE: internal/service/revision/revision.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package revision import ( "context" "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) // RevisionRepo revision repository type RevisionRepo interface { AddRevision(ctx context.Context, revision *entity.Revision, autoUpdateRevisionID bool) (err error) GetRevisionByID(ctx context.Context, revisionID string) (revision *entity.Revision, exist bool, err error) GetLastRevisionByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) GetLastRevisionByFileURL(ctx context.Context, fileURL string) (revision *entity.Revision, exist bool, err error) GetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error) UpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) GetUnreviewedRevisionPage(ctx context.Context, page, pageSize int, objectTypes []int) ([]*entity.Revision, int64, error) CountUnreviewedRevision(ctx context.Context, objectTypeList []int) (count int64, err error) UpdateStatus(ctx context.Context, id string, status int, reviewUserID string) (err error) } ================================================ FILE: internal/service/revision_common/revision_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package revision_common import ( "context" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/service/revision" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/jinzhu/copier" ) // RevisionService user service type RevisionService struct { revisionRepo revision.RevisionRepo userRepo usercommon.UserRepo } func NewRevisionService(revisionRepo revision.RevisionRepo, userRepo usercommon.UserRepo, ) *RevisionService { return &RevisionService{ revisionRepo: revisionRepo, userRepo: userRepo, } } func (rs *RevisionService) GetUnreviewedRevisionCount(ctx context.Context, req *schema.RevisionSearch) (count int64, err error) { if len(req.GetCanReviewObjectTypes()) == 0 { return 0, nil } return rs.revisionRepo.CountUnreviewedRevision(ctx, req.GetCanReviewObjectTypes()) } // AddRevision add revision // // autoUpdateRevisionID bool : if autoUpdateRevisionID is true , the object.revision_id will be updated, // if not need auto update object.revision_id, it must be false. // example: user can edit the object, but need audit, the revision_id will be updated when admin approved func (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) ( revisionID string, err error) { req.ObjectID = uid.DeShortID(req.ObjectID) rev := &entity.Revision{} _ = copier.Copy(rev, req) err = rs.revisionRepo.AddRevision(ctx, rev, autoUpdateRevisionID) if err != nil { return "", err } return rev.ID, nil } // GetRevision get revision func (rs *RevisionService) GetRevision(ctx context.Context, revisionID string) ( revision *entity.Revision, err error) { revisionInfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, revisionID) if err != nil { log.Error(err) return nil, err } if !exist { return nil, errors.BadRequest(reason.ObjectNotFound) } return revisionInfo, nil } // ExistUnreviewedByObjectID func (rs *RevisionService) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) { objectID = uid.DeShortID(objectID) revision, exist, err = rs.revisionRepo.ExistUnreviewedByObjectID(ctx, objectID) return revision, exist, err } ================================================ FILE: internal/service/role/power_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package role import ( "context" "github.com/apache/answer/internal/entity" ) // PowerRepo power repository type PowerRepo interface { GetPowerList(ctx context.Context, power *entity.Power) (powers []*entity.Power, err error) } ================================================ FILE: internal/service/role/role_power_rel_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package role import ( "context" ) // RolePowerRelRepo rolePowerRel repository type RolePowerRelRepo interface { GetRolePowerTypeList(ctx context.Context, roleID int) (powers []string, err error) } // RolePowerRelService user service type RolePowerRelService struct { rolePowerRelRepo RolePowerRelRepo userRoleRelService *UserRoleRelService } // NewRolePowerRelService new role power rel service func NewRolePowerRelService(rolePowerRelRepo RolePowerRelRepo, userRoleRelService *UserRoleRelService) *RolePowerRelService { return &RolePowerRelService{ rolePowerRelRepo: rolePowerRelRepo, userRoleRelService: userRoleRelService, } } // GetRolePowerList get role power list func (rs *RolePowerRelService) GetRolePowerList(ctx context.Context, roleID int) (powers []string, err error) { return rs.rolePowerRelRepo.GetRolePowerTypeList(ctx, roleID) } // GetUserPowerList get list all func (rs *RolePowerRelService) GetUserPowerList(ctx context.Context, userID string) (powers []string, err error) { roleID, err := rs.userRoleRelService.GetUserRole(ctx, userID) if err != nil { return nil, err } return rs.rolePowerRelRepo.GetRolePowerTypeList(ctx, roleID) } ================================================ FILE: internal/service/role/role_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package role import ( "context" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/jinzhu/copier" ) const ( // Since there is currently no need to edit roles to add roles and other operations, // the current role information is translated directly. // Later on, when the relevant ability is available, it can be adjusted by the user himself. RoleUserID = 1 RoleAdminID = 2 RoleModeratorID = 3 roleUserName = "User" roleAdminName = "Admin" roleModeratorName = "Moderator" trRoleNameUser = "role.name.user" trRoleNameAdmin = "role.name.admin" trRoleNameModerator = "role.name.moderator" trRoleDescriptionUser = "role.description.user" trRoleDescriptionAdmin = "role.description.admin" trRoleDescriptionModerator = "role.description.moderator" ) // RoleRepo role repository type RoleRepo interface { GetRoleAllList(ctx context.Context) (roles []*entity.Role, err error) GetRoleAllMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) } // RoleService user service type RoleService struct { roleRepo RoleRepo } func NewRoleService(roleRepo RoleRepo) *RoleService { return &RoleService{ roleRepo: roleRepo, } } // GetRoleList get role list all func (rs *RoleService) GetRoleList(ctx context.Context) (resp []*schema.GetRoleResp, err error) { roles, err := rs.roleRepo.GetRoleAllList(ctx) if err != nil { return } for _, role := range roles { rs.translateRole(ctx, role) } resp = []*schema.GetRoleResp{} _ = copier.Copy(&resp, roles) return } func (rs *RoleService) GetRoleMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) { return rs.roleRepo.GetRoleAllMapping(ctx) } func (rs *RoleService) translateRole(ctx context.Context, role *entity.Role) { switch role.Name { case roleUserName: role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameUser) role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser) case roleAdminName: role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin) role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin) case roleModeratorName: role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator) role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator) } } ================================================ FILE: internal/service/role/user_role_rel_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package role import ( "context" "github.com/apache/answer/internal/entity" ) // UserRoleRelRepo userRoleRel repository type UserRoleRelRepo interface { SaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error) GetUserRoleRelList(ctx context.Context, userIDs []string) (userRoleRelList []*entity.UserRoleRel, err error) GetUserRoleRelListByRoleID(ctx context.Context, roleIDs []int) ( userRoleRelList []*entity.UserRoleRel, err error) GetUserRoleRel(ctx context.Context, userID string) (rolePowerRel *entity.UserRoleRel, exist bool, err error) } // UserRoleRelService user service type UserRoleRelService struct { userRoleRelRepo UserRoleRelRepo roleService *RoleService } // NewUserRoleRelService new user role rel service func NewUserRoleRelService(userRoleRelRepo UserRoleRelRepo, roleService *RoleService) *UserRoleRelService { return &UserRoleRelService{ userRoleRelRepo: userRoleRelRepo, roleService: roleService, } } // SaveUserRole save user role func (us *UserRoleRelService) SaveUserRole(ctx context.Context, userID string, roleID int) (err error) { return us.userRoleRelRepo.SaveUserRoleRel(ctx, userID, roleID) } // GetUserRoleMapping get user role mapping func (us *UserRoleRelService) GetUserRoleMapping(ctx context.Context, userIDs []string) ( userRoleMapping map[string]*entity.Role, err error) { userRoleMapping = make(map[string]*entity.Role, 0) roleMapping, err := us.roleService.GetRoleMapping(ctx) if err != nil { return userRoleMapping, err } if len(roleMapping) == 0 { return userRoleMapping, nil } relMapping, err := us.GetUserRoleRelMapping(ctx, userIDs) if err != nil { return userRoleMapping, err } // default role is user defaultRole := roleMapping[1] for _, userID := range userIDs { roleID, ok := relMapping[userID] if !ok { userRoleMapping[userID] = defaultRole continue } userRoleMapping[userID] = roleMapping[roleID] if userRoleMapping[userID] == nil { userRoleMapping[userID] = defaultRole } } return userRoleMapping, nil } // GetUserRoleRelMapping get user role rel mapping func (us *UserRoleRelService) GetUserRoleRelMapping(ctx context.Context, userIDs []string) ( userRoleRelMapping map[string]int, err error) { userRoleRelMapping = make(map[string]int, 0) relList, err := us.userRoleRelRepo.GetUserRoleRelList(ctx, userIDs) if err != nil { return userRoleRelMapping, err } for _, rel := range relList { userRoleRelMapping[rel.UserID] = rel.RoleID } return userRoleRelMapping, nil } // GetUserRole get user role func (us *UserRoleRelService) GetUserRole(ctx context.Context, userID string) (roleID int, err error) { rolePowerRel, exist, err := us.userRoleRelRepo.GetUserRoleRel(ctx, userID) if err != nil { return 0, err } if !exist { // set default role return 1, nil } return rolePowerRel.RoleID, nil } // GetUserByRoleID get user by role id func (us *UserRoleRelService) GetUserByRoleID(ctx context.Context, roleIDs []int) (rel []*entity.UserRoleRel, err error) { rolePowerRels, err := us.userRoleRelRepo.GetUserRoleRelListByRoleID(ctx, roleIDs) if err != nil { return nil, err } return rolePowerRels, nil } ================================================ FILE: internal/service/search_common/search.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package search_common import ( "context" "github.com/apache/answer/internal/schema" "github.com/apache/answer/plugin" ) type SearchRepo interface { SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult, words []string) (resp []*schema.SearchResult, err error) } ================================================ FILE: internal/service/search_parser/search_parser.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package search_parser import ( "context" "fmt" "regexp" "strings" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/tag_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/converter" ) type SearchParser struct { tagCommonService *tag_common.TagCommonService userCommon *usercommon.UserCommon } func NewSearchParser(tagCommonService *tag_common.TagCommonService, userCommon *usercommon.UserCommon) *SearchParser { return &SearchParser{ tagCommonService: tagCommonService, userCommon: userCommon, } } // ParseStructure parse search structure, maybe match one of type all/questions/answers, // but if match two type, it will return false func (sp *SearchParser) ParseStructure(ctx context.Context, dto *schema.SearchDTO) (cond *schema.SearchCondition) { cond = &schema.SearchCondition{} var ( query = dto.Query limitWords = 5 ) // match tags cond.Tags = sp.parseTags(ctx, &query) // match all cond.UserID = sp.parseUserID(ctx, &query, dto.UserID) cond.VoteAmount = sp.parseVotes(&query) cond.Words = sp.parseWithin(&query) // match questions cond.NotAccepted = sp.parseNotAccepted(&query) if cond.NotAccepted { cond.TargetType = constant.QuestionObjectType } cond.Views = sp.parseViews(&query) if cond.Views != -1 { cond.TargetType = constant.QuestionObjectType } cond.AnswerAmount = sp.parseAnswers(&query) if cond.AnswerAmount != -1 { cond.TargetType = constant.QuestionObjectType } // match answers cond.Accepted = sp.parseAccepted(&query) if cond.Accepted { cond.TargetType = constant.AnswerObjectType } cond.QuestionID = sp.parseQuestionID(&query) if cond.QuestionID != "" { cond.TargetType = constant.AnswerObjectType } if sp.parseIsQuestion(&query) { cond.TargetType = constant.QuestionObjectType } if sp.parseIsAnswer(&query) { cond.TargetType = constant.AnswerObjectType } if len(strings.TrimSpace(query)) > 0 { words := strings.Split(strings.TrimSpace(query), " ") cond.Words = append(cond.Words, words...) } // check limit words if len(cond.Words) > limitWords { cond.Words = cond.Words[:limitWords] } return } // parseTags parse search tags, return tag ids array func (sp *SearchParser) parseTags(ctx context.Context, query *string) (tags [][]string) { var ( // expire tag pattern exprTag = `\[(.*?)\]` q = *query limit = 5 ) re := regexp.MustCompile(exprTag) res := re.FindAllStringSubmatch(q, -1) if len(res) == 0 { return } tags = make([][]string, 0) for _, item := range res { tagGroup := make([]string, 0) tag, exists, err := sp.tagCommonService.GetTagBySlugName(ctx, item[1]) if err != nil || !exists { continue } tagGroup = append(tagGroup, tag.ID) if tag.MainTagID > 0 { tagGroup = append(tagGroup, fmt.Sprintf("%d", tag.MainTagID)) } synIDs, err := sp.tagCommonService.GetTagIDsByMainTagID(ctx, tag.ID) if err != nil || !exists { continue } tagGroup = append(tagGroup, tag.ID) tagGroup = append(tagGroup, synIDs...) tagGroup = converter.UniqueArray(tagGroup) tags = append(tags, tagGroup) } // limit maximum 5 tags if len(tags) > limit { tags = tags[:limit] } q = strings.TrimSpace(re.ReplaceAllString(q, "")) *query = q return } // parseUserID return user id or current login user id func (sp *SearchParser) parseUserID(ctx context.Context, query *string, currentUserID string) (userID string) { var ( exprUsername = `user:(\S+)` exprMe = "user:me" q = *query ) re := regexp.MustCompile(exprUsername) res := re.FindStringSubmatch(q) if strings.Contains(q, exprMe) { userID = currentUserID q = strings.ReplaceAll(q, exprMe, "") } else if len(res) > 1 { name := res[1] user, has, err := sp.userCommon.GetUserBasicInfoByUserName(ctx, name) if err == nil && has { userID = user.ID q = re.ReplaceAllString(q, "") } } *query = strings.TrimSpace(q) return } // parseVotes return the votes of search query func (sp *SearchParser) parseVotes(query *string) (votes int) { var ( expr = `score:(\d+)` q = *query ) votes = -1 re := regexp.MustCompile(expr) res := re.FindStringSubmatch(q) if len(res) > 1 { votes = converter.StringToInt(res[1]) q = re.ReplaceAllString(q, "") } *query = strings.TrimSpace(q) return } // parseWithin parse quotes within words like: "hello world" func (sp *SearchParser) parseWithin(query *string) (words []string) { var ( q = *query expr = `(?U)(".+")` ) re := regexp.MustCompile(expr) matches := re.FindAllStringSubmatch(q, -1) words = []string{} for _, match := range matches { if len(match[1]) == 0 { continue } words = append(words, match[1]) } q = re.ReplaceAllString(q, "") *query = strings.TrimSpace(q) return } // parseNotAccepted return the question has not accepted the answer func (sp *SearchParser) parseNotAccepted(query *string) (notAccepted bool) { var ( q = *query expr = `hasaccepted:no` ) if strings.Contains(q, expr) { q = strings.ReplaceAll(q, expr, "") notAccepted = true } *query = strings.TrimSpace(q) return } // parseIsQuestion check the result if only limit question or not func (sp *SearchParser) parseIsQuestion(query *string) (isQuestion bool) { var ( q = *query expr = `is:question` ) if strings.Contains(q, expr) { q = strings.ReplaceAll(q, expr, "") isQuestion = true } *query = strings.TrimSpace(q) return } // parseViews check search has views or not func (sp *SearchParser) parseViews(query *string) (views int) { var ( q = *query expr = `views:(\d+)` ) views = -1 re := regexp.MustCompile(expr) res := re.FindStringSubmatch(q) if len(res) > 1 { views = converter.StringToInt(res[1]) q = re.ReplaceAllString(q, "") } *query = strings.TrimSpace(q) return } // parseAnswers check whether specified answer count for question func (sp *SearchParser) parseAnswers(query *string) (answers int) { var ( q = *query expr = `answers:(\d+)` ) answers = -1 re := regexp.MustCompile(expr) res := re.FindStringSubmatch(q) if len(res) > 1 { answers = converter.StringToInt(res[1]) q = re.ReplaceAllString(q, "") } *query = strings.TrimSpace(q) return } // parseAccepted check the search is limit accepted answer or not func (sp *SearchParser) parseAccepted(query *string) (accepted bool) { var ( q = *query expr = `isaccepted:yes` ) if strings.Contains(q, expr) { accepted = true q = strings.ReplaceAll(q, expr, "") } *query = strings.TrimSpace(q) return } // parseQuestionID check whether specified question's id func (sp *SearchParser) parseQuestionID(query *string) (questionID string) { var ( q = *query expr = `inquestion:(\d+)` ) re := regexp.MustCompile(expr) res := re.FindStringSubmatch(q) if len(res) == 2 { questionID = res[1] q = re.ReplaceAllString(q, "") } *query = strings.TrimSpace(q) return } // parseIsAnswer check the result if only limit answer or not func (sp *SearchParser) parseIsAnswer(query *string) (isAnswer bool) { var ( q = *query expr = `is:answer` ) if strings.Contains(q, expr) { isAnswer = true q = strings.ReplaceAll(q, expr, "") } *query = strings.TrimSpace(q) return } ================================================ FILE: internal/service/service_config/service_config.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package service_config type ServiceConfig struct { UploadPath string `json:"upload_path" mapstructure:"upload_path" yaml:"upload_path"` CleanUpUploads bool `json:"clean_up_uploads" mapstructure:"clean_up_uploads" yaml:"clean_up_uploads"` CleanOrphanUploadsPeriodHours int `json:"clean_orphan_uploads_period_hours" mapstructure:"clean_orphan_uploads_period_hours" yaml:"clean_orphan_uploads_period_hours"` PurgeDeletedFilesPeriodDays int `json:"purge_deleted_files_period_days" mapstructure:"purge_deleted_files_period_days" yaml:"purge_deleted_files_period_days"` } ================================================ FILE: internal/service/siteinfo/siteinfo_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package siteinfo import ( "context" "encoding/json" errpkg "errors" "fmt" "strings" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/config" "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/file_record" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/siteinfo_common" tagcommon "github.com/apache/answer/internal/service/tag_common" "github.com/apache/answer/plugin" "github.com/go-resty/resty/v2" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) type SiteInfoService struct { siteInfoRepo siteinfo_common.SiteInfoRepo siteInfoCommonService siteinfo_common.SiteInfoCommonService emailService *export.EmailService tagCommonService *tagcommon.TagCommonService configService *config.ConfigService questioncommon *questioncommon.QuestionCommon fileRecordService *file_record.FileRecordService } func NewSiteInfoService( siteInfoRepo siteinfo_common.SiteInfoRepo, siteInfoCommonService siteinfo_common.SiteInfoCommonService, emailService *export.EmailService, tagCommonService *tagcommon.TagCommonService, configService *config.ConfigService, questioncommon *questioncommon.QuestionCommon, fileRecordService *file_record.FileRecordService, ) *SiteInfoService { plugin.RegisterGetSiteURLFunc(func() string { generalSiteInfo, err := siteInfoCommonService.GetSiteGeneral(context.Background()) if err != nil { log.Error(err) return "" } return generalSiteInfo.SiteUrl }) return &SiteInfoService{ siteInfoRepo: siteInfoRepo, siteInfoCommonService: siteInfoCommonService, emailService: emailService, tagCommonService: tagCommonService, configService: configService, questioncommon: questioncommon, fileRecordService: fileRecordService, } } // GetSiteGeneral get site info general func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { return s.siteInfoCommonService.GetSiteGeneral(ctx) } // GetSiteInterface get site info interface func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceSettingsResp, err error) { return s.siteInfoCommonService.GetSiteInterface(ctx) } // GetSiteUsersSettings get site info users settings func (s *SiteInfoService) GetSiteUsersSettings(ctx context.Context) (resp *schema.SiteUsersSettingsResp, err error) { return s.siteInfoCommonService.GetSiteUsersSettings(ctx) } // GetSiteBranding get site info branding func (s *SiteInfoService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) { return s.siteInfoCommonService.GetSiteBranding(ctx) } // GetSiteUsers get site info about users func (s *SiteInfoService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) { return s.siteInfoCommonService.GetSiteUsers(ctx) } // GetSiteTag get site info write func (s *SiteInfoService) GetSiteTag(ctx context.Context) (resp *schema.SiteTagsResp, err error) { resp, err = s.siteInfoCommonService.GetSiteTag(ctx) if err != nil { log.Error(err) return resp, nil } resp.RecommendTags, err = s.tagCommonService.GetSiteWriteRecommendTag(ctx) if err != nil { log.Error(err) } resp.ReservedTags, err = s.tagCommonService.GetSiteWriteReservedTag(ctx) if err != nil { log.Error(err) } return resp, nil } // GetSiteQuestion get site questions settings func (s *SiteInfoService) GetSiteQuestion(ctx context.Context) (resp *schema.SiteQuestionsResp, err error) { return s.siteInfoCommonService.GetSiteQuestion(ctx) } // GetSiteAdvanced get site advanced settings func (s *SiteInfoService) GetSiteAdvanced(ctx context.Context) (resp *schema.SiteAdvancedResp, err error) { return s.siteInfoCommonService.GetSiteAdvanced(ctx) } // GetSitePolicies get site legal info func (s *SiteInfoService) GetSitePolicies(ctx context.Context) (resp *schema.SitePoliciesResp, err error) { return s.siteInfoCommonService.GetSitePolicies(ctx) } // GetSiteSecurity get site security info func (s *SiteInfoService) GetSiteSecurity(ctx context.Context) (resp *schema.SiteSecurityResp, err error) { return s.siteInfoCommonService.GetSiteSecurity(ctx) } // GetSiteLogin get site login info func (s *SiteInfoService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) { return s.siteInfoCommonService.GetSiteLogin(ctx) } // GetSiteCustomCssHTML get site custom css html config func (s *SiteInfoService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) { return s.siteInfoCommonService.GetSiteCustomCssHTML(ctx) } // GetSiteTheme get site theme config func (s *SiteInfoService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) { return s.siteInfoCommonService.GetSiteTheme(ctx) } func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) { req.FormatSiteUrl() content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypeGeneral, Content: string(content), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeGeneral, data) } func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) { // If the language is invalid, set it to the default language "en_US" if !translator.CheckLanguageIsValid(req.Language) { req.Language = "en_US" } content, _ := json.Marshal(req) data := entity.SiteInfo{ Type: constant.SiteTypeInterfaceSettings, Content: string(content), } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeInterfaceSettings, &data) } // SaveSiteUsersSettings save site users settings func (s *SiteInfoService) SaveSiteUsersSettings(ctx context.Context, req schema.SiteUsersSettingsReq) (err error) { content, _ := json.Marshal(req) data := entity.SiteInfo{ Type: constant.SiteTypeInterfaceSettings, Content: string(content), } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsersSettings, &data) } // SaveSiteBranding save site branding information func (s *SiteInfoService) SaveSiteBranding(ctx context.Context, req *schema.SiteBrandingReq) (err error) { content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypeBranding, Content: string(content), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeBranding, data) } // SaveSiteAdvanced save site advanced configuration func (s *SiteInfoService) SaveSiteAdvanced(ctx context.Context, req *schema.SiteAdvancedReq) (resp any, err error) { content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypeAdvanced, Content: string(content), Status: 1, } return nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeAdvanced, data) } // SaveSiteQuestions save site questions configuration func (s *SiteInfoService) SaveSiteQuestions(ctx context.Context, req *schema.SiteQuestionsReq) (resp any, err error) { content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypeQuestions, Content: string(content), Status: 1, } return nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeQuestions, data) } // SaveSiteTags save site tags configuration func (s *SiteInfoService) SaveSiteTags(ctx context.Context, req *schema.SiteTagsReq) (resp any, err error) { recommendTags, reservedTags := make([]string, 0), make([]string, 0) recommendTagMapping, reservedTagMapping := make(map[string]bool), make(map[string]bool) for _, tag := range req.ReservedTags { if !recommendTagMapping[tag.SlugName] { reservedTagMapping[tag.SlugName] = true reservedTags = append(reservedTags, tag.SlugName) } } // recommend tags can't contain reserved tag for _, tag := range req.RecommendTags { if reservedTagMapping[tag.SlugName] { continue } if !recommendTagMapping[tag.SlugName] { recommendTagMapping[tag.SlugName] = true recommendTags = append(recommendTags, tag.SlugName) } } errData, err := s.tagCommonService.SetSiteWriteTag(ctx, recommendTags, reservedTags, req.UserID) if err != nil { return errData, err } content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypeTags, Content: string(content), Status: 1, } return nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeTags, data) } // SaveSitePolicies save site policies configuration func (s *SiteInfoService) SaveSitePolicies(ctx context.Context, req *schema.SitePoliciesReq) (err error) { content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypePolicies, Content: string(content), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypePolicies, data) } // SaveSiteSecurity save site security configuration func (s *SiteInfoService) SaveSiteSecurity(ctx context.Context, req *schema.SiteSecurityReq) (err error) { content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypeSecurity, Content: string(content), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeSecurity, data) } // SaveSiteLogin save site legal configuration func (s *SiteInfoService) SaveSiteLogin(ctx context.Context, req *schema.SiteLoginReq) (err error) { content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypeLogin, Content: string(content), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeLogin, data) } // SaveSiteCustomCssHTML save site custom html configuration func (s *SiteInfoService) SaveSiteCustomCssHTML(ctx context.Context, req *schema.SiteCustomCssHTMLReq) (err error) { content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypeCustomCssHTML, Content: string(content), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeCustomCssHTML, data) } // SaveSiteTheme save site custom html configuration func (s *SiteInfoService) SaveSiteTheme(ctx context.Context, req *schema.SiteThemeReq) (err error) { if len(req.Layout) == 0 { req.Layout = constant.ThemeLayoutFullWidth } content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypeTheme, Content: string(content), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeTheme, data) } // SaveSiteUsers save site users func (s *SiteInfoService) SaveSiteUsers(ctx context.Context, req *schema.SiteUsersReq) (err error) { content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypeUsers, Content: string(content), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsers, data) } // GetSiteAI get site AI configuration func (s *SiteInfoService) GetSiteAI(ctx context.Context) (resp *schema.SiteAIResp, err error) { resp, err = s.siteInfoCommonService.GetSiteAI(ctx) if err != nil { return nil, err } aiProvider, err := s.GetAIProvider(ctx) if err != nil { return nil, err } providerMapping := make(map[string]*schema.SiteAIProvider) for _, provider := range resp.SiteAIProviders { providerMapping[provider.Provider] = provider } providers := make([]*schema.SiteAIProvider, 0) for _, p := range aiProvider { if provider, ok := providerMapping[p.Name]; ok { providers = append(providers, provider) } else { providers = append(providers, &schema.SiteAIProvider{ Provider: p.Name, }) } } resp.SiteAIProviders = providers s.maskAIKeys(resp) return resp, nil } // SaveSiteAI save site AI configuration func (s *SiteInfoService) SaveSiteAI(ctx context.Context, req *schema.SiteAIReq) (err error) { if err := s.restoreMaskedAIKeys(ctx, req); err != nil { return err } if req.PromptConfig == nil { req.PromptConfig = &schema.AIPromptConfig{ ZhCN: constant.DefaultAIPromptConfigZhCN, EnUS: constant.DefaultAIPromptConfigEnUS, } } aiProvider, err := s.GetAIProvider(ctx) if err != nil { return err } providerMapping := make(map[string]*schema.SiteAIProvider) for _, provider := range req.SiteAIProviders { providerMapping[provider.Provider] = provider } providers := make([]*schema.SiteAIProvider, 0) for _, p := range aiProvider { if provider, ok := providerMapping[p.Name]; ok { if len(provider.APIHost) == 0 && provider.Provider == req.ChosenProvider { provider.APIHost = p.DefaultAPIHost } providers = append(providers, provider) } else { providers = append(providers, &schema.SiteAIProvider{ Provider: p.Name, APIHost: p.DefaultAPIHost, }) } } req.SiteAIProviders = providers content, _ := json.Marshal(req) siteInfo := &entity.SiteInfo{ Type: constant.SiteTypeAI, Content: string(content), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeAI, siteInfo) } func (s *SiteInfoService) maskAIKeys(resp *schema.SiteAIResp) { for _, provider := range resp.SiteAIProviders { if provider.APIKey == "" { continue } provider.APIKey = strings.Repeat("*", len(provider.APIKey)) } } func (s *SiteInfoService) restoreMaskedAIKeys(ctx context.Context, req *schema.SiteAIReq) error { hasMasked := false for _, provider := range req.SiteAIProviders { if provider.APIKey != "" && isAllMask(provider.APIKey) { hasMasked = true break } } if !hasMasked { return nil } current, err := s.siteInfoCommonService.GetSiteAI(ctx) if err != nil { return err } currentMapping := make(map[string]*schema.SiteAIProvider) for _, provider := range current.SiteAIProviders { currentMapping[provider.Provider] = provider } for _, provider := range req.SiteAIProviders { if provider.APIKey == "" || !isAllMask(provider.APIKey) { continue } if stored, ok := currentMapping[provider.Provider]; ok { provider.APIKey = stored.APIKey } } return nil } func isAllMask(value string) bool { return strings.Trim(value, "*") == "" } // GetSiteMCP get site MCP configuration func (s *SiteInfoService) GetSiteMCP(ctx context.Context) (resp *schema.SiteMCPResp, err error) { resp, err = s.siteInfoCommonService.GetSiteMCP(ctx) if err != nil { return nil, err } siteInfo, err := s.GetSiteGeneral(ctx) if err != nil { return nil, err } resp.Type = "Server-Sent Event (SSE)" resp.URL = fmt.Sprintf("%s/answer/api/v1/mcp/sse", siteInfo.SiteUrl) resp.HTTPHeader = "Authorization={key}" return } // SaveSiteMCP save site MCP configuration func (s *SiteInfoService) SaveSiteMCP(ctx context.Context, req *schema.SiteMCPReq) (err error) { content, _ := json.Marshal(req) siteInfo := &entity.SiteInfo{ Type: constant.SiteTypeMCP, Content: string(content), Status: 1, } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeMCP, siteInfo) } // GetSMTPConfig get smtp config func (s *SiteInfoService) GetSMTPConfig(ctx context.Context) (resp *schema.GetSMTPConfigResp, err error) { emailConfig, err := s.emailService.GetEmailConfig(ctx) if err != nil { return nil, err } resp = &schema.GetSMTPConfigResp{} _ = copier.Copy(resp, emailConfig) resp.SMTPPassword = strings.Repeat("*", len(resp.SMTPPassword)) return resp, nil } // UpdateSMTPConfig get smtp config func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.UpdateSMTPConfigReq) (err error) { emailConfig, err := s.emailService.GetEmailConfig(ctx) if err != nil { return err } ec := &export.EmailConfig{} _ = copier.Copy(ec, req) if len(ec.SMTPPassword) > 0 && ec.SMTPPassword == strings.Repeat("*", len(ec.SMTPPassword)) { ec.SMTPPassword = emailConfig.SMTPPassword } err = s.emailService.SetEmailConfig(ctx, ec) if err != nil { return err } if len(req.TestEmailRecipient) > 0 { title, body, err := s.emailService.TestTemplate(ctx) if err != nil { return err } go s.emailService.Send(ctx, req.TestEmailRecipient, title, body) } return nil } func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) { resp = &schema.SiteSeoReq{} if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil { return resp, err } siteSecurity, err := s.GetSiteSecurity(ctx) if err != nil { log.Error(err) return resp, nil } // If the site is set to privacy mode, prohibit crawling any page. if siteSecurity.LoginRequired { resp.Robots = "User-agent: *\nDisallow: /" return resp, nil } return resp, nil } func (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (err error) { content, _ := json.Marshal(req) data := entity.SiteInfo{ Type: constant.SiteTypeSeo, Content: string(content), } return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeSeo, &data) } func (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema.GetPrivilegesConfigResp, err error) { privilege := &schema.UpdatePrivilegesConfigReq{} if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypePrivileges, privilege); err != nil { return nil, err } privilegeOptions := schema.DefaultPrivilegeOptions if len(privilege.CustomPrivileges) > 0 { privilegeOptions = append(privilegeOptions, &schema.PrivilegeOption{ Level: schema.PrivilegeLevelCustom, LevelDesc: reason.PrivilegeLevelCustomDesc, Privileges: privilege.CustomPrivileges, }) } else { privilegeOptions = append(privilegeOptions, schema.DefaultCustomPrivilegeOption) } resp = &schema.GetPrivilegesConfigResp{ Options: s.translatePrivilegeOptions(ctx, privilegeOptions), SelectedLevel: schema.PrivilegeLevel3, } if privilege.Level > 0 { resp.SelectedLevel = privilege.Level } return resp, nil } func (s *SiteInfoService) translatePrivilegeOptions(ctx context.Context, privilegeOptions []*schema.PrivilegeOption) (options []*schema.PrivilegeOption) { la := handler.GetLangByCtx(ctx) for _, option := range privilegeOptions { op := &schema.PrivilegeOption{ Level: option.Level, LevelDesc: translator.Tr(la, option.LevelDesc), } for _, privilege := range option.Privileges { op.Privileges = append(op.Privileges, &constant.Privilege{ Key: privilege.Key, Label: translator.Tr(la, privilege.Label), Value: privilege.Value, }) } options = append(options, op) } return } func (s *SiteInfoService) UpdatePrivilegesConfig(ctx context.Context, req *schema.UpdatePrivilegesConfigReq) (err error) { var choosePrivileges []*constant.Privilege if req.Level == schema.PrivilegeLevelCustom { choosePrivileges = req.CustomPrivileges } else { chooseOption := schema.DefaultPrivilegeOptions.Choose(req.Level) if chooseOption == nil { return nil } choosePrivileges = chooseOption.Privileges } if choosePrivileges == nil { return nil } // update site info that user choose which privilege level if req.Level == schema.PrivilegeLevelCustom { privilegeMap := make(map[string]int) for _, privilege := range req.CustomPrivileges { privilegeMap[privilege.Key] = privilege.Value } var privileges []*constant.Privilege for _, privilege := range constant.RankAllPrivileges { privileges = append(privileges, &constant.Privilege{ Key: privilege.Key, Label: privilege.Label, Value: privilegeMap[privilege.Key], }) } req.CustomPrivileges = privileges } else { privilege := &schema.UpdatePrivilegesConfigReq{} if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypePrivileges, privilege); err != nil { return err } req.CustomPrivileges = privilege.CustomPrivileges } content, _ := json.Marshal(req) data := &entity.SiteInfo{ Type: constant.SiteTypePrivileges, Content: string(content), Status: 1, } err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypePrivileges, data) if err != nil { return err } // update privilege in config for _, privilege := range choosePrivileges { err = s.configService.UpdateConfig(ctx, privilege.Key, fmt.Sprintf("%d", privilege.Value)) if err != nil { return err } } return } func (s *SiteInfoService) CleanUpRemovedBrandingFiles( ctx context.Context, newBranding *schema.SiteBrandingReq, currentBranding *schema.SiteBrandingResp, ) error { var allErrors []error currentFiles := map[string]string{ "logo": currentBranding.Logo, "mobile_logo": currentBranding.MobileLogo, "square_icon": currentBranding.SquareIcon, "favicon": currentBranding.Favicon, } newFiles := map[string]string{ "logo": newBranding.Logo, "mobile_logo": newBranding.MobileLogo, "square_icon": newBranding.SquareIcon, "favicon": newBranding.Favicon, } for key, currentFile := range currentFiles { newFile := newFiles[key] if currentFile != "" && currentFile != newFile { fileRecord, err := s.fileRecordService.GetFileRecordByURL(ctx, currentFile) if err != nil { allErrors = append(allErrors, err) continue } if fileRecord == nil { err := errpkg.New("file record is nil for key " + key) allErrors = append(allErrors, err) continue } if err := s.fileRecordService.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil { allErrors = append(allErrors, err) } } } if len(allErrors) > 0 { return errpkg.Join(allErrors...) } return nil } func (s *SiteInfoService) GetAIProvider(ctx context.Context) (resp []*schema.GetAIProviderResp, err error) { resp = make([]*schema.GetAIProviderResp, 0) aiProviderConfig, err := s.configService.GetStringValue(context.TODO(), constant.AIConfigProvider) if err != nil { log.Error(err) return resp, nil } _ = json.Unmarshal([]byte(aiProviderConfig), &resp) return resp, nil } func (s *SiteInfoService) GetAIModels(ctx context.Context, req *schema.GetAIModelsReq) (resp []*schema.GetAIModelResp, err error) { resp = make([]*schema.GetAIModelResp, 0) if req.APIKey != "" && isAllMask(req.APIKey) { storedKey, err := s.getStoredAIKey(ctx, req.APIHost) if err != nil { return resp, err } if storedKey == "" { return resp, errors.BadRequest("api_key is required") } req.APIKey = storedKey } r := resty.New() r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", req.APIKey)) r.SetHeader("Content-Type", "application/json") respBody, err := r.R().Get(req.APIHost + "/v1/models") if err != nil { log.Error(err) return resp, errors.BadRequest(fmt.Sprintf("failed to get AI models %s", err.Error())) } if !respBody.IsSuccess() { log.Error(fmt.Sprintf("failed to get AI models, status code: %d, body: %s", respBody.StatusCode(), respBody.String())) return resp, errors.BadRequest(fmt.Sprintf("failed to get AI models, response: %s", respBody.String())) } data := schema.GetAIModelsResp{} _ = json.Unmarshal(respBody.Body(), &data) for _, model := range data.Data { resp = append(resp, &schema.GetAIModelResp{ Id: model.Id, Object: model.Object, Created: model.Created, OwnedBy: model.OwnedBy, }) } return resp, nil } func (s *SiteInfoService) getStoredAIKey(ctx context.Context, apiHost string) (string, error) { current, err := s.siteInfoCommonService.GetSiteAI(ctx) if err != nil { return "", err } apiHost = strings.TrimRight(apiHost, "/") for _, provider := range current.SiteAIProviders { if strings.TrimRight(provider.APIHost, "/") == apiHost && provider.APIKey != "" { return provider.APIKey, nil } } if current.ChosenProvider != "" { for _, provider := range current.SiteAIProviders { if provider.Provider == current.ChosenProvider { return provider.APIKey, nil } } } return "", nil } ================================================ FILE: internal/service/siteinfo_common/siteinfo_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package siteinfo_common import ( "context" "encoding/json" "html" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/pkg/gravatar" "github.com/segmentfault/pacman/log" ) //go:generate mockgen -source=./siteinfo_service.go -destination=../mock/siteinfo_repo_mock.go -package=mock type SiteInfoRepo interface { SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error) GetByType(ctx context.Context, siteType string, withoutCache ...bool) (siteInfo *entity.SiteInfo, exist bool, err error) IsBrandingFileUsed(ctx context.Context, filePath string) (bool, error) } // siteInfoCommonService site info common service type siteInfoCommonService struct { siteInfoRepo SiteInfoRepo } type SiteInfoCommonService interface { GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceSettingsResp, err error) GetSiteUsersSettings(ctx context.Context) (resp *schema.SiteUsersSettingsResp, err error) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) FormatAvatar(ctx context.Context, originalAvatarData, email string, userStatus int) *schema.AvatarInfo FormatListAvatar(ctx context.Context, userList []*entity.User) (userID2AvatarMapping map[string]*schema.AvatarInfo) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) GetSiteAdvanced(ctx context.Context) (resp *schema.SiteAdvancedResp, err error) GetSiteQuestion(ctx context.Context) (resp *schema.SiteQuestionsResp, err error) GetSiteTag(ctx context.Context) (resp *schema.SiteTagsResp, err error) GetSitePolicies(ctx context.Context) (resp *schema.SitePoliciesResp, err error) GetSiteSecurity(ctx context.Context) (resp *schema.SiteSecurityResp, err error) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) GetSiteInfoByType(ctx context.Context, siteType string, resp any) (err error) IsBrandingFileUsed(ctx context.Context, filePath string) bool GetSiteAI(ctx context.Context) (resp *schema.SiteAIResp, err error) GetSiteMCP(ctx context.Context) (resp *schema.SiteMCPResp, err error) } // NewSiteInfoCommonService new site info common service func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) SiteInfoCommonService { return &siteInfoCommonService{ siteInfoRepo: siteInfoRepo, } } // GetSiteGeneral get site info general func (s *siteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { resp = &schema.SiteGeneralResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil { return nil, err } resp.Name = html.UnescapeString(resp.Name) return resp, nil } // GetSiteInterface get site info interface func (s *siteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceSettingsResp, err error) { resp = &schema.SiteInterfaceSettingsResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeInterfaceSettings, resp); err != nil { return nil, err } return resp, nil } // GetSiteUsersSettings get site info interface func (s *siteInfoCommonService) GetSiteUsersSettings(ctx context.Context) (resp *schema.SiteUsersSettingsResp, err error) { resp = &schema.SiteUsersSettingsResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsersSettings, resp); err != nil { return nil, err } return resp, nil } // GetSiteBranding get site info branding func (s *siteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) { resp = &schema.SiteBrandingResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil { return nil, err } return resp, nil } // GetSiteUsers get site info about users func (s *siteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) { resp = &schema.SiteUsersResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsers, resp); err != nil { return nil, err } return resp, nil } // FormatAvatar format avatar func (s *siteInfoCommonService) FormatAvatar(ctx context.Context, originalAvatarData, email string, userStatus int) *schema.AvatarInfo { gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx) return s.selectedAvatar(originalAvatarData, defaultAvatar, gravatarBaseURL, email, userStatus) } // FormatListAvatar format avatar func (s *siteInfoCommonService) FormatListAvatar(ctx context.Context, userList []*entity.User) ( avatarMapping map[string]*schema.AvatarInfo) { gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx) avatarMapping = make(map[string]*schema.AvatarInfo) for _, user := range userList { avatarMapping[user.ID] = s.selectedAvatar(user.Avatar, defaultAvatar, gravatarBaseURL, user.EMail, user.Status) } return avatarMapping } func (s *siteInfoCommonService) getAvatarDefaultConfig(ctx context.Context) (string, string) { gravatarBaseURL, defaultAvatar := constant.DefaultGravatarBaseURL, constant.DefaultAvatar usersConfig, err := s.GetSiteUsersSettings(ctx) if err != nil { log.Error(err) } if len(usersConfig.GravatarBaseURL) > 0 { gravatarBaseURL = usersConfig.GravatarBaseURL } if len(usersConfig.DefaultAvatar) > 0 { defaultAvatar = usersConfig.DefaultAvatar } return gravatarBaseURL, defaultAvatar } func (s *siteInfoCommonService) selectedAvatar( originalAvatarData, defaultAvatar, gravatarBaseURL, email string, userStatus int) *schema.AvatarInfo { avatarInfo := &schema.AvatarInfo{} _ = json.Unmarshal([]byte(originalAvatarData), avatarInfo) if userStatus == entity.UserStatusDeleted { return &schema.AvatarInfo{ Type: constant.DefaultAvatar, } } if len(avatarInfo.Type) == 0 && defaultAvatar == constant.AvatarTypeGravatar { avatarInfo.Type = constant.AvatarTypeGravatar avatarInfo.Gravatar = gravatar.GetAvatarURL(gravatarBaseURL, email) } else if avatarInfo.Type == constant.AvatarTypeGravatar { avatarInfo.Gravatar = gravatar.GetAvatarURL(gravatarBaseURL, email) } return avatarInfo } // GetSiteWrite get site info write func (s *siteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) { resp = &schema.SiteWriteResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil { return nil, err } return resp, nil } // GetSiteAdvanced get site info advanced func (s *siteInfoCommonService) GetSiteAdvanced(ctx context.Context) (resp *schema.SiteAdvancedResp, err error) { resp = &schema.SiteAdvancedResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeAdvanced, resp); err != nil { return nil, err } return resp, nil } // GetSiteQuestion get site info question func (s *siteInfoCommonService) GetSiteQuestion(ctx context.Context) (resp *schema.SiteQuestionsResp, err error) { resp = &schema.SiteQuestionsResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeQuestions, resp); err != nil { return nil, err } return resp, nil } // GetSiteTag get site info tag func (s *siteInfoCommonService) GetSiteTag(ctx context.Context) (resp *schema.SiteTagsResp, err error) { resp = &schema.SiteTagsResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeTags, resp); err != nil { return nil, err } return resp, nil } // GetSitePolicies get site info policies func (s *siteInfoCommonService) GetSitePolicies(ctx context.Context) (resp *schema.SitePoliciesResp, err error) { resp = &schema.SitePoliciesResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypePolicies, resp); err != nil { return nil, err } return resp, nil } // GetSiteSecurity get site security config func (s *siteInfoCommonService) GetSiteSecurity(ctx context.Context) (resp *schema.SiteSecurityResp, err error) { resp = &schema.SiteSecurityResp{CheckUpdate: true} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeSecurity, resp); err != nil { return nil, err } return resp, nil } // GetSiteLogin get site login config func (s *siteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) { resp = &schema.SiteLoginResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil { return nil, err } return resp, nil } // GetSiteCustomCssHTML get site custom css html config func (s *siteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) { resp = &schema.SiteCustomCssHTMLResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil { return nil, err } return resp, nil } // GetSiteTheme get site theme func (s *siteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) { resp = &schema.SiteThemeResp{ ThemeOptions: schema.GetThemeOptions, Layout: constant.ThemeLayoutFullWidth, } if err = s.GetSiteInfoByType(ctx, constant.SiteTypeTheme, resp); err != nil { return nil, err } if resp.Layout == "" { resp.Layout = constant.ThemeLayoutFullWidth } resp.TrTheme(ctx) return resp, nil } // GetSiteSeo get site seo func (s *siteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) { resp = &schema.SiteSeoResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil { return nil, err } return resp, nil } func (s *siteInfoCommonService) EnableShortID(ctx context.Context) (enabled bool) { siteSeo, err := s.GetSiteSeo(ctx) if err != nil { log.Error(err) return false } return siteSeo.IsShortLink() } func (s *siteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp any) (err error) { siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, siteType) if err != nil { return err } if !exist { return nil } _ = json.Unmarshal([]byte(siteInfo.Content), resp) return nil } func (s *siteInfoCommonService) IsBrandingFileUsed(ctx context.Context, filePath string) bool { used, err := s.siteInfoRepo.IsBrandingFileUsed(ctx, filePath) if err != nil { log.Errorf("error checking if branding file is used: %v", err) // will try again with the next clean up return true } return used } // GetSiteAI get site AI configuration func (s *siteInfoCommonService) GetSiteAI(ctx context.Context) (resp *schema.SiteAIResp, err error) { resp = &schema.SiteAIResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeAI, resp); err != nil { return nil, err } return resp, nil } // GetSiteMCP get site AI configuration func (s *siteInfoCommonService) GetSiteMCP(ctx context.Context) (resp *schema.SiteMCPResp, err error) { resp = &schema.SiteMCPResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeMCP, resp); err != nil { return nil, err } return resp, nil } ================================================ FILE: internal/service/siteinfo_common/siteinfo_service_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package siteinfo_common import ( "context" "testing" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) var ( mockSiteInfoRepo *mock.MockSiteInfoRepo ) func mockInit(ctl *gomock.Controller) { mockSiteInfoRepo = mock.NewMockSiteInfoRepo(ctl) mockSiteInfoRepo.EXPECT().GetByType(gomock.Any(), constant.SiteTypeGeneral). Return(&entity.SiteInfo{Content: `{"name":"name"}`}, true, nil) } func TestSiteInfoCommonService_GetSiteGeneral(t *testing.T) { ctl := gomock.NewController(t) defer ctl.Finish() mockInit(ctl) siteInfoCommonService := NewSiteInfoCommonService(mockSiteInfoRepo) resp, err := siteInfoCommonService.GetSiteGeneral(context.TODO()) require.NoError(t, err) assert.Equal(t, "name", resp.Name) } ================================================ FILE: internal/service/tag/tag_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package tag import ( "context" "encoding/json" "strings" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/service/activityqueue" "github.com/apache/answer/internal/service/revision_common" "github.com/apache/answer/internal/service/siteinfo_common" tagcommonser "github.com/apache/answer/internal/service/tag_common" "github.com/apache/answer/pkg/htmltext" "github.com/jinzhu/copier" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/permission" "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // TagService user service type TagService struct { tagRepo tagcommonser.TagRepo tagCommonService *tagcommonser.TagCommonService revisionService *revision_common.RevisionService followCommon activity_common.FollowRepo siteInfoService siteinfo_common.SiteInfoCommonService activityQueueService activityqueue.Service } // NewTagService new tag service func NewTagService( tagRepo tagcommonser.TagRepo, tagCommonService *tagcommonser.TagCommonService, revisionService *revision_common.RevisionService, followCommon activity_common.FollowRepo, siteInfoService siteinfo_common.SiteInfoCommonService, activityQueueService activityqueue.Service, ) *TagService { return &TagService{ tagRepo: tagRepo, tagCommonService: tagCommonService, revisionService: revisionService, followCommon: followCommon, siteInfoService: siteInfoService, activityQueueService: activityQueueService, } } // RemoveTag delete tag func (ts *TagService) RemoveTag(ctx context.Context, req *schema.RemoveTagReq) (err error) { // If the tag has associated problems, it cannot be deleted tagCount, err := ts.tagCommonService.CountTagRelByTagID(ctx, req.TagID) if err != nil { return err } if tagCount > 0 { return errors.BadRequest(reason.TagIsUsedCannotDelete) } // If the tag has associated problems, it cannot be deleted tagSynonymCount, err := ts.tagRepo.GetTagSynonymCount(ctx, req.TagID) if err != nil { return err } if tagSynonymCount > 0 { return errors.BadRequest(reason.TagIsUsedCannotDelete) } // tagRelRepo err = ts.tagRepo.RemoveTag(ctx, req.TagID) if err != nil { return err } ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: req.TagID, OriginalObjectID: req.TagID, ActivityTypeKey: constant.ActTagDeleted, }) return nil } // UpdateTag update tag func (ts *TagService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (err error) { return ts.tagCommonService.UpdateTag(ctx, req) } // RecoverTag recover tag func (ts *TagService) RecoverTag(ctx context.Context, req *schema.RecoverTagReq) (err error) { tagInfo, exist, err := ts.tagRepo.MustGetTagByNameOrID(ctx, req.TagID, "") if err != nil { return err } if !exist { return errors.BadRequest(reason.TagNotFound) } if tagInfo.Status != entity.TagStatusDeleted { return nil } err = ts.tagRepo.RecoverTag(ctx, req.TagID) if err != nil { return err } ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: req.TagID, OriginalObjectID: req.TagID, ActivityTypeKey: constant.ActTagUndeleted, }) return nil } // GetTagInfo get tag one func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq) (resp *schema.GetTagResp, err error) { var ( tagInfo *entity.Tag exist bool ) if len(req.ID) > 0 { tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, req.ID) } else { tagInfo, exist, err = ts.tagCommonService.GetTagBySlugName(ctx, req.Name) } // If user can recover deleted tag, try to search in all tags including deleted tags if !exist && req.CanRecover { tagInfo, exist, err = ts.tagRepo.MustGetTagByNameOrID(ctx, req.ID, req.Name) } if err != nil { return nil, err } if !exist { return nil, errors.NotFound(reason.TagNotFound) } resp = &schema.GetTagResp{} // if tag is synonyms get original tag info if tagInfo.MainTagID > 0 { tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID)) if err != nil { return nil, err } if !exist { return nil, errors.NotFound(reason.TagNotFound) } resp.MainTagSlugName = tagInfo.SlugName } resp.TagID = tagInfo.ID resp.CreatedAt = tagInfo.CreatedAt.Unix() resp.UpdatedAt = tagInfo.UpdatedAt.Unix() resp.SlugName = tagInfo.SlugName resp.DisplayName = tagInfo.DisplayName resp.OriginalText = tagInfo.OriginalText resp.ParsedText = tagInfo.ParsedText resp.Description = htmltext.FetchExcerpt(tagInfo.ParsedText, "...", 240) resp.FollowCount = tagInfo.FollowCount resp.QuestionCount = tagInfo.QuestionCount resp.Recommend = tagInfo.Recommend resp.Reserved = tagInfo.Reserved resp.IsFollower = ts.checkTagIsFollow(ctx, req.UserID, tagInfo.ID) resp.Status = entity.TagStatusDisplayMapping[tagInfo.Status] resp.MemberActions = permission.GetTagPermission(ctx, tagInfo.Status, req.CanEdit, req.CanDelete, req.CanMerge, req.CanRecover) resp.GetExcerpt() return resp, nil } // GetTagsBySlugName get tags by slug name func (ts *TagService) GetTagsBySlugName(ctx context.Context, req *schema.SearchTagsBySlugName) ( resp []*schema.GetTagBasicResp, err error) { resp = make([]*schema.GetTagBasicResp, 0) tagSlugNames := strings.Split(req.Tags, ",") if len(tagSlugNames) == 0 { return resp, nil } tagList, err := ts.tagCommonService.GetTagListByNames(ctx, tagSlugNames) if err != nil { return resp, err } for _, tag := range tagList { tagItem := &schema.GetTagBasicResp{} _ = copier.Copy(tagItem, tag) resp = append(resp, tagItem) } return resp, nil } // GetFollowingTags get following tags func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( resp []*schema.GetFollowingTagsResp, err error) { resp = make([]*schema.GetFollowingTagsResp, 0) if len(userID) == 0 { return resp, nil } objIDs, err := ts.followCommon.GetFollowIDs(ctx, userID, entity.Tag{}.TableName()) if err != nil { return nil, err } tagList, err := ts.tagCommonService.GetTagListByIDs(ctx, objIDs) if err != nil { return nil, err } for _, t := range tagList { tagInfo := &schema.GetFollowingTagsResp{ TagID: t.ID, SlugName: t.SlugName, DisplayName: t.DisplayName, Recommend: t.Recommend, Reserved: t.Reserved, } if t.MainTagID > 0 { mainTag, exist, err := ts.tagCommonService.GetTagByID(ctx, converter.IntToString(t.MainTagID)) if err != nil { return nil, err } if exist { tagInfo.MainTagSlugName = mainTag.SlugName } } resp = append(resp, tagInfo) } return resp, nil } // GetTagSynonyms get tag synonyms func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSynonymsReq) ( resp *schema.GetTagSynonymsResp, err error) { resp = &schema.GetTagSynonymsResp{Synonyms: make([]*schema.TagSynonym, 0)} tag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return } if !exist { return nil, errors.BadRequest(reason.TagNotFound) } var tagList []*entity.Tag var mainTagSlugName string if tag.MainTagID > 0 { tagList, err = ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: tag.MainTagID}) } else { tagList, err = ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tag.ID)}) } if err != nil { return } // get main tag slug name if tag.MainTagID > 0 { for _, tagInfo := range tagList { if tag.MainTagID == 0 { mainTagSlugName = tagInfo.SlugName break } } } else { mainTagSlugName = tag.SlugName } for _, t := range tagList { resp.Synonyms = append(resp.Synonyms, &schema.TagSynonym{ TagID: t.ID, SlugName: t.SlugName, DisplayName: t.DisplayName, MainTagSlugName: mainTagSlugName, }) } resp.MemberActions = permission.GetTagSynonymPermission(ctx, req.CanEdit) return } // UpdateTagSynonym add tag synonym func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTagSynonymReq) (err error) { // format tag slug name req.Format() addSynonymTagList := make([]string, 0) removeSynonymTagList := make([]string, 0) mainTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return err } if !exist { return errors.BadRequest(reason.TagNotFound) } // find all exist tag for _, item := range req.SynonymTagList { if item.SlugName == mainTagInfo.SlugName { return errors.BadRequest(reason.TagCannotSetSynonymAsItself) } addSynonymTagList = append(addSynonymTagList, item.SlugName) } tagListInDB, err := ts.tagCommonService.GetTagListByNames(ctx, addSynonymTagList) if err != nil { return err } existTagMapping := make(map[string]*entity.Tag, 0) for _, tag := range tagListInDB { existTagMapping[tag.SlugName] = tag } // add tag list needAddTagList := make([]*entity.Tag, 0) for _, tag := range req.SynonymTagList { if existTagMapping[tag.SlugName] != nil { continue } item := &entity.Tag{} item.SlugName = tag.SlugName item.DisplayName = tag.DisplayName item.OriginalText = tag.OriginalText item.ParsedText = tag.ParsedText item.Status = entity.TagStatusAvailable item.UserID = req.UserID needAddTagList = append(needAddTagList, item) } if len(needAddTagList) > 0 { err = ts.tagCommonService.AddTagList(ctx, needAddTagList) if err != nil { return err } // update tag revision for _, tag := range needAddTagList { existTagMapping[tag.SlugName] = tag revisionDTO := &schema.AddRevisionDTO{ UserID: req.UserID, ObjectID: tag.ID, Title: tag.SlugName, } tagInfoJson, _ := json.Marshal(tag) revisionDTO.Content = string(tagInfoJson) revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return err } ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: tag.ID, OriginalObjectID: tag.ID, ActivityTypeKey: constant.ActTagCreated, RevisionID: revisionID, }) } } // get all old synonyms list oldSynonymList, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(mainTagInfo.ID)}) if err != nil { return err } for _, oldSynonym := range oldSynonymList { if existTagMapping[oldSynonym.SlugName] == nil { removeSynonymTagList = append(removeSynonymTagList, oldSynonym.SlugName) } } // remove old synonyms if len(removeSynonymTagList) > 0 { err = ts.tagRepo.UpdateTagSynonym(ctx, removeSynonymTagList, 0, "") if err != nil { return err } } // update new synonyms if len(addSynonymTagList) > 0 { err = ts.tagRepo.UpdateTagSynonym(ctx, addSynonymTagList, converter.StringToInt64(req.TagID), mainTagInfo.SlugName) if err != nil { return err } } return nil } // GetTagWithPage get tag list page func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWithPageReq) (pageModel *pager.PageModel, err error) { tag := &entity.Tag{} _ = copier.Copy(tag, req) tag.UserID = "" page := req.Page pageSize := req.PageSize tags, total, err := ts.tagCommonService.GetTagPage(ctx, page, pageSize, tag, req.QueryCond) if err != nil { return } resp := make([]*schema.GetTagPageResp, 0) for _, tag := range tags { item := &schema.GetTagPageResp{ TagID: tag.ID, SlugName: tag.SlugName, Description: htmltext.FetchExcerpt(tag.ParsedText, "...", 240), DisplayName: tag.DisplayName, OriginalText: tag.OriginalText, ParsedText: tag.ParsedText, FollowCount: tag.FollowCount, QuestionCount: tag.QuestionCount, IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID), CreatedAt: tag.CreatedAt.Unix(), UpdatedAt: tag.UpdatedAt.Unix(), Recommend: tag.Recommend, Reserved: tag.Reserved, } item.GetExcerpt() resp = append(resp, item) } return pager.NewPageModel(total, resp), nil } // MergeTag merge tag func (ts *TagService) MergeTag(ctx context.Context, req *schema.MergeTagReq) (err error) { // 1. get source tag and its synonyms sourceTag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.SourceTagID) if err != nil { return err } if !exist { return errors.BadRequest(reason.TagNotFound) } sourceTagSynonyms, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(sourceTag.ID)}) if err != nil { return err } addSynonymTagList := make([]string, 0) addSynonymTagList = append(addSynonymTagList, sourceTag.SlugName) for _, tag := range sourceTagSynonyms { addSynonymTagList = append(addSynonymTagList, tag.SlugName) } // 2. get target tag targetTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TargetTagID) if err != nil { return err } if !exist { return errors.BadRequest(reason.TagNotFound) } // 3. update source tag and its synonyms as synonyms of target tag if len(addSynonymTagList) > 0 { err = ts.tagRepo.UpdateTagSynonym(ctx, addSynonymTagList, converter.StringToInt64(targetTagInfo.ID), targetTagInfo.SlugName) if err != nil { return err } } // 4. update tag followers err = ts.followCommon.MigrateFollowers(ctx, sourceTag.ID, targetTagInfo.ID, "follow") if err != nil { return err } // 5. update question tags err = ts.tagCommonService.MigrateTagQuestions(ctx, sourceTag.ID, targetTagInfo.ID) if err != nil { return err } err = ts.tagCommonService.RefreshTagQuestionCount(ctx, []string{targetTagInfo.ID, sourceTag.ID}) if err != nil { return err } return nil } // checkTagIsFollow get tag list page func (ts *TagService) checkTagIsFollow(ctx context.Context, userID, tagID string) bool { if len(userID) == 0 { return false } followed, err := ts.followCommon.IsFollowed(ctx, userID, tagID) if err != nil { log.Error(err) } return followed } ================================================ FILE: internal/service/tag_common/tag_common.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package tag_common import ( "context" "encoding/json" "fmt" "sort" "strings" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activityqueue" "github.com/apache/answer/internal/service/revision_common" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) type TagCommonRepo interface { AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) GetTagListByName(ctx context.Context, name string, recommend, reserved bool) (tagList []*entity.Tag, err error) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) GetTagByID(ctx context.Context, tagID string, includeDeleted bool) (tag *entity.Tag, exist bool, err error) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (tagList []*entity.Tag, total int64, err error) GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error) GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error) UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) } type TagRepo interface { RemoveTag(ctx context.Context, tagID string) (err error) UpdateTag(ctx context.Context, tag *entity.Tag) (err error) RecoverTag(ctx context.Context, tagID string) (err error) MustGetTagByNameOrID(ctx context.Context, tagID, slugName string) (tag *entity.Tag, exist bool, err error) UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string) (err error) GetTagSynonymCount(ctx context.Context, tagID string) (count int64, err error) GetIDsByMainTagId(ctx context.Context, mainTagID string) (tagIDs []string, err error) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) } type TagRelRepo interface { AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) RecoverTagRelListByObjectID(ctx context.Context, objectID string) (err error) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) EnableTagRelByIDs(ctx context.Context, ids []int64, hide bool) (err error) GetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) (tagRel *entity.TagRel, exist bool, err error) GetObjectTagRelList(ctx context.Context, objectId string) (tagListList []*entity.TagRel, err error) BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) GetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) (status int, err error) MigrateTagObjects(ctx context.Context, sourceTagId, targetTagId string) error } // TagCommonService user service type TagCommonService struct { revisionService *revision_common.RevisionService tagCommonRepo TagCommonRepo tagRelRepo TagRelRepo tagRepo TagRepo siteInfoService siteinfo_common.SiteInfoCommonService activityQueueService activityqueue.Service } // NewTagCommonService new tag service func NewTagCommonService( tagCommonRepo TagCommonRepo, tagRelRepo TagRelRepo, tagRepo TagRepo, revisionService *revision_common.RevisionService, siteInfoService siteinfo_common.SiteInfoCommonService, activityQueueService activityqueue.Service, ) *TagCommonService { return &TagCommonService{ tagCommonRepo: tagCommonRepo, tagRelRepo: tagRelRepo, tagRepo: tagRepo, revisionService: revisionService, siteInfoService: siteInfoService, activityQueueService: activityQueueService, } } // SearchTagLike get tag list all func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []schema.GetTagBasicResp, err error) { tags, err := ts.tagCommonRepo.GetTagListByName(ctx, req.Tag, len(req.Tag) == 0, false) if err != nil { return } ts.TagsFormatRecommendAndReserved(ctx, tags) mainTagId := make([]string, 0) for _, tag := range tags { if tag.MainTagID != 0 { mainTagId = append(mainTagId, converter.IntToString(tag.MainTagID)) } } mainTagMap := make(map[string]*entity.Tag) if len(mainTagId) > 0 { mainTagList, err := ts.tagCommonRepo.GetTagListByIDs(ctx, mainTagId) if err != nil { return nil, err } for _, tag := range mainTagList { mainTagMap[tag.ID] = tag } } for _, tag := range tags { if tag.MainTagID == 0 { continue } mainTagID := converter.IntToString(tag.MainTagID) if _, ok := mainTagMap[mainTagID]; ok { tag.ID = mainTagMap[mainTagID].ID tag.SlugName = mainTagMap[mainTagID].SlugName tag.DisplayName = mainTagMap[mainTagID].DisplayName tag.Reserved = mainTagMap[mainTagID].Reserved tag.Recommend = mainTagMap[mainTagID].Recommend } } resp = make([]schema.GetTagBasicResp, 0) repetitiveTag := make(map[string]bool) for _, tag := range tags { if _, ok := repetitiveTag[tag.SlugName]; !ok { item := schema.GetTagBasicResp{} item.TagID = tag.ID item.SlugName = tag.SlugName item.DisplayName = tag.DisplayName item.Recommend = tag.Recommend item.Reserved = tag.Reserved resp = append(resp, item) repetitiveTag[tag.SlugName] = true } } return resp, nil } func (ts *TagCommonService) GetSiteWriteRecommendTag(ctx context.Context) (tags []*schema.SiteWriteTag, err error) { tags = make([]*schema.SiteWriteTag, 0) list, err := ts.tagCommonRepo.GetRecommendTagList(ctx) if err != nil { return tags, err } for _, item := range list { tags = append(tags, &schema.SiteWriteTag{ SlugName: item.SlugName, DisplayName: item.DisplayName, }) } return tags, nil } func (ts *TagCommonService) GetSiteWriteReservedTag(ctx context.Context) (tags []*schema.SiteWriteTag, err error) { tags = make([]*schema.SiteWriteTag, 0) list, err := ts.tagCommonRepo.GetReservedTagList(ctx) if err != nil { return tags, err } for _, item := range list { tags = append(tags, &schema.SiteWriteTag{ SlugName: item.SlugName, DisplayName: item.DisplayName, }) } return tags, nil } func (ts *TagCommonService) SetSiteWriteTag(ctx context.Context, recommendTags, reservedTags []string, userID string) ( errFields []*validator.FormErrorField, err error) { recommendErr := ts.CheckTag(ctx, recommendTags, userID) reservedErr := ts.CheckTag(ctx, reservedTags, userID) if recommendErr != nil { errFields = append(errFields, &validator.FormErrorField{ ErrorField: "recommend_tags", ErrorMsg: recommendErr.Error(), }) err = recommendErr } if reservedErr != nil { errFields = append(errFields, &validator.FormErrorField{ ErrorField: "reserved_tags", ErrorMsg: reservedErr.Error(), }) err = reservedErr } if len(errFields) > 0 { return errFields, err } err = ts.SetTagsAttribute(ctx, recommendTags, "recommend") if err != nil { return nil, err } err = ts.SetTagsAttribute(ctx, reservedTags, "reserved") if err != nil { return nil, err } return nil, nil } // SetTagsAttribute func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, attribute string) (err error) { var oldTags []*entity.Tag switch attribute { case "recommend": oldTags, err = ts.tagCommonRepo.GetRecommendTagList(ctx) case "reserved": oldTags, err = ts.tagCommonRepo.GetReservedTagList(ctx) default: return } if err != nil { return err } oldTagSlugNameList := make([]string, 0) for _, tag := range oldTags { oldTagSlugNameList = append(oldTagSlugNameList, tag.SlugName) } err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, oldTagSlugNameList, attribute, false) if err != nil { return err } err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tags, attribute, true) if err != nil { return err } return nil } func (ts *TagCommonService) GetTagListByNames(ctx context.Context, tagNames []string) ([]*entity.Tag, error) { for k, tagname := range tagNames { tagNames[k] = strings.ToLower(tagname) } tagList, err := ts.tagCommonRepo.GetTagListByNames(ctx, tagNames) if err != nil { return nil, err } ts.TagsFormatRecommendAndReserved(ctx, tagList) return tagList, nil } func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.TagItem) (bool, error) { taginfo, err := ts.siteInfoService.GetSiteTag(ctx) if err != nil { return false, err } if !taginfo.RequiredTag || len(taginfo.RecommendTags) == 0 { return true, nil } tagNames := make([]string, 0) for _, item := range tags { item.SlugName = strings.ReplaceAll(item.SlugName, " ", "-") tagNames = append(tagNames, item.SlugName) } list, err := ts.GetTagListByNames(ctx, tagNames) if err != nil { return false, err } for _, item := range list { if item.Recommend { return true, nil } } return false, nil } func (ts *TagCommonService) GetMinimumTags(ctx context.Context) (int, error) { siteInfo, err := ts.siteInfoService.GetSiteQuestion(ctx) if err != nil { return 1, err } minimumTags := siteInfo.MinimumTags return minimumTags, nil } func (ts *TagCommonService) HasNewTag(ctx context.Context, tags []*schema.TagItem) (bool, error) { tagNames := make([]string, 0) tagMap := make(map[string]bool) for _, item := range tags { item.SlugName = strings.ReplaceAll(item.SlugName, " ", "-") tagNames = append(tagNames, item.SlugName) tagMap[item.SlugName] = false } list, err := ts.GetTagListByNames(ctx, tagNames) if err != nil { return true, err } for _, item := range list { _, ok := tagMap[item.SlugName] if ok { tagMap[item.SlugName] = true } } for _, has := range tagMap { if !has { return true, nil } } return false, nil } // GetObjectTag get object tag func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (objTags []*schema.TagResp, err error) { tagsInfoList, err := ts.GetObjectEntityTag(ctx, objectId) if err != nil { return nil, err } return ts.TagFormat(ctx, tagsInfoList) } // AddTag get object tag func (ts *TagCommonService) AddTag(ctx context.Context, req *schema.AddTagReq) (resp *schema.AddTagResp, err error) { _, exist, err := ts.GetTagBySlugName(ctx, req.SlugName) if err != nil { return nil, err } if exist { return nil, errors.BadRequest(reason.TagAlreadyExist) } slugName := strings.ReplaceAll(req.SlugName, " ", "-") slugName = strings.ToLower(slugName) tagInfo := &entity.Tag{ SlugName: slugName, DisplayName: req.DisplayName, OriginalText: req.OriginalText, ParsedText: req.ParsedText, Status: entity.TagStatusAvailable, UserID: req.UserID, } tagList := []*entity.Tag{tagInfo} err = ts.tagCommonRepo.AddTagList(ctx, tagList) if err != nil { return nil, err } revisionDTO := &schema.AddRevisionDTO{ UserID: req.UserID, ObjectID: tagInfo.ID, Title: tagInfo.SlugName, } tagInfoJson, _ := json.Marshal(tagInfo) revisionDTO.Content = string(tagInfoJson) revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return nil, err } ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: tagInfo.ID, OriginalObjectID: tagInfo.ID, ActivityTypeKey: constant.ActTagCreated, RevisionID: revisionID, }) return &schema.AddTagResp{SlugName: tagInfo.SlugName}, nil } // AddTagList get object tag func (ts *TagCommonService) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) { return ts.tagCommonRepo.AddTagList(ctx, tagList) } // GetTagByID get object tag func (ts *TagCommonService) GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error) { tag, exist, err = ts.tagCommonRepo.GetTagByID(ctx, tagID, false) if !exist { return } ts.tagFormatRecommendAndReserved(ctx, tag) return } // GetTagIDsByMainTagID get object tag func (ts *TagCommonService) GetTagIDsByMainTagID(ctx context.Context, tagID string) (tagIDs []string, err error) { tagIDs, err = ts.tagRepo.GetIDsByMainTagId(ctx, tagID) return } // GetTagBySlugName get object tag func (ts *TagCommonService) GetTagBySlugName(ctx context.Context, slugName string) (tag *entity.Tag, exist bool, err error) { tag, exist, err = ts.tagCommonRepo.GetTagBySlugName(ctx, slugName) if !exist { return } ts.tagFormatRecommendAndReserved(ctx, tag) return } // GetTagListByIDs get object tag func (ts *TagCommonService) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) { tagList, err = ts.tagCommonRepo.GetTagListByIDs(ctx, ids) if err != nil { return nil, err } ts.TagsFormatRecommendAndReserved(ctx, tagList) return } // GetTagPage get object tag func (ts *TagCommonService) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) ( tagList []*entity.Tag, total int64, err error) { tagList, total, err = ts.tagCommonRepo.GetTagPage(ctx, page, pageSize, tag, queryCond) if err != nil { return nil, 0, err } ts.TagsFormatRecommendAndReserved(ctx, tagList) return } func (ts *TagCommonService) GetObjectEntityTag(ctx context.Context, objectId string) (objTags []*entity.Tag, err error) { tagList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, objectId) if err != nil { return nil, err } tagIDList := make([]string, 0) for _, tag := range tagList { tagIDList = append(tagIDList, tag.TagID) } objTags, err = ts.GetTagListByIDs(ctx, tagIDList) if err != nil { return nil, err } return objTags, nil } func (ts *TagCommonService) TagFormat(ctx context.Context, tags []*entity.Tag) (objTags []*schema.TagResp, err error) { objTags = make([]*schema.TagResp, 0) for _, tagInfo := range tags { objTags = append(objTags, &schema.TagResp{ SlugName: tagInfo.SlugName, DisplayName: tagInfo.DisplayName, MainTagSlugName: tagInfo.MainTagSlugName, Recommend: tagInfo.Recommend, Reserved: tagInfo.Reserved, }) } return objTags, nil } func (ts *TagCommonService) TagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) { if len(tagList) == 0 { return } tagConfig, err := ts.siteInfoService.GetSiteTag(ctx) if err != nil { log.Error(err) return } if !tagConfig.RequiredTag { for _, tag := range tagList { tag.Recommend = false } } } func (ts *TagCommonService) tagFormatRecommendAndReserved(ctx context.Context, tag *entity.Tag) { if tag == nil { return } tagConfig, err := ts.siteInfoService.GetSiteTag(ctx) if err != nil { log.Error(err) return } if !tagConfig.RequiredTag { tag.Recommend = false } } // BatchGetObjectTag batch get object tag func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []string) (map[string][]*schema.TagResp, error) { objectIDTagMap := make(map[string][]*schema.TagResp) if len(objectIds) == 0 { return objectIDTagMap, nil } objectTagRelList, err := ts.tagRelRepo.BatchGetObjectTagRelList(ctx, objectIds) if err != nil { return objectIDTagMap, err } tagIDList := make([]string, 0) for _, tag := range objectTagRelList { tagIDList = append(tagIDList, tag.TagID) } tagsInfoList, err := ts.GetTagListByIDs(ctx, tagIDList) if err != nil { return objectIDTagMap, err } tagsInfoMapping := make(map[string]*entity.Tag) tagsRank := make(map[string]int) // Used for sorting for idx, item := range tagsInfoList { tagsInfoMapping[item.ID] = item tagsRank[item.ID] = idx } for _, item := range objectTagRelList { _, ok := tagsInfoMapping[item.TagID] if ok { tagInfo := tagsInfoMapping[item.TagID] t := &schema.TagResp{ ID: tagInfo.ID, SlugName: tagInfo.SlugName, DisplayName: tagInfo.DisplayName, MainTagSlugName: tagInfo.MainTagSlugName, Recommend: tagInfo.Recommend, Reserved: tagInfo.Reserved, } objectIDTagMap[item.ObjectID] = append(objectIDTagMap[item.ObjectID], t) } } // The sorting in tagsRank is correct, object tags should be sorted by tagsRank for _, objectTags := range objectIDTagMap { sort.SliceStable(objectTags, func(i, j int) bool { return tagsRank[objectTags[i].ID] < tagsRank[objectTags[j].ID] }) } return objectIDTagMap, nil } func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID string) (err error) { if len(tags) == 0 { return nil } // find tags name tagListInDb, err := ts.GetTagListByNames(ctx, tags) if err != nil { return err } tagInDbMapping := make(map[string]*entity.Tag) checktags := make([]string, 0) for _, tag := range tagListInDb { if tag.MainTagID != 0 { checktags = append(checktags, fmt.Sprintf("\"%s\"", tag.SlugName)) } tagInDbMapping[tag.SlugName] = tag } if len(checktags) > 0 { err = errors.BadRequest(reason.TagNotContainSynonym).WithMsg(fmt.Sprintf("Should not contain synonym tags %s", strings.Join(checktags, ","))) return err } addTagList := make([]*entity.Tag, 0) addTagMsgList := make([]string, 0) for _, tag := range tags { _, ok := tagInDbMapping[tag] if ok { continue } item := &entity.Tag{} item.SlugName = tag item.DisplayName = tag item.OriginalText = "" item.ParsedText = "" item.Status = entity.TagStatusAvailable item.UserID = userID addTagList = append(addTagList, item) addTagMsgList = append(addTagMsgList, tag) } if len(addTagList) > 0 { err = errors.BadRequest(reason.TagNotFound).WithMsg(fmt.Sprintf("tag [%s] does not exist", strings.Join(addTagMsgList, ","))) return err } return nil } // CheckTagsIsChange func (ts *TagCommonService) CheckTagsIsChange(ctx context.Context, tagNameList, oldtagNameList []string) bool { check := make(map[string]bool) if len(tagNameList) != len(oldtagNameList) { return true } for _, item := range tagNameList { check[item] = false } for _, item := range oldtagNameList { _, ok := check[item] if !ok { return true } check[item] = true } for _, value := range check { if !value { return true } } return false } func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) { reservedTagsMap := make(map[string]bool) needTagsMap := make([]string, 0) notNeedTagsMap := make([]string, 0) for _, tag := range objectTagData { if tag.Reserved { reservedTagsMap[tag.SlugName] = true } } for _, tag := range oldobjectTagData { if tag.Reserved { _, ok := reservedTagsMap[tag.SlugName] if !ok { needTagsMap = append(needTagsMap, tag.SlugName) } else { reservedTagsMap[tag.SlugName] = false } } } for k, v := range reservedTagsMap { if v { notNeedTagsMap = append(notNeedTagsMap, k) } } if len(needTagsMap) > 0 { return false, true, needTagsMap, []string{} } if len(notNeedTagsMap) > 0 { return true, false, []string{}, notNeedTagsMap } return true, true, []string{}, []string{} } // ObjectChangeTag change object tag list func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *schema.TagChange, minimumTags int) (errorlist []*validator.FormErrorField, err error) { // checks if the tags sent in the put req are less than the minimum, if so, tag changes are not applied if len(objectTagData.Tags) < minimumTags { errorlist := make([]*validator.FormErrorField, 0) errorlist = append(errorlist, &validator.FormErrorField{ ErrorField: "tags", ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount), }) err = errors.BadRequest(reason.TagMinCount) return errorlist, err } thisObjTagNameList := make([]string, 0) thisObjTagIDList := make([]string, 0) for _, t := range objectTagData.Tags { t.SlugName = strings.ToLower(t.SlugName) thisObjTagNameList = append(thisObjTagNameList, t.SlugName) } // find tags name tagListInDb, err := ts.tagCommonRepo.GetTagListByNames(ctx, thisObjTagNameList) if err != nil { return nil, err } tagInDbMapping := make(map[string]*entity.Tag) for _, tag := range tagListInDb { tagInDbMapping[strings.ToLower(tag.SlugName)] = tag thisObjTagIDList = append(thisObjTagIDList, tag.ID) } addTagList := make([]*entity.Tag, 0) for _, tag := range objectTagData.Tags { _, ok := tagInDbMapping[strings.ToLower(tag.SlugName)] if ok { continue } item := &entity.Tag{} item.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-") item.DisplayName = tag.DisplayName item.OriginalText = tag.OriginalText item.ParsedText = tag.ParsedText item.Status = entity.TagStatusAvailable item.UserID = objectTagData.UserID addTagList = append(addTagList, item) } if len(addTagList) > 0 { err = ts.tagCommonRepo.AddTagList(ctx, addTagList) if err != nil { return nil, err } for _, tag := range addTagList { thisObjTagIDList = append(thisObjTagIDList, tag.ID) revisionDTO := &schema.AddRevisionDTO{ UserID: objectTagData.UserID, ObjectID: tag.ID, Title: tag.SlugName, } tagInfoJson, _ := json.Marshal(tag) revisionDTO.Content = string(tagInfoJson) revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return nil, err } ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: objectTagData.UserID, ObjectID: tag.ID, OriginalObjectID: tag.ID, ActivityTypeKey: constant.ActTagCreated, RevisionID: revisionID, }) } } err = ts.CreateOrUpdateTagRelList(ctx, objectTagData.ObjectID, thisObjTagIDList) if err != nil { return nil, err } return nil, nil } func (ts *TagCommonService) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) { return ts.tagRelRepo.CountTagRelByTagID(ctx, tagID) } // RefreshTagQuestionCount refresh tag question count func (ts *TagCommonService) RefreshTagQuestionCount(ctx context.Context, tagIDs []string) (err error) { for _, tagID := range tagIDs { count, err := ts.tagRelRepo.CountTagRelByTagID(ctx, tagID) if err != nil { return err } err = ts.tagCommonRepo.UpdateTagQuestionCount(ctx, tagID, int(count)) if err != nil { return err } log.Debugf("tag count updated %s %d", tagID, count) } return nil } func (ts *TagCommonService) RefreshTagCountByQuestionID(ctx context.Context, questionID string) (err error) { tagListList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, questionID) if err != nil { return err } tagIDs := make([]string, 0) for _, item := range tagListList { tagIDs = append(tagIDs, item.TagID) } err = ts.RefreshTagQuestionCount(ctx, tagIDs) if err != nil { return err } return nil } // RemoveTagRelListByObjectID remove tag relation by object id func (ts *TagCommonService) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) { return ts.tagRelRepo.RemoveTagRelListByObjectID(ctx, objectID) } // RecoverTagRelListByObjectID recover tag relation by object id func (ts *TagCommonService) RecoverTagRelListByObjectID(ctx context.Context, objectID string) (err error) { return ts.tagRelRepo.RecoverTagRelListByObjectID(ctx, objectID) } func (ts *TagCommonService) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) { return ts.tagRelRepo.HideTagRelListByObjectID(ctx, objectID) } func (ts *TagCommonService) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) { return ts.tagRelRepo.ShowTagRelListByObjectID(ctx, objectID) } // CreateOrUpdateTagRelList if tag relation is exists update status, if not create it func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, objectId string, tagIDs []string) (err error) { addTagIDMapping := make(map[string]struct{}) for _, t := range tagIDs { addTagIDMapping[t] = struct{}{} } // get all old relation oldTagRelList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, objectId) if err != nil { return err } var deleteTagRel []int64 needRefreshTagIDs := make([]string, 0, len(oldTagRelList)+len(tagIDs)) needRefreshTagIDs = append(needRefreshTagIDs, tagIDs...) for _, rel := range oldTagRelList { if _, ok := addTagIDMapping[rel.TagID]; !ok { deleteTagRel = append(deleteTagRel, rel.ID) needRefreshTagIDs = append(needRefreshTagIDs, rel.TagID) } } addTagRelList := make([]*entity.TagRel, 0) enableTagRelList := make([]int64, 0) defaultTagRelStatus, err := ts.tagRelRepo.GetTagRelDefaultStatusByObjectID(ctx, objectId) if err != nil { return err } for _, tagID := range tagIDs { rel, exist, err := ts.tagRelRepo.GetObjectTagRelWithoutStatus(ctx, objectId, tagID) if err != nil { return err } // if not exist add tag relation if !exist { addTagRelList = append(addTagRelList, &entity.TagRel{ TagID: tagID, ObjectID: objectId, Status: defaultTagRelStatus, }) } // if exist and has been removed, that should be enabled if exist && rel.Status != entity.TagRelStatusAvailable && rel.Status != entity.TagRelStatusHide { enableTagRelList = append(enableTagRelList, rel.ID) } } if len(deleteTagRel) > 0 { if err = ts.tagRelRepo.RemoveTagRelListByIDs(ctx, deleteTagRel); err != nil { return err } } if len(addTagRelList) > 0 { if err = ts.tagRelRepo.AddTagRelList(ctx, addTagRelList); err != nil { return err } } if len(enableTagRelList) > 0 { if err = ts.tagRelRepo.EnableTagRelByIDs(ctx, enableTagRelList, defaultTagRelStatus == entity.TagRelStatusHide); err != nil { return err } } err = ts.RefreshTagQuestionCount(ctx, needRefreshTagIDs) if err != nil { log.Error(err) } return nil } func (ts *TagCommonService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (err error) { var canUpdate bool _, existUnreviewed, err := ts.revisionService.ExistUnreviewedByObjectID(ctx, req.TagID) if err != nil { return err } if existUnreviewed { err = errors.BadRequest(reason.AnswerCannotUpdate) return err } tagInfo, exist, err := ts.GetTagByID(ctx, req.TagID) if err != nil { return err } if !exist { return errors.BadRequest(reason.TagNotFound) } // Adding equivalent slug formatting for tag update slugName := strings.ReplaceAll(req.SlugName, " ", "-") slugName = strings.ToLower(slugName) // If the content is the same, ignore it if tagInfo.OriginalText == req.OriginalText && tagInfo.DisplayName == req.DisplayName && tagInfo.SlugName == slugName { return nil } tagInfo.SlugName = slugName tagInfo.DisplayName = req.DisplayName tagInfo.OriginalText = req.OriginalText tagInfo.ParsedText = req.ParsedText revisionDTO := &schema.AddRevisionDTO{ UserID: req.UserID, ObjectID: tagInfo.ID, Title: tagInfo.SlugName, Log: req.EditSummary, } if req.NoNeedReview { canUpdate = true err = ts.tagRepo.UpdateTag(ctx, tagInfo) if err != nil { return err } if tagInfo.MainTagID == 0 && len(req.SlugName) > 0 { log.Debugf("tag %s update slug_name", tagInfo.SlugName) tagList, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)}) if err != nil { return err } updateTagSlugNames := make([]string, 0) for _, tag := range tagList { updateTagSlugNames = append(updateTagSlugNames, tag.SlugName) } err = ts.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName) if err != nil { return err } } revisionDTO.Status = entity.RevisionReviewPassStatus } else { revisionDTO.Status = entity.RevisionUnreviewedStatus } tagInfoJson, _ := json.Marshal(tagInfo) revisionDTO.Content = string(tagInfoJson) revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return err } if canUpdate { ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: tagInfo.ID, OriginalObjectID: tagInfo.ID, ActivityTypeKey: constant.ActTagEdited, RevisionID: revisionID, }) } return } // MigrateTagQuestions migrate tag question func (ts *TagCommonService) MigrateTagQuestions(ctx context.Context, sourceTagID, targetTagID string) (err error) { return ts.tagRelRepo.MigrateTagObjects(ctx, sourceTagID, targetTagID) } ================================================ FILE: internal/service/unique/uniqid_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package unique import ( "context" ) // UniqueIDRepo unique id repository type UniqueIDRepo interface { GenUniqueIDStr(ctx context.Context, key string) (uniqueID string, err error) } ================================================ FILE: internal/service/uploader/upload.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package uploader import ( "bytes" "fmt" "io" "mime/multipart" "net/http" "net/url" "os" "path" "path/filepath" "strings" "github.com/apache/answer/internal/service/file_record" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/service/service_config" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/dir" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/plugin" "github.com/disintegration/imaging" "github.com/gin-gonic/gin" exifremove "github.com/scottleedavis/go-exif-remove" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) var ( subPathList = []string{ constant.AvatarSubPath, constant.AvatarThumbSubPath, constant.PostSubPath, constant.BrandingSubPath, constant.FilesPostSubPath, constant.DeletedSubPath, } supportedThumbFileExtMapping = map[string]imaging.Format{ ".jpg": imaging.JPEG, ".jpeg": imaging.JPEG, ".png": imaging.PNG, ".gif": imaging.GIF, } ) type UploaderService interface { UploadAvatarFile(ctx *gin.Context, userID string) (url string, err error) UploadPostFile(ctx *gin.Context, userID string) (url string, err error) UploadPostAttachment(ctx *gin.Context, userID string) (url string, err error) UploadBrandingFile(ctx *gin.Context, userID string) (url string, err error) AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error) } // uploaderService uploader service type uploaderService struct { serviceConfig *service_config.ServiceConfig siteInfoService siteinfo_common.SiteInfoCommonService fileRecordService *file_record.FileRecordService } // NewUploaderService new upload service func NewUploaderService( serviceConfig *service_config.ServiceConfig, siteInfoService siteinfo_common.SiteInfoCommonService, fileRecordService *file_record.FileRecordService, ) UploaderService { for _, subPath := range subPathList { err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath)) if err != nil { panic(err) } } return &uploaderService{ serviceConfig: serviceConfig, siteInfoService: siteInfoService, fileRecordService: fileRecordService, } } // UploadAvatarFile upload avatar file func (us *uploaderService) UploadAvatarFile(ctx *gin.Context, userID string) (url string, err error) { url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar) if err != nil { return "", err } if len(url) > 0 { return url, nil } siteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx) if err != nil { return "", err } ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteAdvanced.GetMaxImageSize()) file, fileHeader, err := ctx.Request.FormFile("file") if err != nil { return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } defer func() { _ = file.Close() }() fileExt := strings.ToLower(path.Ext(fileHeader.Filename)) if _, ok := plugin.DefaultFileTypeCheckMapping[plugin.UserAvatar][fileExt]; !ok { return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) avatarFilePath := path.Join(constant.AvatarSubPath, newFilename) url, err = us.uploadImageFile(ctx, fileHeader, avatarFilePath) if err != nil { return "", err } us.fileRecordService.AddFileRecord(ctx, userID, avatarFilePath, url, string(plugin.UserAvatar)) return url, nil } func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error) { fileSuffix := path.Ext(fileName) if _, ok := supportedThumbFileExtMapping[fileSuffix]; !ok { // if file type is not supported, return original file return path.Join(us.serviceConfig.UploadPath, constant.AvatarSubPath, fileName), nil } if size > 1024 { size = 1024 } thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName) thumbFilePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, constant.AvatarThumbSubPath, thumbFileName) _, err = os.ReadFile(thumbFilePath) if err == nil { return thumbFilePath, nil } filePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, constant.AvatarSubPath, fileName) avatarFile, err := os.ReadFile(filePath) if err != nil { return "", errors.NotFound(reason.UnknownError).WithError(err) } reader := bytes.NewReader(avatarFile) img, err := imaging.Decode(reader) if err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } var buf bytes.Buffer newImage := imaging.Fill(img, size, size, imaging.Center, imaging.Linear) if err = imaging.Encode(&buf, newImage, supportedThumbFileExtMapping[fileSuffix]); err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } if err = dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, constant.AvatarThumbSubPath)); err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } avatarFilePath := path.Join(constant.AvatarThumbSubPath, thumbFileName) saveFilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) out, err := os.Create(saveFilePath) if err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } defer func() { _ = out.Close() }() thumbReader := bytes.NewReader(buf.Bytes()) if _, err = io.Copy(out, thumbReader); err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } return saveFilePath, nil } func (us *uploaderService) UploadPostFile(ctx *gin.Context, userID string) ( url string, err error) { url, err = us.tryToUploadByPlugin(ctx, plugin.UserPost) if err != nil { return "", err } if len(url) > 0 { return url, nil } siteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx) if err != nil { return "", err } ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteAdvanced.GetMaxImageSize()) file, fileHeader, err := ctx.Request.FormFile("file") if err != nil { return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } defer func() { _ = file.Close() }() if checker.IsUnAuthorizedExtension(fileHeader.Filename, siteAdvanced.AuthorizedImageExtensions) { return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } fileExt := strings.ToLower(path.Ext(fileHeader.Filename)) newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) avatarFilePath := path.Join(constant.PostSubPath, newFilename) url, err = us.uploadImageFile(ctx, fileHeader, avatarFilePath) if err != nil { return "", err } us.fileRecordService.AddFileRecord(ctx, userID, avatarFilePath, url, string(plugin.UserPost)) return url, nil } func (us *uploaderService) UploadPostAttachment(ctx *gin.Context, userID string) ( url string, err error) { url, err = us.tryToUploadByPlugin(ctx, plugin.UserPostAttachment) if err != nil { return "", err } if len(url) > 0 { return url, nil } resp, err := us.siteInfoService.GetSiteAdvanced(ctx) if err != nil { return "", err } ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, resp.GetMaxAttachmentSize()) file, fileHeader, err := ctx.Request.FormFile("file") if err != nil { return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } defer func() { _ = file.Close() }() if checker.IsUnAuthorizedExtension(fileHeader.Filename, resp.AuthorizedAttachmentExtensions) { return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } fileExt := strings.ToLower(path.Ext(fileHeader.Filename)) newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) attachmentFilePath := path.Join(constant.FilesPostSubPath, newFilename) url, err = us.uploadAttachmentFile(ctx, fileHeader, fileHeader.Filename, attachmentFilePath) if err != nil { return "", err } us.fileRecordService.AddFileRecord(ctx, userID, attachmentFilePath, url, string(plugin.UserPostAttachment)) return url, nil } func (us *uploaderService) UploadBrandingFile(ctx *gin.Context, userID string) ( url string, err error) { url, err = us.tryToUploadByPlugin(ctx, plugin.AdminBranding) if err != nil { return "", err } if len(url) > 0 { return url, nil } siteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx) if err != nil { return "", err } ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteAdvanced.GetMaxImageSize()) file, fileHeader, err := ctx.Request.FormFile("file") if err != nil { return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } defer func() { _ = file.Close() }() fileExt := strings.ToLower(path.Ext(fileHeader.Filename)) if _, ok := plugin.DefaultFileTypeCheckMapping[plugin.AdminBranding][fileExt]; !ok { return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) avatarFilePath := path.Join(constant.BrandingSubPath, newFilename) url, err = us.uploadImageFile(ctx, fileHeader, avatarFilePath) if err != nil { return "", err } us.fileRecordService.AddFileRecord(ctx, userID, avatarFilePath, url, string(plugin.AdminBranding)) return url, nil } func (us *uploaderService) uploadImageFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) ( url string, err error) { siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx) if err != nil { return "", err } siteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx) if err != nil { return "", err } filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath) if err := ctx.SaveUploadedFile(file, filePath); err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } src, err := file.Open() if err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } defer func() { _ = src.Close() }() if !checker.DecodeAndCheckImageFile(filePath, siteAdvanced.GetMaxImageMegapixel()) { return "", errors.BadRequest(reason.UploadFileUnsupportedFileFormat) } if err := removeExif(filePath); err != nil { log.Error(err) } url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath) return url, nil } func (us *uploaderService) uploadAttachmentFile(ctx *gin.Context, file *multipart.FileHeader, originalFilename, fileSubPath string) ( downloadUrl string, err error) { siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx) if err != nil { return "", err } filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath) if err := ctx.SaveUploadedFile(file, filePath); err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } // Need url encode the original filename. Because the filename may contain special characters that conflict with the markdown syntax. originalFilename = url.QueryEscape(originalFilename) // The original filename is 123.pdf // The local saved path is /UploadPath/hash.pdf // When downloading, the download link will be redirect to the local saved path. And the download filename will be 123.png. downloadPath := strings.TrimSuffix(fileSubPath, filepath.Ext(fileSubPath)) + "/" + originalFilename downloadUrl = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, downloadPath) return downloadUrl, nil } func (us *uploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) ( url string, err error) { siteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx) if err != nil { return "", err } cond := plugin.UploadFileCondition{ Source: source, MaxImageSize: siteAdvanced.MaxImageSize, MaxAttachmentSize: siteAdvanced.MaxAttachmentSize, MaxImageMegapixel: siteAdvanced.MaxImageMegapixel, AuthorizedImageExtensions: siteAdvanced.AuthorizedImageExtensions, AuthorizedAttachmentExtensions: siteAdvanced.AuthorizedAttachmentExtensions, } _ = plugin.CallStorage(func(fn plugin.Storage) error { resp := fn.UploadFile(ctx, cond) if resp.OriginalError != nil { log.Errorf("upload file by plugin failed, err: %v", resp.OriginalError) err = errors.BadRequest("").WithMsg(resp.DisplayErrorMsg.Translate(ctx)).WithError(err) } else { url = resp.FullURL } return nil }) return url, err } // removeExif remove exif // only support jpg/jpeg/png func removeExif(path string) error { ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(path), ".")) if ext != "jpeg" && ext != "jpg" && ext != "png" { return nil } img, err := os.ReadFile(path) if err != nil { return err } noExifBytes, err := exifremove.Remove(img) if err != nil { return err } return os.WriteFile(path, noExifBytes, 0644) } ================================================ FILE: internal/service/user_admin/user_backyard.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package user_admin import ( "context" "fmt" "net/mail" "strings" "time" "unicode" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/badge" "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/export" notificationcommon "github.com/apache/answer/internal/service/notification_common" "github.com/apache/answer/internal/service/plugin_common" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/pkg/token" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/internal/service/role" "github.com/apache/answer/internal/service/siteinfo_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/internal/service/user_external_login" "github.com/apache/answer/pkg/checker" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "golang.org/x/crypto/bcrypt" ) // UserAdminRepo user repository type UserAdminRepo interface { UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, email string, suspendedUntil time.Time) (err error) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User, usernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error) AddUser(ctx context.Context, user *entity.User) (err error) AddUsers(ctx context.Context, users []*entity.User) (err error) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) DeletePermanentlyUsers(ctx context.Context) (err error) GetExpiredSuspendedUsers(ctx context.Context) (users []*entity.User, err error) } // UserAdminService user service type UserAdminService struct { userRepo UserAdminRepo userRoleRelService *role.UserRoleRelService authService *auth.AuthService userCommonService *usercommon.UserCommon userActivity activity.UserActiveActivityRepo siteInfoCommonService siteinfo_common.SiteInfoCommonService emailService *export.EmailService questionCommonRepo questioncommon.QuestionRepo answerCommonRepo answercommon.AnswerRepo commentCommonRepo comment_common.CommentCommonRepo userExternalLoginRepo user_external_login.UserExternalLoginRepo notificationRepo notificationcommon.NotificationRepo pluginUserConfigRepo plugin_common.PluginUserConfigRepo badgeAwardRepo badge.BadgeAwardRepo } // NewUserAdminService new user admin service func NewUserAdminService( userRepo UserAdminRepo, userRoleRelService *role.UserRoleRelService, authService *auth.AuthService, userCommonService *usercommon.UserCommon, userActivity activity.UserActiveActivityRepo, siteInfoCommonService siteinfo_common.SiteInfoCommonService, emailService *export.EmailService, questionCommonRepo questioncommon.QuestionRepo, answerCommonRepo answercommon.AnswerRepo, commentCommonRepo comment_common.CommentCommonRepo, userExternalLoginRepo user_external_login.UserExternalLoginRepo, notificationRepo notificationcommon.NotificationRepo, pluginUserConfigRepo plugin_common.PluginUserConfigRepo, badgeAwardRepo badge.BadgeAwardRepo, ) *UserAdminService { return &UserAdminService{ userRepo: userRepo, userRoleRelService: userRoleRelService, authService: authService, userCommonService: userCommonService, userActivity: userActivity, siteInfoCommonService: siteInfoCommonService, emailService: emailService, questionCommonRepo: questionCommonRepo, answerCommonRepo: answerCommonRepo, commentCommonRepo: commentCommonRepo, userExternalLoginRepo: userExternalLoginRepo, notificationRepo: notificationRepo, pluginUserConfigRepo: pluginUserConfigRepo, badgeAwardRepo: badgeAwardRepo, } } // UpdateUserStatus update user func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.UpdateUserStatusReq) (err error) { // Admin cannot modify their status if req.UserID == req.LoginUserID { return errors.BadRequest(reason.AdminCannotModifySelfStatus) } userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return } if !exist { return errors.BadRequest(reason.UserNotFound) } // if user status is deleted if userInfo.Status == entity.UserStatusDeleted { return nil } if req.IsInactive() { userInfo.MailStatus = entity.EmailStatusToBeVerified } if req.IsDeleted() { userInfo.Status = entity.UserStatusDeleted userInfo.EMail = fmt.Sprintf("%s.%d", userInfo.EMail, time.Now().Unix()) } if req.IsSuspended() { userInfo.Status = entity.UserStatusSuspended } if req.IsNormal() { userInfo.Status = entity.UserStatusAvailable userInfo.MailStatus = entity.EmailStatusAvailable } suspendedUntil := req.GetSuspendedUntil() err = us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail, suspendedUntil) if err != nil { return err } // remove all content that user created, such as question, answer, comment, etc. if req.RemoveAllContent { us.removeAllUserCreatedContent(ctx, userInfo.ID) } if req.IsDeleted() { us.removeAllUserConfiguration(ctx, userInfo.ID) } // if user reputation is zero means this user is inactive, so try to activate this user. if req.IsNormal() && userInfo.Rank == 0 { return us.userActivity.UserActive(ctx, userInfo.ID) } return nil } // removeAllUserConfiguration remove all user configuration func (us *UserAdminService) removeAllUserConfiguration(ctx context.Context, userID string) { err := us.userExternalLoginRepo.DeleteUserExternalLoginByUserID(ctx, userID) if err != nil { log.Errorf("remove all user external login error: %v", err) } err = us.notificationRepo.DeleteNotification(ctx, userID) if err != nil { log.Errorf("remove all user notification error: %v", err) } err = us.notificationRepo.DeleteUserNotificationConfig(ctx, userID) if err != nil { log.Errorf("remove all user notification config error: %v", err) } err = us.pluginUserConfigRepo.DeleteUserPluginConfig(ctx, userID) if err != nil { log.Errorf("remove all user plugin config error: %v", err) } err = us.badgeAwardRepo.DeleteUserBadgeAward(ctx, userID) if err != nil { log.Errorf("remove all user badge award error: %v", err) } } // removeAllUserCreatedContent remove all user created content func (us *UserAdminService) removeAllUserCreatedContent(ctx context.Context, userID string) { if err := us.questionCommonRepo.RemoveAllUserQuestion(ctx, userID); err != nil { log.Errorf("remove all user question error: %v", err) } if err := us.answerCommonRepo.RemoveAllUserAnswer(ctx, userID); err != nil { log.Errorf("remove all user answer error: %v", err) } if err := us.commentCommonRepo.RemoveAllUserComment(ctx, userID); err != nil { log.Errorf("remove all user comment error: %v", err) } } // UpdateUserRole update user role func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.UpdateUserRoleReq) (err error) { // Users cannot modify their roles if req.UserID == req.LoginUserID { return errors.BadRequest(reason.UserCannotUpdateYourRole) } err = us.userRoleRelService.SaveUserRole(ctx, req.UserID, req.RoleID) if err != nil { return err } us.authService.RemoveUserAllTokens(ctx, req.UserID) return } // AddUser add user func (us *UserAdminService) AddUser(ctx context.Context, req *schema.AddUserReq) (err error) { _, has, err := us.userRepo.GetUserInfoByEmail(ctx, req.Email) if err != nil { return err } if has { return errors.BadRequest(reason.EmailDuplicate) } hashPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return err } userInfo := &entity.User{} userInfo.EMail = req.Email userInfo.DisplayName = req.DisplayName userInfo.Pass = string(hashPwd) userInfo.Username, err = us.userCommonService.MakeUsername(ctx, userInfo.DisplayName) if err != nil { return err } userInfo.MailStatus = entity.EmailStatusAvailable userInfo.Status = entity.UserStatusAvailable userInfo.Rank = 1 err = us.userRepo.AddUser(ctx, userInfo) if err != nil { return err } return } // AddUsers add users func (us *UserAdminService) AddUsers(ctx context.Context, req *schema.AddUsersReq) ( resp []*validator.FormErrorField, err error) { resp, err = req.ParseUsers(ctx) if err != nil { return resp, err } errData := us.checkUserDuplicateInner(ctx, req.Users) if errData != nil { return errData.GetErrField(ctx), errors.BadRequest(reason.RequestFormatError) } users, errData, err := us.formatBulkAddUsers(ctx, req) if err != nil { return resp, err } if errData != nil { return errData.GetErrField(ctx), errors.BadRequest(reason.RequestFormatError) } return nil, us.userRepo.AddUsers(ctx, users) } func (us *UserAdminService) checkUserDuplicateInner(ctx context.Context, users []*schema.AddUserReq) ( errorData *schema.AddUsersErrorData) { lang := handler.GetLangByCtx(ctx) val := validator.GetValidatorByLang(lang) emails := make(map[string]bool) displayNames := make(map[string]bool) for line, user := range users { if errFields, e := val.Check(user); e != nil { errorData = &schema.AddUsersErrorData{} if len(errFields) > 0 { errorData.Field = errFields[0].ErrorField errorData.ExtraMessage = errFields[0].ErrorMsg } errorData.Line = line + 1 errorData.Content = fmt.Sprintf("%s, %s, %s", user.DisplayName, user.Email, user.Password) return errorData } if emails[user.Email] { return &schema.AddUsersErrorData{ Field: "email", Line: line + 1, Content: user.Email, ExtraMessage: translator.Tr(lang, reason.EmailDuplicate), } } if displayNames[user.DisplayName] { return &schema.AddUsersErrorData{ Field: "name", Line: line + 1, Content: user.DisplayName, ExtraMessage: translator.Tr(lang, reason.UsernameDuplicate), } } emails[user.Email] = true displayNames[user.DisplayName] = true } return nil } func (us *UserAdminService) formatBulkAddUsers(ctx context.Context, req *schema.AddUsersReq) ( users []*entity.User, errorData *schema.AddUsersErrorData, err error) { lang := handler.GetLangByCtx(ctx) errorData = &schema.AddUsersErrorData{Line: -1} for line, user := range req.Users { _, has, e := us.userRepo.GetUserInfoByEmail(ctx, user.Email) if e != nil { return nil, nil, e } if has { errorData.Field = "email" errorData.Line = line + 1 errorData.Content = user.Email errorData.ExtraMessage = translator.Tr(lang, reason.EmailDuplicate) return nil, errorData, nil } userInfo := &entity.User{} userInfo.EMail = user.Email userInfo.DisplayName = user.DisplayName hashPwd, _ := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) userInfo.Pass = string(hashPwd) userInfo.Username, err = us.userCommonService.MakeUsername(ctx, userInfo.DisplayName) if err != nil { errorData.Field = "name" errorData.Line = line + 1 errorData.Content = user.DisplayName errorData.ExtraMessage = translator.Tr(lang, reason.UsernameInvalid) return nil, errorData, nil } userInfo.MailStatus = entity.EmailStatusAvailable userInfo.Status = entity.UserStatusAvailable userInfo.Rank = 1 users = append(users, userInfo) } return users, nil, nil } // UpdateUserPassword update user password func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.UpdateUserPasswordReq) (err error) { // Users cannot modify their password if req.UserID == req.LoginUserID { return errors.BadRequest(reason.AdminCannotUpdateTheirPassword) } userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return err } if !exist { return errors.BadRequest(reason.UserNotFound) } hashPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return err } err = us.userRepo.UpdateUserPassword(ctx, userInfo.ID, string(hashPwd)) if err != nil { return err } // logout this user us.authService.RemoveUserAllTokens(ctx, req.UserID) return } // EditUserProfile edit user profile func (us *UserAdminService) EditUserProfile(ctx context.Context, req *schema.EditUserProfileReq) ( errFields []*validator.FormErrorField, err error) { if req.UserID == req.LoginUserID { return nil, errors.BadRequest(reason.AdminCannotEditTheirProfile) } _, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } if checker.IsInvalidUsername(req.Username) || checker.IsUsersIgnorePath(req.Username) { return append(errFields, &validator.FormErrorField{ ErrorField: "username", ErrorMsg: reason.UsernameInvalid, }), errors.BadRequest(reason.UsernameInvalid) } userInfo, exist, err := us.userCommonService.GetByUsername(ctx, req.Username) if err != nil { return nil, err } if exist && userInfo.ID != req.UserID { return append(errFields, &validator.FormErrorField{ ErrorField: "username", ErrorMsg: reason.UsernameDuplicate, }), errors.BadRequest(reason.UsernameDuplicate) } userInfo, exist, err = us.userCommonService.GetByEmail(ctx, req.Email) if err != nil { return nil, err } if exist && userInfo.ID != req.UserID { return append(errFields, &validator.FormErrorField{ ErrorField: "email", ErrorMsg: reason.EmailDuplicate, }), errors.BadRequest(reason.EmailDuplicate) } user := &entity.User{} user.ID = req.UserID user.DisplayName = req.DisplayName user.Username = req.Username user.EMail = req.Email user.MailStatus = entity.EmailStatusAvailable err = us.userCommonService.UpdateUserProfile(ctx, user) if err != nil { return nil, err } return } // GetUserInfo get user one func (us *UserAdminService) GetUserInfo(ctx context.Context, userID string) (resp *schema.GetUserInfoResp, err error) { user, exist, err := us.userRepo.GetUserInfo(ctx, userID) if err != nil { return } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } resp = &schema.GetUserInfoResp{} _ = copier.Copy(resp, user) return resp, nil } // GetUserPage get user list page func (us *UserAdminService) GetUserPage(ctx context.Context, req *schema.GetUserPageReq) (pageModel *pager.PageModel, err error) { user := &entity.User{} _ = copier.Copy(user, req) switch { case req.IsInactive(): user.MailStatus = entity.EmailStatusToBeVerified user.Status = entity.UserStatusAvailable case req.IsSuspended(): user.Status = entity.UserStatusSuspended case req.IsDeleted(): user.Status = entity.UserStatusDeleted default: user.MailStatus = entity.EmailStatusAvailable user.Status = entity.UserStatusAvailable } if len(req.Query) > 0 { if email, e := mail.ParseAddress(req.Query); e == nil { user.EMail = email.Address req.Query = "" } else if after, ok := strings.CutPrefix(req.Query, "user:"); ok { id := strings.TrimSpace(after) idSearch := true for _, r := range id { if !unicode.IsDigit(r) { idSearch = false break } } if idSearch { user.ID = id req.Query = "" } else { req.Query = id } } } users, total, err := us.userRepo.GetUserPage(ctx, req.Page, req.PageSize, user, req.Query, req.Staff) if err != nil { return } avatarMapping := us.siteInfoCommonService.FormatListAvatar(ctx, users) resp := make([]*schema.GetUserPageResp, 0) for _, u := range users { t := &schema.GetUserPageResp{ UserID: u.ID, CreatedAt: u.CreatedAt.Unix(), Username: u.Username, EMail: u.EMail, Rank: u.Rank, DisplayName: u.DisplayName, Avatar: avatarMapping[u.ID].GetURL(), } switch { case u.Status == entity.UserStatusDeleted: t.Status = constant.UserDeleted t.DeletedAt = u.DeletedAt.Unix() case u.Status == entity.UserStatusSuspended: t.Status = constant.UserSuspended t.SuspendedAt = u.SuspendedAt.Unix() if !u.SuspendedUntil.IsZero() { t.SuspendedUntil = u.SuspendedUntil.Unix() } case u.MailStatus == entity.EmailStatusToBeVerified: t.Status = constant.UserInactive default: t.Status = constant.UserNormal } resp = append(resp, t) } us.setUserRoleInfo(ctx, resp) return pager.NewPageModel(total, resp), nil } func (us *UserAdminService) setUserRoleInfo(ctx context.Context, resp []*schema.GetUserPageResp) { var userIDs []string for _, u := range resp { userIDs = append(userIDs, u.UserID) } userRoleMapping, err := us.userRoleRelService.GetUserRoleMapping(ctx, userIDs) if err != nil { log.Error(err) return } for _, u := range resp { r := userRoleMapping[u.UserID] if r == nil { continue } u.RoleID = r.ID u.RoleName = r.Name } } func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.GetUserActivationReq) ( resp *schema.GetUserActivationResp, err error) { userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } general, err := us.siteInfoCommonService.GetSiteGeneral(ctx) if err != nil { return nil, err } data := &schema.EmailCodeContent{ Email: userInfo.EMail, UserID: userInfo.ID, } code := token.GenerateToken() us.emailService.SaveCode(ctx, userInfo.ID, code, data.ToJSONString()) resp = &schema.GetUserActivationResp{ ActivationURL: fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code), } return resp, nil } // SendUserActivation send user activation email func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema.SendUserActivationReq) (err error) { userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return err } if !exist { return errors.BadRequest(reason.UserNotFound) } general, err := us.siteInfoCommonService.GetSiteGeneral(ctx) if err != nil { return err } data := &schema.EmailCodeContent{ Email: userInfo.EMail, UserID: userInfo.ID, } code := token.GenerateToken() verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return err } go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) return nil } func (us *UserAdminService) DeletePermanently(ctx context.Context, req *schema.DeletePermanentlyReq) (err error) { switch req.Type { case constant.DeletePermanentlyUsers: return us.userRepo.DeletePermanentlyUsers(ctx) case constant.DeletePermanentlyQuestions: return us.questionCommonRepo.DeletePermanentlyQuestions(ctx) case constant.DeletePermanentlyAnswers: return us.answerCommonRepo.DeletePermanentlyAnswers(ctx) } return errors.BadRequest(reason.RequestFormatError) } // CheckAndUnsuspendExpiredUsers checks for users whose suspension has expired and restores them to normal status func (us *UserAdminService) CheckAndUnsuspendExpiredUsers(ctx context.Context) error { // Find all suspended users whose suspension time has expired expiredUsers, err := us.userRepo.GetExpiredSuspendedUsers(ctx) if err != nil { return err } now := time.Now() for _, user := range expiredUsers { // Check if suspension has expired (not permanent and time has passed) if user.Status == entity.UserStatusSuspended && !user.SuspendedUntil.IsZero() && user.SuspendedUntil.Before(now) { log.Infof("Unsuspending user %s (ID: %s) - suspension expired at %v", user.Username, user.ID, user.SuspendedUntil) // Update user status to normal err = us.userRepo.UpdateUserStatus(ctx, user.ID, entity.UserStatusAvailable, entity.EmailStatusAvailable, user.EMail, time.Time{}) if err != nil { log.Errorf("Failed to unsuspend user %s (ID: %s): %v", user.Username, user.ID, err) continue } } } return nil } ================================================ FILE: internal/service/user_common/user.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package usercommon import ( "context" "strings" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/internal/service/role" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/random" "github.com/mozillazg/go-pinyin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) type UserRepo interface { AddUser(ctx context.Context, user *entity.User) (err error) IncreaseAnswerCount(ctx context.Context, userID string, amount int) (err error) IncreaseQuestionCount(ctx context.Context, userID string, amount int) (err error) UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) UpdateLastLoginDate(ctx context.Context, userID string) (err error) UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error UpdateEmail(ctx context.Context, userID, email string) error UpdateUserInterface(ctx context.Context, userID, language, colorSchema string) (err error) UpdatePass(ctx context.Context, userID, pass string) error UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) UpdateUserProfile(ctx context.Context, userInfo *entity.User) (err error) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) GetUserCount(ctx context.Context) (count int64, err error) SearchUserListByName(ctx context.Context, name string, limit int, onlyStaff bool) (userList []*entity.User, err error) IsAvatarFileUsed(ctx context.Context, filePath string) (bool, error) } // UserCommon user service type UserCommon struct { userRepo UserRepo userRoleService *role.UserRoleRelService authService *auth.AuthService siteInfoCommonService siteinfo_common.SiteInfoCommonService } func NewUserCommon( userRepo UserRepo, userRoleService *role.UserRoleRelService, authService *auth.AuthService, siteInfoCommonService siteinfo_common.SiteInfoCommonService, ) *UserCommon { return &UserCommon{ userRepo: userRepo, userRoleService: userRoleService, authService: authService, siteInfoCommonService: siteInfoCommonService, } } func (us *UserCommon) GetUserBasicInfoByID(ctx context.Context, id string) ( userBasicInfo *schema.UserBasicInfo, exist bool, err error) { userInfo, exist, err := us.userRepo.GetByUserID(ctx, id) if err != nil { return nil, exist, err } info := us.FormatUserBasicInfo(ctx, userInfo) info.Avatar = us.siteInfoCommonService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() return info, exist, nil } func (us *UserCommon) GetUserBasicInfoByUserName(ctx context.Context, username string) (*schema.UserBasicInfo, bool, error) { userInfo, exist, err := us.userRepo.GetByUsername(ctx, username) if err != nil { return nil, exist, err } info := us.FormatUserBasicInfo(ctx, userInfo) info.Avatar = us.siteInfoCommonService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() return info, exist, nil } func (us *UserCommon) BatchGetUserBasicInfoByUserNames(ctx context.Context, usernames []string) (map[string]*schema.UserBasicInfo, error) { infomap := make(map[string]*schema.UserBasicInfo) list, err := us.userRepo.GetByUsernames(ctx, usernames) if err != nil { return infomap, err } avatarMapping := us.siteInfoCommonService.FormatListAvatar(ctx, list) for _, user := range list { info := us.FormatUserBasicInfo(ctx, user) info.Avatar = avatarMapping[user.ID].GetURL() infomap[user.Username] = info } return infomap, nil } func (us *UserCommon) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) { return us.userRepo.GetByEmail(ctx, email) } func (us *UserCommon) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) { return us.userRepo.GetByUsername(ctx, username) } func (us *UserCommon) UpdateUserProfile(ctx context.Context, userInfo *entity.User) (err error) { return us.userRepo.UpdateUserProfile(ctx, userInfo) } func (us *UserCommon) UpdateAnswerCount(ctx context.Context, userID string, num int) error { return us.userRepo.UpdateAnswerCount(ctx, userID, num) } func (us *UserCommon) UpdateQuestionCount(ctx context.Context, userID string, num int64) error { return us.userRepo.UpdateQuestionCount(ctx, userID, num) } func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, userIDs []string) (map[string]*schema.UserBasicInfo, error) { userIDs = checker.FilterEmptyString(userIDs) userMap := make(map[string]*schema.UserBasicInfo) if len(userIDs) == 0 { return userMap, nil } userList, err := us.userRepo.BatchGetByID(ctx, userIDs) if err != nil { return userMap, err } avatarMapping := us.siteInfoCommonService.FormatListAvatar(ctx, userList) for _, user := range userList { info := us.FormatUserBasicInfo(ctx, user) info.Avatar = avatarMapping[user.ID].GetURL() userMap[user.ID] = info } for _, id := range userIDs { if _, ok := userMap[id]; !ok { userMap[id] = &schema.UserBasicInfo{ ID: id, DisplayName: "user" + converter.DeleteUserDisplay(id), Status: constant.UserDeleted, } } } return userMap, nil } // FormatUserBasicInfo format user basic info func (us *UserCommon) FormatUserBasicInfo(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo { userBasicInfo := &schema.UserBasicInfo{} userBasicInfo.ID = userInfo.ID userBasicInfo.Username = userInfo.Username userBasicInfo.Rank = userInfo.Rank userBasicInfo.DisplayName = userInfo.DisplayName userBasicInfo.Website = userInfo.Website userBasicInfo.Location = userInfo.Location userBasicInfo.Language = userInfo.Language userBasicInfo.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus) if !userInfo.SuspendedUntil.IsZero() { userBasicInfo.SuspendedUntil = userInfo.SuspendedUntil.Unix() } if userBasicInfo.Status == constant.UserDeleted { userBasicInfo.Avatar = "" userBasicInfo.DisplayName = "user" + converter.DeleteUserDisplay(userInfo.ID) } return userBasicInfo } // MakeUsername // Generate a unique Username based on the displayName func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (username string, err error) { // Chinese processing if has := checker.IsChinese(displayName); has { displayName = strings.Join(pinyin.LazyConvert(displayName, nil), "") } username = strings.ReplaceAll(displayName, " ", "-") username = strings.ToLower(username) suffix := "" if checker.IsInvalidUsername(username) { return "", errors.BadRequest(reason.UsernameInvalid) } if checker.IsReservedUsername(username) { return "", errors.BadRequest(reason.UsernameInvalid) } for { _, has, err := us.userRepo.GetByUsername(ctx, username+suffix) if err != nil { return "", err } if !has { break } suffix = random.UsernameSuffix() } return username + suffix, nil } func (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, userStatus, emailStatus int, externalID string) ( accessToken string, userCacheInfo *entity.UserCacheInfo, err error) { roleID, err := us.userRoleService.GetUserRole(ctx, userID) if err != nil { log.Error(err) } userCacheInfo = &entity.UserCacheInfo{ UserID: userID, EmailStatus: emailStatus, UserStatus: userStatus, RoleID: roleID, ExternalID: externalID, } accessToken, _, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) if err != nil { return "", nil, err } if userCacheInfo.RoleID == role.RoleAdminID { if err = us.authService.SetAdminUserCacheInfo(ctx, accessToken, userCacheInfo); err != nil { return "", nil, err } } return accessToken, userCacheInfo, nil } func (us *UserCommon) IsAvatarFileUsed(ctx context.Context, filePath string) bool { used, err := us.userRepo.IsAvatarFileUsed(ctx, filePath) if err != nil { log.Errorf("error checking if branding file is used: %v", err) // will try again with the next clean up return true } return used } ================================================ FILE: internal/service/user_external_login/user_center_login_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package user_external_login import ( "context" "encoding/json" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/siteinfo_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/random" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/log" ) // UserCenterLoginService user external login service type UserCenterLoginService struct { userRepo usercommon.UserRepo userExternalLoginRepo UserExternalLoginRepo userCommonService *usercommon.UserCommon userActivity activity.UserActiveActivityRepo siteInfoCommonService siteinfo_common.SiteInfoCommonService } // NewUserCenterLoginService new user external login service func NewUserCenterLoginService( userRepo usercommon.UserRepo, userCommonService *usercommon.UserCommon, userExternalLoginRepo UserExternalLoginRepo, userActivity activity.UserActiveActivityRepo, siteInfoCommonService siteinfo_common.SiteInfoCommonService, ) *UserCenterLoginService { return &UserCenterLoginService{ userRepo: userRepo, userCommonService: userCommonService, userExternalLoginRepo: userExternalLoginRepo, userActivity: userActivity, siteInfoCommonService: siteInfoCommonService, } } func (us *UserCenterLoginService) ExternalLogin( ctx context.Context, userCenter plugin.UserCenter, basicUserInfo *plugin.UserCenterBasicUserInfo) ( resp *schema.UserExternalLoginResp, err error) { if len(basicUserInfo.ExternalID) == 0 { return &schema.UserExternalLoginResp{ ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserExternalLoginMissingUserID), }, nil } if len(basicUserInfo.Email) > 0 { // check whether site allow register or not siteInfo, err := us.siteInfoCommonService.GetSiteLogin(ctx) if err != nil { return nil, err } if !checker.EmailInAllowEmailDomain(basicUserInfo.Email, siteInfo.AllowEmailDomains) { log.Debugf("email domain not allowed: %s", basicUserInfo.Email) return &schema.UserExternalLoginResp{ ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError), }, nil } } oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx, userCenter.Info().SlugName, basicUserInfo.ExternalID) if err != nil { return nil, err } if exist { // if user is already a member, login directly oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID) if err != nil { return nil, err } if exist { // if user is deleted, do not allow login if oldUserInfo.Status == entity.UserStatusDeleted { return &schema.UserExternalLoginResp{ ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserPageAccessDenied), }, nil } if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil { log.Errorf("update user last login date failed: %v", err) } accessToken, _, err := us.userCommonService.CacheLoginUserInfo( ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID) return &schema.UserExternalLoginResp{AccessToken: accessToken}, err } } // cache external user info, waiting for user enter email address. if userCenter.Description().MustAuthEmailEnabled && len(basicUserInfo.Email) == 0 { return &schema.UserExternalLoginResp{ErrMsg: "Requires authorized email to login"}, nil } oldUserInfo, err := us.registerNewUser(ctx, userCenter.Info().SlugName, basicUserInfo) if err != nil { return nil, err } if err := us.activeUser(ctx, oldUserInfo); err != nil { return nil, err } accessToken, _, err := us.userCommonService.CacheLoginUserInfo( ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID) return &schema.UserExternalLoginResp{AccessToken: accessToken}, err } func (us *UserCenterLoginService) registerNewUser(ctx context.Context, provider string, basicUserInfo *plugin.UserCenterBasicUserInfo) (userInfo *entity.User, err error) { userInfo = &entity.User{} userInfo.EMail = basicUserInfo.Email userInfo.DisplayName = basicUserInfo.DisplayName userInfo.Username, err = us.userCommonService.MakeUsername(ctx, basicUserInfo.Username) if err != nil { log.Error(err) userInfo.Username = random.Username() } if len(basicUserInfo.Avatar) > 0 { avatarInfo := &schema.AvatarInfo{ Type: constant.AvatarTypeCustom, Custom: basicUserInfo.Avatar, } avatar, _ := json.Marshal(avatarInfo) userInfo.Avatar = string(avatar) } userInfo.MailStatus = entity.EmailStatusAvailable userInfo.Status = entity.UserStatusAvailable userInfo.LastLoginDate = time.Now() userInfo.Bio = basicUserInfo.Bio userInfo.BioHTML = converter.Markdown2HTML(basicUserInfo.Bio) err = us.userRepo.AddUser(ctx, userInfo) if err != nil { return nil, err } metaInfo, _ := json.Marshal(basicUserInfo) newExternalUserInfo := &entity.UserExternalLogin{ UserID: userInfo.ID, Provider: provider, ExternalID: basicUserInfo.ExternalID, MetaInfo: string(metaInfo), } err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo) if err != nil { return nil, err } return userInfo, nil } func (us *UserCenterLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User) error { if err := us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil { log.Error(err) return err } return nil } func (us *UserCenterLoginService) UserCenterUserSettings(ctx context.Context, userID string) ( resp *schema.UserCenterUserSettingsResp, err error) { resp = &schema.UserCenterUserSettingsResp{} userCenter, ok := plugin.GetUserCenter() if !ok { return resp, nil } // get external login info externalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID) if err != nil { return nil, err } var externalInfo *entity.UserExternalLogin for _, t := range externalLoginList { if t.Provider == userCenter.Info().SlugName { externalInfo = t } } if externalInfo == nil { return resp, nil } settings, err := userCenter.UserSettings(externalInfo.ExternalID) if err != nil { log.Error(err) return resp, nil } if len(settings.AccountSettingRedirectURL) > 0 { resp.AccountSettingAgent = schema.UserSettingAgent{ Enabled: true, RedirectURL: settings.AccountSettingRedirectURL, } } if len(settings.ProfileSettingRedirectURL) > 0 { resp.ProfileSettingAgent = schema.UserSettingAgent{ Enabled: true, RedirectURL: settings.ProfileSettingRedirectURL, } } return resp, nil } // UserCenterAdminFunctionAgent Check in the backend administration interface if the user-related functions // are turned off due to turning on the User Center plugin. func (us *UserCenterLoginService) UserCenterAdminFunctionAgent(ctx context.Context) ( resp *schema.UserCenterAdminFunctionAgentResp, err error) { resp = &schema.UserCenterAdminFunctionAgentResp{ AllowCreateUser: true, AllowUpdateUserStatus: true, AllowUpdateUserPassword: true, AllowUpdateUserRole: true, } userCenter, ok := plugin.GetUserCenter() if !ok { return } desc := userCenter.Description() // If user status agent is enabled, admin can not update user status in answer. resp.AllowUpdateUserStatus = !desc.UserStatusAgentEnabled resp.AllowUpdateUserRole = !desc.UserRoleAgentEnabled // If original user system is enabled, admin can update user password and role in answer. resp.AllowUpdateUserPassword = desc.EnabledOriginalUserSystem resp.AllowCreateUser = desc.EnabledOriginalUserSystem return resp, nil } func (us *UserCenterLoginService) UserCenterPersonalBranding(ctx context.Context, username string) ( resp *schema.UserCenterPersonalBranding, err error) { resp = &schema.UserCenterPersonalBranding{ PersonalBranding: make([]*schema.PersonalBranding, 0), } userCenter, ok := plugin.GetUserCenter() if !ok { return } userInfo, exist, err := us.userRepo.GetByUsername(ctx, username) if err != nil { return nil, err } if !exist { return resp, nil } // get external login info externalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userInfo.ID) if err != nil { return nil, err } var externalInfo *entity.UserExternalLogin for _, t := range externalLoginList { if t.Provider == userCenter.Info().SlugName { externalInfo = t } } if externalInfo == nil { return resp, nil } resp.Enabled = true branding := userCenter.PersonalBranding(externalInfo.ExternalID) for _, t := range branding { resp.PersonalBranding = append(resp.PersonalBranding, &schema.PersonalBranding{ Icon: t.Icon, Name: t.Name, Label: t.Label, Url: t.Url, }) } return resp, nil } ================================================ FILE: internal/service/user_external_login/user_external_login_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package user_external_login import ( "context" "encoding/json" "fmt" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/siteinfo_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/internal/service/user_notification_config" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/random" "github.com/apache/answer/pkg/token" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) type UserExternalLoginRepo interface { AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error) UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error) GetByExternalID(ctx context.Context, provider, externalID string) (userInfo *entity.UserExternalLogin, exist bool, err error) GetByUserID(ctx context.Context, provider, userID string) (userInfo *entity.UserExternalLogin, exist bool, err error) GetUserExternalLoginList(ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error) DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error) DeleteUserExternalLoginByUserID(ctx context.Context, userID string) (err error) SetCacheUserExternalLoginInfo(ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) GetCacheUserExternalLoginInfo(ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) } // UserExternalLoginService user external login service type UserExternalLoginService struct { userRepo usercommon.UserRepo userExternalLoginRepo UserExternalLoginRepo userCommonService *usercommon.UserCommon emailService *export.EmailService siteInfoCommonService siteinfo_common.SiteInfoCommonService userActivity activity.UserActiveActivityRepo userNotificationConfigService *user_notification_config.UserNotificationConfigService } // NewUserExternalLoginService new user external login service func NewUserExternalLoginService( userRepo usercommon.UserRepo, userCommonService *usercommon.UserCommon, userExternalLoginRepo UserExternalLoginRepo, emailService *export.EmailService, siteInfoCommonService siteinfo_common.SiteInfoCommonService, userActivity activity.UserActiveActivityRepo, userNotificationConfigService *user_notification_config.UserNotificationConfigService, ) *UserExternalLoginService { return &UserExternalLoginService{ userRepo: userRepo, userCommonService: userCommonService, userExternalLoginRepo: userExternalLoginRepo, emailService: emailService, siteInfoCommonService: siteInfoCommonService, userActivity: userActivity, userNotificationConfigService: userNotificationConfigService, } } // ExternalLogin if user is already a member logged in func (us *UserExternalLoginService) ExternalLogin( ctx context.Context, externalUserInfo *schema.ExternalLoginUserInfoCache) ( resp *schema.UserExternalLoginResp, err error) { if len(externalUserInfo.ExternalID) == 0 { return &schema.UserExternalLoginResp{ ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserExternalLoginMissingUserID), }, nil } oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx, externalUserInfo.Provider, externalUserInfo.ExternalID) if err != nil { return nil, err } if exist { // if user is already a member, login directly oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID) if err != nil { return nil, err } if exist && oldUserInfo.Status != entity.UserStatusDeleted { if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil { log.Errorf("update user last login date failed: %v", err) } newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo) if err != nil { log.Error(err) } accessToken, _, err := us.userCommonService.CacheLoginUserInfo( ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID) return &schema.UserExternalLoginResp{AccessToken: accessToken}, err } } // cache external user info, waiting for user enter email address. if len(externalUserInfo.Email) == 0 { bindingKey := token.GenerateToken() err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, bindingKey, externalUserInfo) if err != nil { return nil, err } return &schema.UserExternalLoginResp{BindingKey: bindingKey}, nil } // check whether site allow register or not siteInfo, err := us.siteInfoCommonService.GetSiteLogin(ctx) if err != nil { return nil, err } if !checker.EmailInAllowEmailDomain(externalUserInfo.Email, siteInfo.AllowEmailDomains) { log.Debugf("email domain not allowed: %s", externalUserInfo.Email) return &schema.UserExternalLoginResp{ ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError), }, nil } oldUserInfo, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email) if err != nil { return nil, err } // if user is not a member, register a new user if !exist { oldUserInfo, err = us.registerNewUser(ctx, externalUserInfo) if err != nil { return nil, err } } // bind external user info to user err = us.bindOldUser(ctx, externalUserInfo, oldUserInfo) if err != nil { return nil, err } // If user login with external account and email is exist, active user directly. newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo) if err != nil { log.Error(err) } // set default user notification config for external user if err := us.userNotificationConfigService.SetDefaultUserNotificationConfig(ctx, []string{oldUserInfo.ID}); err != nil { log.Errorf("set default user notification config failed, err: %v", err) } accessToken, _, err := us.userCommonService.CacheLoginUserInfo( ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID) return &schema.UserExternalLoginResp{AccessToken: accessToken}, err } func (us *UserExternalLoginService) registerNewUser(ctx context.Context, externalUserInfo *schema.ExternalLoginUserInfoCache) (userInfo *entity.User, err error) { userInfo = &entity.User{} userInfo.EMail = externalUserInfo.Email userInfo.DisplayName = externalUserInfo.DisplayName userInfo.Username, err = us.userCommonService.MakeUsername(ctx, externalUserInfo.Username) if err != nil { log.Error(err) userInfo.Username = random.Username() } if len(externalUserInfo.Avatar) > 0 { avatarInfo := &schema.AvatarInfo{ Type: constant.AvatarTypeCustom, Custom: externalUserInfo.Avatar, } avatar, _ := json.Marshal(avatarInfo) userInfo.Avatar = string(avatar) } userInfo.MailStatus = entity.EmailStatusToBeVerified userInfo.Status = entity.UserStatusAvailable userInfo.LastLoginDate = time.Now() userInfo.Bio = externalUserInfo.Bio userInfo.BioHTML = externalUserInfo.Bio err = us.userRepo.AddUser(ctx, userInfo) if err != nil { return nil, err } return userInfo, nil } func (us *UserExternalLoginService) bindOldUser(ctx context.Context, externalUserInfo *schema.ExternalLoginUserInfoCache, oldUserInfo *entity.User) (err error) { oldExternalUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx, externalUserInfo.Provider, externalUserInfo.ExternalID) if err != nil { return err } if exist { oldExternalUserInfo.MetaInfo = externalUserInfo.MetaInfo oldExternalUserInfo.UserID = oldUserInfo.ID err = us.userExternalLoginRepo.UpdateInfo(ctx, oldExternalUserInfo) } else { newExternalUserInfo := &entity.UserExternalLogin{ UserID: oldUserInfo.ID, Provider: externalUserInfo.Provider, ExternalID: externalUserInfo.ExternalID, MetaInfo: externalUserInfo.MetaInfo, } err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo) } return err } func (us *UserExternalLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User, externalUserInfo *schema.ExternalLoginUserInfoCache) ( mailStatus int, err error) { log.Infof("user %s login with external account, try to active email, old status is %d", oldUserInfo.ID, oldUserInfo.MailStatus) // try to active user email if oldUserInfo.MailStatus == entity.EmailStatusToBeVerified { err = us.userRepo.UpdateEmailStatus(ctx, oldUserInfo.ID, entity.EmailStatusAvailable) if err != nil { return oldUserInfo.MailStatus, err } } // try to update user avatar if oldUserInfo.Avatar == "" && len(externalUserInfo.Avatar) > 0 { avatarInfo := &schema.AvatarInfo{ Type: constant.AvatarTypeCustom, Custom: externalUserInfo.Avatar, } avatar, _ := json.Marshal(avatarInfo) oldUserInfo.Avatar = string(avatar) err = us.userRepo.UpdateInfo(ctx, oldUserInfo) if err != nil { log.Error(err) } } if err = us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil { return oldUserInfo.MailStatus, err } return entity.EmailStatusAvailable, nil } // ExternalLoginBindingUserSendEmail Send an email for third-party account login for binding user func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail( ctx context.Context, req *schema.ExternalLoginBindingUserSendEmailReq) ( resp *schema.ExternalLoginBindingUserSendEmailResp, err error) { siteGeneral, err := us.siteInfoCommonService.GetSiteGeneral(ctx) if err != nil { return nil, err } resp = &schema.ExternalLoginBindingUserSendEmailResp{} externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, req.BindingKey) if err != nil || externalLoginInfo == nil { return nil, errors.BadRequest(reason.UserNotFound) } if len(externalLoginInfo.Email) > 0 { log.Warnf("the binding email has been sent %s", req.BindingKey) return &schema.ExternalLoginBindingUserSendEmailResp{}, nil } userInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email) if err != nil { return nil, err } if exist && !req.Must { resp.EmailExistAndMustBeConfirmed = true return resp, nil } if !exist { externalLoginInfo.Email = req.Email userInfo, err = us.registerNewUser(ctx, externalLoginInfo) if err != nil { return nil, err } resp.AccessToken, _, err = us.userCommonService.CacheLoginUserInfo( ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, externalLoginInfo.ExternalID) if err != nil { log.Error(err) } } err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, req.BindingKey, externalLoginInfo) if err != nil { return nil, err } // send bind confirmation email data := &schema.EmailCodeContent{ SourceType: schema.BindingSourceType, Email: req.Email, UserID: userInfo.ID, BindingKey: req.BindingKey, } code := token.GenerateToken() verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", siteGeneral.SiteUrl, code) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return nil, err } go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) return resp, nil } // ExternalLoginBindingUser // The user clicks on the email link of the bound account and requests the API to bind the user officially func (us *UserExternalLoginService) ExternalLoginBindingUser( ctx context.Context, bindingKey string, oldUserInfo *entity.User) (err error) { externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, bindingKey) if err != nil || externalLoginInfo == nil { return errors.BadRequest(reason.UserNotFound) } return us.bindOldUser(ctx, externalLoginInfo, oldUserInfo) } // GetExternalLoginUserInfoList get external login user info list func (us *UserExternalLoginService) GetExternalLoginUserInfoList( ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error) { return us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID) } // ExternalLoginUnbinding external login unbinding func (us *UserExternalLoginService) ExternalLoginUnbinding( ctx context.Context, req *schema.ExternalLoginUnbindingReq) (resp any, err error) { // If user has only one external login and never set password, he can't unbind it. userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } if len(userInfo.Pass) == 0 { loginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, req.UserID) if err != nil { return nil, err } if len(loginList) <= 1 { return schema.ErrTypeToast, errors.BadRequest(reason.UserExternalLoginUnbindingForbidden) } } return nil, us.userExternalLoginRepo.DeleteUserExternalLogin(ctx, req.UserID, req.ExternalID) } // CheckUserStatusInUserCenter check user status in user center func (us *UserExternalLoginService) CheckUserStatusInUserCenter(ctx context.Context, userID string) ( valid bool, externalID string, err error) { // If enable user center plugin, user status should be checked by user center userCenter, ok := plugin.GetUserCenter() if !ok { return true, "", nil } userInfoList, err := us.GetExternalLoginUserInfoList(ctx, userID) if err != nil { return false, "", err } var thisUcUserInfo *entity.UserExternalLogin for _, t := range userInfoList { if t.Provider == userCenter.Info().SlugName { thisUcUserInfo = t break } } // If this user not login by user center, no need to check user status if thisUcUserInfo == nil { return true, "", nil } userStatus := userCenter.UserStatus(thisUcUserInfo.ExternalID) if userStatus == plugin.UserStatusDeleted { return false, thisUcUserInfo.ExternalID, nil } return true, thisUcUserInfo.ExternalID, nil } ================================================ FILE: internal/service/user_notification_config/user_notification_config_service.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package user_notification_config import ( "context" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" usercommon "github.com/apache/answer/internal/service/user_common" ) type UserNotificationConfigRepo interface { Add(ctx context.Context, userIDs []string, source, channels string) (err error) Save(ctx context.Context, uc *entity.UserNotificationConfig) (err error) GetByUserID(ctx context.Context, userID string) ([]*entity.UserNotificationConfig, error) GetBySource(ctx context.Context, source constant.NotificationSource) ([]*entity.UserNotificationConfig, error) GetByUserIDAndSource(ctx context.Context, userID string, source constant.NotificationSource) ( conf *entity.UserNotificationConfig, exist bool, err error) GetByUsersAndSource(ctx context.Context, userIDs []string, source constant.NotificationSource) ( []*entity.UserNotificationConfig, error) } type UserNotificationConfigService struct { userRepo usercommon.UserRepo userNotificationConfigRepo UserNotificationConfigRepo } func NewUserNotificationConfigService( userRepo usercommon.UserRepo, userNotificationConfigRepo UserNotificationConfigRepo, ) *UserNotificationConfigService { return &UserNotificationConfigService{ userRepo: userRepo, userNotificationConfigRepo: userNotificationConfigRepo, } } func (us *UserNotificationConfigService) GetUserNotificationConfig(ctx context.Context, userID string) ( resp *schema.GetUserNotificationConfigResp, err error) { notificationConfigs, err := us.userNotificationConfigRepo.GetByUserID(ctx, userID) if err != nil { return nil, err } resp = &schema.GetUserNotificationConfigResp{} resp.NotificationConfig = schema.NewNotificationConfig(notificationConfigs) resp.Format() return resp, nil } func (us *UserNotificationConfigService) UpdateUserNotificationConfig( ctx context.Context, req *schema.UpdateUserNotificationConfigReq) (err error) { req.Format() err = us.userNotificationConfigRepo.Save(ctx, us.convertToEntity(ctx, req.UserID, constant.InboxSource, req.Inbox)) if err != nil { return err } err = us.userNotificationConfigRepo.Save(ctx, us.convertToEntity(ctx, req.UserID, constant.AllNewQuestionSource, req.AllNewQuestion)) if err != nil { return err } err = us.userNotificationConfigRepo.Save(ctx, us.convertToEntity(ctx, req.UserID, constant.AllNewQuestionForFollowingTagsSource, req.AllNewQuestionForFollowingTags)) if err != nil { return err } return nil } // SetDefaultUserNotificationConfig set default user notification config for user register func (us *UserNotificationConfigService) SetDefaultUserNotificationConfig(ctx context.Context, userIDs []string) ( err error) { return us.userNotificationConfigRepo.Add(ctx, userIDs, string(constant.InboxSource), `[{"key":"email","enable":true}]`) } func (us *UserNotificationConfigService) convertToEntity(_ context.Context, userID string, source constant.NotificationSource, channel schema.NotificationChannelConfig) (c *entity.UserNotificationConfig) { var channels schema.NotificationChannels channels = append(channels, &channel) c = &entity.UserNotificationConfig{ UserID: userID, Source: string(source), Channels: channels.ToJsonString(), } for _, ch := range channels { if ch.Enable { c.Enabled = true break } } return c } ================================================ FILE: licenserc.toml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. headerPath = "Apache-2.0-ASF.txt" excludes = [ "docs/release/**", "ui/build/**", "answer-data/**", "NOTICE", "DISCLAIMER", "Makefile", "go.mod", "go.sum", "ui/.eslintignore", "ui/.browserslistrc", "ui/.npmrc", "ui/.env.*", "script/plugin_list", "charts/templates/_helpers.tpl", "charts/.helmignore", ] ================================================ FILE: pkg/checker/chinese.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker import "unicode" func IsChinese(str string) bool { for _, v := range str { if unicode.Is(unicode.Han, v) { return true } } return false } ================================================ FILE: pkg/checker/email.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker import "strings" func EmailInAllowEmailDomain(email string, allowEmailDomains []string) bool { if len(allowEmailDomains) == 0 { return true } for _, domain := range allowEmailDomains { if strings.HasSuffix(email, domain) { return true } } return false } ================================================ FILE: pkg/checker/file_type.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker import ( "fmt" "image" _ "image/gif" // use init to support decode jpeg,jpg,png,gif _ "image/jpeg" _ "image/png" "io" "os" "path/filepath" "slices" "strings" "github.com/segmentfault/pacman/log" "golang.org/x/image/webp" ) // IsUnAuthorizedExtension check whether the file extension is not in the allowedExtensions // WANING Only checks the file extension is not reliable, but `http.DetectContentType` and `mimetype` are not reliable for all file types. func IsUnAuthorizedExtension(fileName string, allowedExtensions []string) bool { ext := strings.ToLower(strings.Trim(filepath.Ext(fileName), ".")) return !slices.Contains(allowedExtensions, ext) } // DecodeAndCheckImageFile currently answers support image type is // `image/jpeg, image/jpg, image/png, image/gif, image/webp` func DecodeAndCheckImageFile(localFilePath string, maxImageMegapixel int) bool { ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(localFilePath), ".")) switch ext { case "jpg", "jpeg", "png", "gif": // only allow for `image/jpeg, image/jpg, image/png, image/gif` if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, standardImageConfigCheck) { return false } if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, standardImageCheck) { return false } case "webp": if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, webpImageConfigCheck) { return false } if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, webpImageCheck) { return false } } return true } func decodeAndCheckImageFile(localFilePath string, maxImageMegapixel int, checker func(file io.Reader, maxImageMegapixel int) error) bool { file, err := os.Open(localFilePath) if err != nil { log.Errorf("open file error: %v", err) return false } defer func() { _ = file.Close() }() if err = checker(file, maxImageMegapixel); err != nil { log.Errorf("check image format error: %v", err) return false } return true } func standardImageConfigCheck(file io.Reader, maxImageMegapixel int) error { config, _, err := image.DecodeConfig(file) if err != nil { return fmt.Errorf("decode image config error: %v", err) } if imageSizeTooLarge(config, maxImageMegapixel) { return fmt.Errorf("image size too large") } return nil } func standardImageCheck(file io.Reader, maxImageMegapixel int) error { _, _, err := image.Decode(file) if err != nil { return fmt.Errorf("decode image error: %v", err) } return nil } func webpImageConfigCheck(file io.Reader, maxImageMegapixel int) error { config, err := webp.DecodeConfig(file) if err != nil { return fmt.Errorf("decode webp image config error: %v", err) } if imageSizeTooLarge(config, maxImageMegapixel) { return fmt.Errorf("image size too large") } return nil } func webpImageCheck(file io.Reader, maxImageMegapixel int) error { _, err := webp.Decode(file) if err != nil { return fmt.Errorf("decode webp image error: %v", err) } return nil } func imageSizeTooLarge(config image.Config, maxImageMegapixel int) bool { return config.Width*config.Height > maxImageMegapixel } ================================================ FILE: pkg/checker/password.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker import ( "errors" "fmt" "regexp" "strings" ) const ( levelD = iota LevelC LevelB LevelA LevelS ) const ( PasswordCannotContainSpaces = "error.password.space_invalid" ) // CheckPassword checks the password strength func CheckPassword(password string) error { if strings.Contains(password, " ") { return errors.New(PasswordCannotContainSpaces) } // TODO Currently there is no requirement for password strength minLevel := 0 // The password strength level is initialized to D. // The regular is used to verify the password strength. // If the matching is successful, the password strength increases by 1 level := levelD patternList := []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[~!@#$%^&*?_-]+`} for _, pattern := range patternList { match, _ := regexp.MatchString(pattern, password) if match { level++ } } // If the final password strength falls below the required minimum strength, return with an error if level < minLevel { return fmt.Errorf("the password does not satisfy the current policy requirements") } return nil } ================================================ FILE: pkg/checker/path_ignore.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker import ( "slices" "sync" "github.com/apache/answer/configs" "github.com/segmentfault/pacman/log" "gopkg.in/yaml.v3" ) type PathIgnore struct { Users []string `yaml:"users"` Questions []string `yaml:"questions"` } var ( ignorePathInit sync.Once pathIgnore = &PathIgnore{} ) func initPathIgnore() { if err := yaml.Unmarshal(configs.PathIgnore, pathIgnore); err != nil { log.Error(err) } } // IsUsersIgnorePath checks whether the username is in ignore path func IsUsersIgnorePath(username string) bool { ignorePathInit.Do(initPathIgnore) return slices.Contains(pathIgnore.Users, username) } // IsQuestionsIgnorePath checks whether the questionID is in ignore path func IsQuestionsIgnorePath(questionID string) bool { ignorePathInit.Do(initPathIgnore) return slices.Contains(pathIgnore.Questions, questionID) } ================================================ FILE: pkg/checker/question_link.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker import ( "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/pkg/obj" "github.com/apache/answer/pkg/uid" ) const ( QuestionLinkTypeURL = 1 QuestionLinkTypeID = 2 ) type QuestionLink struct { LinkType int QuestionID string AnswerID string } func GetQuestionLink(content string) []QuestionLink { uniqueIDs := make(map[string]struct{}) var questionLinks []QuestionLink // use two pointer to find the link left, right := 0, 0 for right < len(content) { // find "/questions/" or "#" switch { case right+11 < len(content) && content[right:right+11] == "/questions/": left = right right += 11 processURL(content, &left, &right, uniqueIDs, &questionLinks) case content[right] == '#': left = right + 1 right = left processID(content, &left, &right, uniqueIDs, &questionLinks) default: right++ } } return questionLinks } func processURL(content string, left, right *int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) { for *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) { *right++ } questionID := content[*left+len("/questions/") : *right] answerID := "" if *right < len(content) && content[*right] == '/' { *left = *right + 1 *right = *left for *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) { *right++ } answerID = content[*left:*right] } addUniqueID(questionID, answerID, QuestionLinkTypeURL, uniqueIDs, questionLinks) } func processID(content string, left, right *int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) { for *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) { *right++ } id := content[*left:*right] addUniqueID(id, "", QuestionLinkTypeID, uniqueIDs, questionLinks) } func isDigit(c byte) bool { return c >= '0' && c <= '9' } func isLetter(c byte) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') } func addUniqueID(questionID, answerID string, linkType int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) { isAdd := false if answerID != "" { objectType, err := obj.GetObjectTypeStrByObjectID(uid.DeShortID(answerID)) if err != nil { answerID = "" } else if objectType == constant.AnswerObjectType { if _, ok := uniqueIDs[answerID]; !ok { uniqueIDs[answerID] = struct{}{} isAdd = true } } } if objectType, err := obj.GetObjectTypeStrByObjectID(uid.DeShortID(questionID)); err == nil { if _, ok := uniqueIDs[questionID]; !ok { uniqueIDs[questionID] = struct{}{} isAdd = true if objectType == constant.AnswerObjectType { answerID = questionID questionID = "" } } } if isAdd { *questionLinks = append(*questionLinks, QuestionLink{ LinkType: linkType, QuestionID: questionID, AnswerID: answerID, }) } } ================================================ FILE: pkg/checker/question_link_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker_test import ( "testing" "github.com/apache/answer/pkg/checker" "github.com/stretchr/testify/assert" ) func TestGetQuestionLink(t *testing.T) { // Step 1: Test empty content t.Run("Empty content", func(t *testing.T) { links := checker.GetQuestionLink("") assert.Empty(t, links) }) // Step 2: Test content without link or ID t.Run("Content without link or ID", func(t *testing.T) { links := checker.GetQuestionLink("This is a random text") assert.Empty(t, links) }) // Step 3: Test content with valid question link t.Run("Valid question link", func(t *testing.T) { links := checker.GetQuestionLink("Check this question: https://example.com/questions/10010000000000060") assert.Equal(t, []checker.QuestionLink{ { LinkType: checker.QuestionLinkTypeURL, QuestionID: "10010000000000060", AnswerID: "", }, }, links) }) // Step 4: Test content with valid question and answer link t.Run("Valid question and answer link", func(t *testing.T) { links := checker.GetQuestionLink("Check this answer: https://example.com/questions/10010000000000060/10020000000000060?from=copy") assert.Equal(t, []checker.QuestionLink{ { LinkType: checker.QuestionLinkTypeURL, QuestionID: "10010000000000060", AnswerID: "10020000000000060", }, }, links) }) // Step 5: Test content with #questionID t.Run("Content with #questionID", func(t *testing.T) { links := checker.GetQuestionLink("This is question #10010000000000060") assert.Equal(t, []checker.QuestionLink{ { LinkType: checker.QuestionLinkTypeID, QuestionID: "10010000000000060", AnswerID: "", }, }, links) }) // Step 6: Test content with #answerID t.Run("Content with #answerID", func(t *testing.T) { links := checker.GetQuestionLink("This is answer #10020000000000060") assert.Equal(t, []checker.QuestionLink{ { LinkType: checker.QuestionLinkTypeID, QuestionID: "", AnswerID: "10020000000000060", }, }, links) }) // Step 7: Test invalid question ID t.Run("Invalid question ID", func(t *testing.T) { links := checker.GetQuestionLink("https://example.com/questions/invalid") assert.Empty(t, links) }) // Step 8: Test invalid answer ID t.Run("Invalid answer ID", func(t *testing.T) { links := checker.GetQuestionLink("https://example.com/questions/10010000000000060/invalid") assert.Equal(t, []checker.QuestionLink{ { LinkType: checker.QuestionLinkTypeURL, QuestionID: "10010000000000060", AnswerID: "", }, }, links) }) // Step 9: Test content with multiple links and IDs t.Run("Multiple links and IDs", func(t *testing.T) { content := "Question #10010000000000060 and https://example.com/questions/10010000000000060/10020000000000061 and https://example.com/questions/10010000000000065/10020000000000066 and another #10020000000000066" links := checker.GetQuestionLink(content) assert.Equal(t, []checker.QuestionLink{ { LinkType: checker.QuestionLinkTypeID, QuestionID: "10010000000000060", AnswerID: "", }, { LinkType: checker.QuestionLinkTypeURL, QuestionID: "10010000000000060", AnswerID: "10020000000000061", }, { LinkType: checker.QuestionLinkTypeURL, QuestionID: "10010000000000065", AnswerID: "10020000000000066", }, }, links) }) // Step 11: Test URL with www prefix t.Run("URL with www prefix", func(t *testing.T) { links := checker.GetQuestionLink("Check this question: https://www.example.com/questions/10010000000000060") assert.Equal(t, []checker.QuestionLink{ { LinkType: checker.QuestionLinkTypeURL, QuestionID: "10010000000000060", AnswerID: "", }, }, links) }) // Step 12: Test URL without protocol t.Run("URL without protocol", func(t *testing.T) { links := checker.GetQuestionLink("Check this question: example.com/questions/10010000000000060") assert.Equal(t, []checker.QuestionLink{ { LinkType: checker.QuestionLinkTypeURL, QuestionID: "10010000000000060", AnswerID: "", }, }, links) }) // Step 14: Test error id t.Run("Error id", func(t *testing.T) { links := checker.GetQuestionLink("https://example.com/questions/10110000000000060") assert.Empty(t, links) }) // step 15: SEO options t.Run("SEO options", func(t *testing.T) { content := ` URL1: http://localhost:3000/questions/D1I2 URL2: http://localhost:3000/questions/D1I2/hello URL3: http://localhost:3000/questions/10010000000000068 URL4: http://localhost:3000/questions/10010000000000068/hello ERROR URL: http://localhost:3000/questions/AAAA/BBBB ` links := checker.GetQuestionLink(content) assert.Equal(t, []checker.QuestionLink{ { LinkType: checker.QuestionLinkTypeURL, QuestionID: "D1I2", AnswerID: "", }, { LinkType: checker.QuestionLinkTypeURL, QuestionID: "10010000000000068", AnswerID: "", }, }, links) }) } ================================================ FILE: pkg/checker/reserved_username.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker import ( "encoding/json" "os" "path/filepath" "sync" "github.com/apache/answer/configs" "github.com/apache/answer/internal/base/path" "github.com/apache/answer/pkg/dir" ) var ( reservedUsernameMapping = make(map[string]bool) reservedUsernameInit sync.Once ) func initReservedUsername() { reservedUsernamesJsonFilePath := filepath.Join(path.ConfigFileDir, path.DefaultReservedUsernamesConfigFileName) if dir.CheckFileExist(reservedUsernamesJsonFilePath) { // if reserved username file exists, read it and replace configuration reservedUsernamesJsonFile, err := os.ReadFile(reservedUsernamesJsonFilePath) if err == nil { configs.ReservedUsernames = reservedUsernamesJsonFile } } var usernames []string _ = json.Unmarshal(configs.ReservedUsernames, &usernames) for _, username := range usernames { reservedUsernameMapping[username] = true } } // IsReservedUsername checks whether the username is reserved func IsReservedUsername(username string) bool { reservedUsernameInit.Do(initReservedUsername) return reservedUsernameMapping[username] } ================================================ FILE: pkg/checker/url.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker import ( "net/url" "strings" ) func IsURL(str string) bool { s := strings.ToLower(str) if len(s) == 0 { return false } u, err := url.Parse(s) if err != nil || u.Scheme == "" { return false } if u.Host == "" && u.Fragment == "" && u.Opaque == "" { return false } return u.Scheme == "http" || u.Scheme == "https" } ================================================ FILE: pkg/checker/username.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker import "regexp" var ( usernameReg = regexp.MustCompile(`^[\w.\- ]{2,30}$`) ) func IsInvalidUsername(username string) bool { return !usernameReg.MatchString(username) } ================================================ FILE: pkg/checker/zero_string.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package checker // IsNotZeroString check s is not empty string and is not "0" func IsNotZeroString(s string) bool { return len(s) > 0 && s != "0" } // FilterEmptyString filter empty string from string slice func FilterEmptyString(strs []string) []string { var result []string for _, str := range strs { if IsNotZeroString(str) { result = append(result, str) } } return result } ================================================ FILE: pkg/converter/array.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package converter func ArrayNotInArray(original []string, search []string) []string { var result []string originalMap := make(map[string]bool) for _, v := range original { originalMap[v] = true } for _, v := range search { if _, ok := originalMap[v]; !ok { result = append(result, v) } } return result } func UniqueArray[T comparable](input []T) []T { result := make([]T, 0, len(input)) seen := make(map[T]bool, len(input)) for _, element := range input { if !seen[element] { result = append(result, element) seen[element] = true } } return result } ================================================ FILE: pkg/converter/markdown.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package converter import ( "bytes" "regexp" "strings" "github.com/asaskevich/govalidator" "github.com/microcosm-cc/bluemonday" "github.com/segmentfault/pacman/log" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" goldmarkHTML "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/util" ) // Markdown2HTML convert markdown to html func Markdown2HTML(source string) string { mdConverter := goldmark.New( goldmark.WithExtensions(&DangerousHTMLFilterExtension{}, extension.GFM, extension.Footnote), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), goldmark.WithRendererOptions( goldmarkHTML.WithHardWraps(), ), ) var buf bytes.Buffer if err := mdConverter.Convert([]byte(source), &buf); err != nil { log.Error(err) return source } html := buf.String() filter := bluemonday.UGCPolicy() filter.AllowStyling() filter.RequireNoFollowOnLinks(false) filter.RequireParseableURLs(false) filter.RequireNoFollowOnFullyQualifiedLinks(false) filter.AllowElements("kbd") filter.AllowAttrs("title").Matching(regexp.MustCompile(`^[\p{L}\p{N}\s\-_',\[\]!\./\\\(\)]*$|^@embed?$`)).Globally() filter.AllowAttrs("start").OnElements("ol") html = strings.TrimSpace(filter.Sanitize(html)) return html } // Markdown2BasicHTML convert markdown to html, Only basic syntax can be used func Markdown2BasicHTML(source string) string { content := Markdown2HTML(source) filter := bluemonday.NewPolicy() filter.AllowElements("p", "b", "br", "strong", "em") filter.AllowAttrs("src").OnElements("img") filter.AddSpaceWhenStrippingTag(true) content = filter.Sanitize(content) return content } type DangerousHTMLFilterExtension struct { } func (e *DangerousHTMLFilterExtension) Extend(m goldmark.Markdown) { m.Renderer().AddOptions(renderer.WithNodeRenderers( util.Prioritized(&DangerousHTMLRenderer{ Config: goldmarkHTML.NewConfig(), Filter: bluemonday.UGCPolicy(), }, 1), )) } type DangerousHTMLRenderer struct { goldmarkHTML.Config Filter *bluemonday.Policy } // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock) reg.Register(ast.KindRawHTML, r.renderRawHTML) reg.Register(ast.KindLink, r.renderLink) reg.Register(ast.KindAutoLink, r.renderAutoLink) } func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkSkipChildren, nil } n := node.(*ast.RawHTML) l := n.Segments.Len() for i := range l { segment := n.Segments.At(i) if string(source[segment.Start:segment.Stop]) == "" || string(source[segment.Start:segment.Stop]) == "" { _, _ = w.Write(segment.Value(source)) } else { _, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source))) } } return ast.WalkSkipChildren, nil } func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.HTMLBlock) if entering { l := n.Lines().Len() for i := range l { line := n.Lines().At(i) r.Writer.SecureWrite(w, line.Value(source)) } } else if n.HasClosure() { closure := n.ClosureLine r.Writer.SecureWrite(w, closure.Value(source)) } return ast.WalkContinue, nil } func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.Link) if entering && r.renderLinkIsUrl(string(n.Destination)) { _, _ = w.WriteString("') } else { _, _ = w.WriteString("") } return ast.WalkContinue, nil } func (r *DangerousHTMLRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.AutoLink) if !entering || !r.renderLinkIsUrl(string(n.URL(source))) { return ast.WalkContinue, nil } _, _ = w.WriteString(`') } else { _, _ = w.WriteString(`">`) } _, _ = w.Write(util.EscapeHTML(label)) _, _ = w.WriteString(``) return ast.WalkContinue, nil } func (r *DangerousHTMLRenderer) renderLinkIsUrl(verifyURL string) bool { isURL := govalidator.IsURL(verifyURL) isPath, _ := regexp.MatchString(`^/`, verifyURL) return isURL || isPath } ================================================ FILE: pkg/converter/str.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package converter import ( "fmt" "strconv" "github.com/segmentfault/pacman/log" ) func StringToInt64(str string) int64 { num, err := strconv.ParseInt(str, 10, 64) if err != nil { return 0 } return num } func StringToInt(str string) int { num, err := strconv.Atoi(str) if err != nil { return 0 } return num } func IntToString(data int64) string { return fmt.Sprintf("%d", data) } // InterfaceToString converts data to string // It will be used in template render func InterfaceToString(data any) string { switch t := data.(type) { case int: i := data.(int) return strconv.Itoa(i) case int8: i := data.(int8) return strconv.Itoa(int(i)) case int16: i := data.(int16) return strconv.Itoa(int(i)) case int32: i := data.(int32) return string(i) case int64: i := data.(int64) return strconv.FormatInt(i, 10) case string: return data.(string) default: log.Warn("can't convert type:", t) } return "" } ================================================ FILE: pkg/converter/user.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package converter import ( "regexp" "github.com/segmentfault/pacman/utils" ) func DeleteUserDisplay(userID string) string { return utils.EnShortID(StringToInt64(userID), 100) } func GetMentionUsernameList(text string) []string { re := regexp.MustCompile(`\[@([^\]]+)\]\(/users/[^\)]+\)`) matches := re.FindAllStringSubmatch(text, -1) var usernames []string for _, match := range matches { if len(match) > 1 { usernames = append(usernames, match[1]) } } return usernames } ================================================ FILE: pkg/day/day.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package day import ( "strings" "time" ) var placeholder = map[string]string{ "YY": "06", // 06 year "YYYY": "2006", // 2006 full year "M": "1", // 1-12 month "MM": "01", // 01-12 month "MMM": "Jan", // Jan-Dec month "MMMM": "January", // January-December month "D": "2", // 1-31 date "DD": "02", // 01-31 date preset 0 "H": "15", // 00-23 hour 24 "HH": "15", // 00-23 hour 24 "h": "3", // 1-12 hour 12 "hh": "03", // 01-12 hour 12 "m": "4", // 0-59 minute "mm": "04", // 00-59 minute "s": "5", // 0-59 second "ss": "05", // 00-59 second "A": "PM", // AM / PM "a": "pm", // am / pm "[at]": "at", // at string } func Format(unix int64, format, tz string) (formatted string) { /*l := len(placeholders) - 1 for i := l; i >= 0; i-- { format = strings.ReplaceAll(format, placeholders[i].old, placeholders[i].new) }*/ var toFormat strings.Builder from := []rune(format) for len(from) > 0 { to, suffix := nextStdChunk(from) toFormat.WriteString(string(to)) from = suffix } _, _ = time.LoadLocation(tz) formatted = time.Unix(unix, 0).Format(toFormat.String()) return } func nextStdChunk(from []rune) (to, suffix []rune) { if len(from) == 0 { to = []rune{} suffix = []rune{} return } s := string(from[0]) old := "" switch s { case "Y": if len(from) >= 4 && string(from[:4]) == "YYYY" { old = "YYYY" } else if len(from) >= 2 && string(from[:2]) == "YY" { old = "YY" } case "M": for i := 4; i > 0; i-- { if len(from) >= i { switch string(from[:i]) { case "MMMM": old = "MMMM" case "MMM": old = "MMM" case "MM": old = "MM" case "M": old = "M" } } if old != "" { break } } case "D": for i := 2; i >= 0; i-- { if len(from) >= i { switch string(from[:i]) { case "DD": old = "DD" case "D": old = "D" } } if old != "" { break } } case "H": for i := 2; i >= 0; i-- { if len(from) >= i { switch string(from[:i]) { case "HH": old = "HH" case "H": old = "H" } } if old != "" { break } } case "h": for i := 2; i >= 0; i-- { if len(from) >= i { switch string(from[:i]) { case "hh": old = "hh" case "h": old = "h" } } if old != "" { break } } case "m": for i := 2; i >= 0; i-- { if len(from) >= i { switch string(from[:i]) { case "mm": old = "mm" case "m": old = "m" } } if old != "" { break } } case "s": for i := 2; i >= 0; i-- { if len(from) >= i { switch string(from[:i]) { case "ss": old = "ss" case "s": old = "s" } } if old != "" { break } } case "A": old = "A" case "a": old = "a" case "[": if len(from) >= 4 && string(from[:4]) == "[at]" { old = "[at]" } default: old = s } tos, ok := placeholder[old] if !ok { to = []rune(old) } else { to = []rune(tos) } suffix = from[len([]rune(old)):] return } ================================================ FILE: pkg/day/day_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package day import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestFormat(t *testing.T) { sec := time.Now().Unix() tz := "Asia/Shanghai" actual := Format(sec, "YYYY-MM-DD HH:mm:ss", tz) _, _ = time.LoadLocation(tz) expected := time.Unix(sec, 0).Format("2006-01-02 15:04:05") assert.Equal(t, expected, actual) } ================================================ FILE: pkg/dir/dir.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package dir import ( "fmt" "os" "path/filepath" ) func CreateDirIfNotExist(path string) error { return os.MkdirAll(path, os.ModePerm) } func CheckDirExist(path string) bool { f, err := os.Stat(path) return err == nil && f.IsDir() } func CheckFileExist(path string) bool { f, err := os.Stat(path) return err == nil && !f.IsDir() } func DirSize(path string) (int64, error) { var size int64 err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { if !info.IsDir() { size += info.Size() } return err }) return size, err } func FormatFileSize(fileSize int64) (size string) { switch { case fileSize < 1024: // return strconv.FormatInt(fileSize, 10) + "B" return fmt.Sprintf("%.2f B", float64(fileSize)/float64(1)) case fileSize < (1024 * 1024): return fmt.Sprintf("%.2f KB", float64(fileSize)/float64(1024)) case fileSize < (1024 * 1024 * 1024): return fmt.Sprintf("%.2f MB", float64(fileSize)/float64(1024*1024)) case fileSize < (1024 * 1024 * 1024 * 1024): return fmt.Sprintf("%.2f GB", float64(fileSize)/float64(1024*1024*1024)) case fileSize < (1024 * 1024 * 1024 * 1024 * 1024): return fmt.Sprintf("%.2f TB", float64(fileSize)/float64(1024*1024*1024*1024)) default: // if fileSize < (1024 * 1024 * 1024 * 1024 * 1024 * 1024) return fmt.Sprintf("%.2f EB", float64(fileSize)/float64(1024*1024*1024*1024*1024)) } } ================================================ FILE: pkg/display/url.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package display import ( "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/uid" ) // QuestionURL get question url func QuestionURL(permalink int, siteUrl, questionID, title string) string { u := siteUrl + "/questions" if permalink == constant.PermalinkQuestionIDAndTitle || permalink == constant.PermalinkQuestionID { questionID = uid.DeShortID(questionID) } else { questionID = uid.EnShortID(questionID) } u += "/" + questionID if permalink == constant.PermalinkQuestionIDAndTitle || permalink == constant.PermalinkQuestionIDAndTitleByShortID { u += "/" + htmltext.UrlTitle(title) } return u } // AnswerURL get answer url func AnswerURL(permalink int, siteUrl, questionID, title, answerID string) string { if permalink == constant.PermalinkQuestionIDAndTitle || permalink == constant.PermalinkQuestionID { answerID = uid.DeShortID(answerID) } else { answerID = uid.EnShortID(answerID) } return QuestionURL(permalink, siteUrl, questionID, title) + "/" + answerID } // CommentURL get comment url func CommentURL(permalink int, siteUrl, questionID, title, answerID, commentID string) string { if len(answerID) > 0 { return AnswerURL(permalink, siteUrl, questionID, answerID, title) + "?commentId=" + commentID } return QuestionURL(permalink, siteUrl, questionID, title) + "?commentId=" + commentID } // UserURL get user url func UserURL(siteUrl, username string) string { return siteUrl + "/users/" + username } ================================================ FILE: pkg/encryption/md5.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package encryption import ( "crypto/md5" "encoding/hex" ) // MD5 return md5 hash func MD5(data string) string { h := md5.New() h.Write([]byte(data)) return hex.EncodeToString(h.Sum(nil)) } ================================================ FILE: pkg/gravatar/gravatar.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package gravatar import ( "crypto/sha256" "encoding/hex" "fmt" "net/url" "strings" ) // GetAvatarURL get avatar url from gravatar by email func GetAvatarURL(baseURL, email string) string { hasher := sha256.Sum256([]byte(strings.TrimSpace(email))) hash := hex.EncodeToString(hasher[:]) return baseURL + hash } // Resize resize avatar by pixel func Resize(originalAvatarURL string, sizePixel int) (resizedAvatarURL string) { if len(originalAvatarURL) == 0 { return } originalURL, err := url.Parse(originalAvatarURL) if err != nil { return originalAvatarURL } query := originalURL.Query() query.Set("s", fmt.Sprintf("%d", sizePixel)) originalURL.RawQuery = query.Encode() return originalURL.String() } ================================================ FILE: pkg/gravatar/gravatar_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package gravatar import ( "testing" "github.com/apache/answer/internal/base/constant" "github.com/stretchr/testify/assert" ) func TestGetAvatarURL(t *testing.T) { type args struct { email string } tests := []struct { name string args args want string }{ { name: "answer@answer.com", args: args{email: "answer@answer.com"}, want: "https://www.gravatar.com/avatar/7296942c1f63d97f6c124705142009867638f7b3dbcdadd0cb1bcb40e427eb8e", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, GetAvatarURL(constant.DefaultGravatarBaseURL, tt.args.email)) }) } } func TestResize(t *testing.T) { type args struct { originalAvatarURL string sizePixel int } tests := []struct { name string args args wantResizedAvatarURL string }{ { name: "original url", args: args{ originalAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7", sizePixel: 128, }, wantResizedAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=128", }, { name: "already resized url", args: args{ originalAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=128", sizePixel: 64, }, wantResizedAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=64", }, { name: "empty url", args: args{ originalAvatarURL: "", sizePixel: 64, }, wantResizedAvatarURL: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equalf(t, tt.wantResizedAvatarURL, Resize(tt.args.originalAvatarURL, tt.args.sizePixel), "Resize(%v, %v)", tt.args.originalAvatarURL, tt.args.sizePixel) }) } } ================================================ FILE: pkg/htmltext/htmltext.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package htmltext import ( "io" "net/http" "net/url" "regexp" "strings" "unicode/utf8" "github.com/Machiel/slugify" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/converter" strip "github.com/grokify/html-strip-tags-go" "github.com/mozillazg/go-pinyin" ) var ( reCode = regexp.MustCompile(`(?ism)<(pre)>.*<\/pre>`) reCodeReplace = "{code...}" reLink = regexp.MustCompile(`(?ism)(.*)?<\/a>`) reLinkReplace = " [$1] " reSpace = regexp.MustCompile(` +`) reSpaceReplace = " " spaceReplacer = strings.NewReplacer( "\n", " ", "\r", " ", "\t", " ", ) ) // ClearText clear HTML, get the clear text func ClearText(html string) string { if html == "" { return html } html = reCode.ReplaceAllString(html, reCodeReplace) html = reLink.ReplaceAllString(html, reLinkReplace) text := spaceReplacer.Replace(strip.StripTags(html)) // replace multiple spaces to one space return strings.TrimSpace(reSpace.ReplaceAllString(text, reSpaceReplace)) } func UrlTitle(title string) (text string) { title = convertChinese(title) title = clearEmoji(title) title = slugify.Slugify(title) title = url.QueryEscape(title) title = cutLongTitle(title) if len(title) == 0 { title = "topic" } return title } func clearEmoji(s string) string { var ret strings.Builder rs := []rune(s) for i := range rs { if len(string(rs[i])) != 4 { ret.WriteString(string(rs[i])) } } return ret.String() } func convertChinese(content string) string { has := checker.IsChinese(content) if !has { return content } return strings.Join(pinyin.LazyConvert(content, nil), "-") } func cutLongTitle(title string) string { maxBytes := 150 if len(title) <= maxBytes { return title } truncated := title[:maxBytes] for len(truncated) > 0 && !utf8.ValidString(truncated) { truncated = truncated[:len(truncated)-1] } return truncated } // FetchExcerpt return the excerpt from the HTML string func FetchExcerpt(html, trimMarker string, limit int) (text string) { return FetchRangedExcerpt(html, trimMarker, 0, limit) } // findFirstMatchedWord returns the first matched word and its index func findFirstMatchedWord(text string, words []string) (string, int) { if len(text) == 0 || len(words) == 0 { return "", 0 } words = converter.UniqueArray(words) firstWord := "" firstIndex := len(text) for _, word := range words { if idx := strings.Index(text, word); idx != -1 && idx < firstIndex { firstIndex = idx firstWord = word } } if firstIndex != len(text) { return firstWord, firstIndex } return "", 0 } // getRuneRange returns the valid begin and end indexes of the runeText func getRuneRange(runeText []rune, offset, limit int) (begin, end int) { runeLen := len(runeText) limit = min(runeLen, max(0, limit)) begin = min(runeLen, max(0, offset)) end = min(runeLen, begin+limit) return } // FetchRangedExcerpt returns a ranged excerpt from the HTML string. // Note: offset is a rune index, not a byte index func FetchRangedExcerpt(html, trimMarker string, offset int, limit int) (text string) { if len(html) == 0 { text = html return } runeText := []rune(ClearText(html)) begin, end := getRuneRange(runeText, offset, limit) text = string(runeText[begin:end]) if begin > 0 { text = trimMarker + text } if end < len(runeText) { text += trimMarker } return } // FetchMatchedExcerpt returns the matched excerpt according to the words func FetchMatchedExcerpt(html string, words []string, trimMarker string, trimLength int) string { text := ClearText(html) matchedWord, matchedIndex := findFirstMatchedWord(text, words) runeIndex := utf8.RuneCountInString(text[0:matchedIndex]) trimLength = max(0, trimLength) runeOffset := runeIndex - trimLength runeLimit := trimLength + trimLength + utf8.RuneCountInString(matchedWord) textRuneCount := utf8.RuneCountInString(text) if runeOffset+runeLimit > textRuneCount { // Reserved extra chars before the matched word runeOffset = textRuneCount - runeLimit } return FetchRangedExcerpt(html, trimMarker, runeOffset, runeLimit) } func GetPicByUrl(url string) string { res, err := http.Get(url) if err != nil { return "" } defer func() { _ = res.Body.Close() }() pix, err := io.ReadAll(res.Body) if err != nil { return "" } return string(pix) } ================================================ FILE: pkg/htmltext/htmltext_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package htmltext import ( "fmt" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestClearText(t *testing.T) { var ( expected, clearedText string ) // test code clear text expected = "hello{code...}" clearedText = ClearText("

hello

var a = \"good\"

") assert.Equal(t, expected, clearedText) // test link clear text expected = "hello [example.com]" clearedText = ClearText("

hello example.com

") assert.Equal(t, expected, clearedText) clearedText = ClearText("

helloexample.com

") assert.Equal(t, expected, clearedText) expected = "hello world" clearedText = ClearText("
hello
\n
world
") assert.Equal(t, expected, clearedText) } func TestFetchExcerpt(t *testing.T) { var ( expected, text string ) // test english string expected = "hello..." text = FetchExcerpt("

hello world

", "...", 5) assert.Equal(t, expected, text) // test mixed string expected = "hello你好..." text = FetchExcerpt("

hello你好world

", "...", 7) assert.Equal(t, expected, text) // test mixed string with emoticon expected = "hello你好😂..." text = FetchExcerpt("

hello你好😂world

", "...", 8) assert.Equal(t, expected, text) expected = "hello你好" text = FetchExcerpt("

hello你好

", "...", 8) assert.Equal(t, expected, text) } func TestUrlTitle(t *testing.T) { list := []string{ "hello你好😂...", "这是一个,标题,title", } for _, title := range list { formatTitle := UrlTitle(title) fmt.Println(formatTitle) } } func TestFindFirstMatchedWord(t *testing.T) { var ( expectedWord, actualWord string expectedIndex, actualIndex int ) text := "Hello, I have 中文 and 😂 and I am supposed to work fine." // test find nothing expectedWord, expectedIndex = "", 0 actualWord, actualIndex = findFirstMatchedWord(text, []string{"youcantfindme"}) assert.Equal(t, expectedWord, actualWord) assert.Equal(t, expectedIndex, actualIndex) // test find one word expectedWord, expectedIndex = "文", 17 actualWord, actualIndex = findFirstMatchedWord(text, []string{"文"}) assert.Equal(t, expectedWord, actualWord) assert.Equal(t, expectedIndex, actualIndex) // test find multiple matched words expectedWord, expectedIndex = "Hello", 0 actualWord, actualIndex = findFirstMatchedWord(text, []string{"Hello", "文"}) assert.Equal(t, expectedWord, actualWord) assert.Equal(t, expectedIndex, actualIndex) } func TestGetRuneRange(t *testing.T) { var ( expectedBegin, expectedEnd, actualBegin, actualEnd int ) runeText := []rune("Hello, I have 中文 and 😂.") runeLen := len(runeText) // test get range of negative offset and negative limit expectedBegin, expectedEnd = 0, 0 actualBegin, actualEnd = getRuneRange(runeText, -1, -1) assert.Equal(t, expectedBegin, actualBegin) assert.Equal(t, expectedEnd, actualEnd) // test get range of exceeding offset and exceeding limit expectedBegin, expectedEnd = runeLen, runeLen actualBegin, actualEnd = getRuneRange(runeText, runeLen+1, runeLen+1) assert.Equal(t, expectedBegin, actualBegin) assert.Equal(t, expectedEnd, actualEnd) // test get range of normal offset and exceeding limit expectedBegin, expectedEnd = 3, runeLen actualBegin, actualEnd = getRuneRange(runeText, 3, runeLen) assert.Equal(t, expectedBegin, actualBegin) assert.Equal(t, expectedEnd, actualEnd) // test get range of normal offset and normal limit expectedBegin, expectedEnd = 3, 10 actualBegin, actualEnd = getRuneRange(runeText, 3, 7) assert.Equal(t, expectedBegin, actualBegin) assert.Equal(t, expectedEnd, actualEnd) } func TestFetchRangedExcerpt(t *testing.T) { var ( expected, actual string ) // test english string expected = "hello..." actual = FetchRangedExcerpt("

hello world

", "...", 0, 5) assert.Equal(t, expected, actual) // test string with offset expected = "...llo你好..." actual = FetchRangedExcerpt("

hello你好world

", "...", 2, 5) assert.Equal(t, expected, actual) // test mixed string with emoticon with offset expected = "...你好😂..." actual = FetchRangedExcerpt("

hello你好😂world

", "...", 5, 3) assert.Equal(t, expected, actual) // test mixed string with offset and exceeding limit expected = "...你好😂world" actual = FetchRangedExcerpt("

hello你好😂world

", "...", 5, 100) assert.Equal(t, expected, actual) } func TestCutLongTitle(t *testing.T) { // Short title, no cutting needed short := "hello" assert.Equal(t, short, cutLongTitle(short)) // Exactly max bytes, no cutting needed exact150 := strings.Repeat("a", 150) assert.Len(t, cutLongTitle(exact150), 150) // Just over max bytes, should be cut exact151 := strings.Repeat("a", 151) assert.Len(t, cutLongTitle(exact151), 150) // Multi-byte rune at boundary gets removed properly asciiPart := strings.Repeat("a", 149) // 149 bytes multiByteChar := "中" // 3 bytes - will span bytes 149-151 title := asciiPart + multiByteChar // 152 bytes total assert.Equal(t, asciiPart, cutLongTitle(title)) } func TestFetchMatchedExcerpt(t *testing.T) { var ( expected, actual string ) html := "

Hello, I have 中文 and 😂 and I am supposed to work fine

" // test find nothing // it should return from the begin with double trimLength text expected = "Hello, I h..." actual = FetchMatchedExcerpt(html, []string{"youcantfindme"}, "...", 5) assert.Equal(t, expected, actual) // test find the word at the end // it should return the word beginning with double trimLenth plus len(word) expected = "... work fine" actual = FetchMatchedExcerpt(html, []string{"youcant", "fine"}, "...", 3) assert.Equal(t, expected, actual) // test find multiple words // it should return the first matched word with trimmedText expected = "... have 中文 and 😂..." actual = FetchMatchedExcerpt(html, []string{"中文", "😂"}, "...", 6) assert.Equal(t, expected, actual) } ================================================ FILE: pkg/obj/obj.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package obj import ( "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" ) // GetObjectTypeStrByObjectID get object key by object id func GetObjectTypeStrByObjectID(objectID string) (objectTypeStr string, err error) { if err := checkObjectID(objectID); err != nil { return "", err } objectTypeNumber := converter.StringToInt(objectID[1:4]) objectTypeStr, ok := constant.ObjectTypeNumberMapping[objectTypeNumber] if ok { return objectTypeStr, nil } return "", errors.BadRequest(reason.ObjectNotFound) } // GetObjectTypeNumberByObjectID get object type by object id func GetObjectTypeNumberByObjectID(objectID string) (objectTypeNumber int, err error) { if err := checkObjectID(objectID); err != nil { return 0, err } return converter.StringToInt(objectID[1:4]), nil } func checkObjectID(objectID string) (err error) { if len(objectID) < 5 { return errors.BadRequest(reason.ObjectNotFound) } return nil } ================================================ FILE: pkg/random/random_username.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package random import ( "crypto/rand" "encoding/hex" ) func UsernameSuffix() string { bytes := make([]byte, 2) _, _ = rand.Read(bytes) return hex.EncodeToString(bytes) } func Username() string { bytes := make([]byte, 6) _, _ = rand.Read(bytes) return hex.EncodeToString(bytes) } ================================================ FILE: pkg/token/token.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package token import "github.com/google/uuid" // GenerateToken generate token func GenerateToken() string { uid, _ := uuid.NewV7() return uid.String() } ================================================ FILE: pkg/uid/id.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package uid import ( "math/rand" "time" "github.com/bwmarrin/snowflake" ) // SnowFlakeID snowflake id type SnowFlakeID struct { *snowflake.Node } var snowFlakeIDGenerator *SnowFlakeID func init() { source := rand.NewSource(time.Now().UnixNano()) r := rand.New(source) node, err := snowflake.NewNode(int64(r.Intn(1000)) + 1) if err != nil { panic(err.Error()) } snowFlakeIDGenerator = &SnowFlakeID{node} } func ID() snowflake.ID { id := snowFlakeIDGenerator.Generate() return id } func IDStr12() string { id := snowFlakeIDGenerator.Generate() return id.Base58() } func IDStr() string { id := snowFlakeIDGenerator.Generate() return id.Base32() } ================================================ FILE: pkg/uid/sid.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package uid import ( "strconv" "github.com/segmentfault/pacman/utils" ) const salt = int64(100) // NumToShortID num to string func NumToShortID(id int64) string { sid := strconv.FormatInt(id, 10) if len(sid) < 17 { return "" } sTypeCode := sid[1:4] sid = sid[4:int32(len(sid))] id, err := strconv.ParseInt(sid, 10, 64) if err != nil { return "" } typeCode, err := strconv.ParseInt(sTypeCode, 10, 64) if err != nil { return "" } code := utils.EnShortID(id, salt) tcode := utils.EnShortID(typeCode, salt) return tcode + code } // ShortIDToNum string to num func ShortIDToNum(code string) int64 { if len(code) < 2 { return 0 } scodeType := code[0:2] code = code[2:int32(len(code))] id := utils.DeShortID(code, salt) codeType := utils.DeShortID(scodeType, salt) return 10000000000000000 + codeType*10000000000000 + id } func EnShortID(id string) string { num, err := strconv.ParseInt(id, 10, 64) if err != nil { return id } return NumToShortID(num) } func DeShortID(sid string) string { num, err := strconv.ParseInt(sid, 10, 64) if err != nil { return strconv.FormatInt(ShortIDToNum(sid), 10) } if num < 10000000000000000 { return strconv.FormatInt(ShortIDToNum(sid), 10) } return sid } func IsShortID(id string) bool { num, err := strconv.ParseInt(id, 10, 64) if err != nil { return true } if num < 10000000000000000 { return true } return false } ================================================ FILE: pkg/writer/writer.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package writer import ( "bufio" "os" ) // ReplaceFile remove old file and write new file func ReplaceFile(filePath, content string) error { _ = os.Remove(filePath) return WriteFile(filePath, content) } // WriteFile write file to path func WriteFile(filePath, content string) error { file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666) if err != nil { return err } defer func() { _ = file.Close() }() writer := bufio.NewWriter(file) if _, err := writer.WriteString(content); err != nil { return err } if err := writer.Flush(); err != nil { return err } return nil } // MoveFile move file to new path func MoveFile(oldPath, newPath string) error { return os.Rename(oldPath, newPath) } ================================================ FILE: plugin/agent.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin import ( "github.com/gin-gonic/gin" ) type Agent interface { Base RegisterUnAuthRouter(r *gin.RouterGroup) RegisterAuthUserRouter(r *gin.RouterGroup) RegisterAuthAdminRouter(r *gin.RouterGroup) } var ( CallAgent, registerAgent = MakePlugin[Agent](true) siteURLFn func() string ) // SiteURL The site url is the domain address of the current site. e.g. http://localhost:8080 // When some Agent plugins want to redirect to the origin site, it can use this function to get the site url. func SiteURL() string { if siteURLFn != nil { return siteURLFn() } return "" } // RegisterGetSiteURLFunc Register a function to get the site url. func RegisterGetSiteURLFunc(fn func() string) { siteURLFn = fn } ================================================ FILE: plugin/base.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin // Info presents the plugin information type Info struct { Name Translator SlugName string Description Translator Author string Version string Link string } // Base is the base plugin type Base interface { // Info returns the plugin information Info() Info } var ( // CallBase is a function that calls all registered base plugins CallBase, registerBase = MakePlugin[Base](true) ) ================================================ FILE: plugin/cache.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin import ( "context" "time" ) type Cache interface { Base GetString(ctx context.Context, key string) (data string, exist bool, err error) SetString(ctx context.Context, key, value string, ttl time.Duration) (err error) GetInt64(ctx context.Context, key string) (data int64, exist bool, err error) SetInt64(ctx context.Context, key string, value int64, ttl time.Duration) (err error) Increase(ctx context.Context, key string, value int64) (data int64, err error) Decrease(ctx context.Context, key string, value int64) (data int64, err error) Del(ctx context.Context, key string) (err error) Flush(ctx context.Context) (err error) } var ( // CallCache is a function that calls all registered cache CallCache, registerCache = MakePlugin[Cache](false) ) ================================================ FILE: plugin/captcha.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin type Captcha interface { Base // GetConfig required. Get the captcha plugin configuration. // The configuration is used to generate the captcha for frontend. Such as the token for third-party service. GetConfig() (configJsonStr string) // Create optional. If this plugin need to create captcha via backend, implement this method. // On other hand, if this plugin create captcha via third-party service, ignore this method. // Return captcha: The captcha image base64 string, code: The real captcha code. Create() (captcha, code string) // Verify required. Verify the user input captcha is correct or not // captcha: The captchaCode generated by Create method, if not implemented, it's empty. Verify(captchaCode, userInput string) (pass bool) } var ( // CallCaptcha is a function that calls all registered parsers callCaptcha, registerCaptcha = MakePlugin[Captcha](false) ) func CallCaptcha(fn func(fn Captcha) error) error { slugName := "" _ = callCaptcha(func(captcha Captcha) error { slugName = captcha.Info().SlugName return nil }) if slugName == "" { return nil } return callCaptcha(func(captcha Captcha) error { if captcha.Info().SlugName == slugName { return fn(captcha) } return nil }) } func CaptchaEnabled() (enabled bool) { _ = callCaptcha(func(fn Captcha) error { enabled = true return nil }) return } func coordinatedCaptchaPlugins(slugName string) (enabledSlugNames []string) { isCaptcha := false _ = callCaptcha(func(captcha Captcha) error { name := captcha.Info().SlugName if slugName == name { isCaptcha = true } else { enabledSlugNames = append(enabledSlugNames, name) } return nil }) if isCaptcha { return enabledSlugNames } return nil } ================================================ FILE: plugin/cdn.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin var ( DefaultCDNFileType = map[string]bool{ ".ico": true, ".json": true, ".css": true, ".js": true, ".webp": true, ".woff2": true, ".woff": true, ".jpg": true, ".svg": true, ".png": true, ".map": true, ".txt": true, } ) type CDN interface { Base GetStaticPrefix() string } var ( // CallCDN is a function that calls all registered parsers CallCDN, registerCDN = MakePlugin[CDN](false) ) func coordinatedCDNPlugins(slugName string) (enabledSlugNames []string) { isCDN := false _ = CallCDN(func(cdn CDN) error { name := cdn.Info().SlugName if slugName == name { isCDN = true } else { enabledSlugNames = append(enabledSlugNames, name) } return nil }) if isCDN { return enabledSlugNames } return nil } ================================================ FILE: plugin/config.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin type ConfigType string type InputType string const ( ConfigTypeInput ConfigType = "input" ConfigTypeTextarea ConfigType = "textarea" ConfigTypeCheckbox ConfigType = "checkbox" ConfigTypeRadio ConfigType = "radio" ConfigTypeSelect ConfigType = "select" ConfigTypeUpload ConfigType = "upload" ConfigTypeTimezone ConfigType = "timezone" ConfigTypeSwitch ConfigType = "switch" ConfigTypeButton ConfigType = "button" ConfigTypeLegend ConfigType = "legend" ConfigTypeTagSelector ConfigType = "tag_selector" ) const ( InputTypeText InputType = "text" InputTypeColor InputType = "color" InputTypeDate InputType = "date" InputTypeDatetime InputType = "datetime-local" InputTypeEmail InputType = "email" InputTypeMonth InputType = "month" InputTypeNumber InputType = "number" InputTypePassword InputType = "password" InputTypeRange InputType = "range" InputTypeSearch InputType = "search" InputTypeTel InputType = "tel" InputTypeTime InputType = "time" InputTypeUrl InputType = "url" InputTypeWeek InputType = "week" ) type ConfigField struct { Name string `json:"name"` Type ConfigType `json:"type"` Title Translator `json:"title"` Description Translator `json:"description"` Required bool `json:"required"` Value any `json:"value"` UIOptions ConfigFieldUIOptions `json:"ui_options"` Options []ConfigFieldOption `json:"options,omitempty"` } type ConfigFieldUIOptions struct { Placeholder Translator `json:"placeholder"` Rows string `json:"rows,omitempty"` InputType InputType `json:"input_type,omitempty"` Label Translator `json:"label"` Action *UIOptionAction `json:"action,omitempty"` Variant string `json:"variant,omitempty"` Text Translator `json:"text"` ClassName string `json:"class_name,omitempty"` FieldClassName string `json:"field_class_name,omitempty"` } type ConfigFieldOption struct { Label Translator `json:"label"` Value string `json:"value"` } type UIOptionAction struct { Url string `json:"url"` Method string `json:"method,omitempty"` Loading *LoadingAction `json:"loading,omitempty"` OnComplete *OnCompleteAction `json:"on_complete,omitempty"` } const ( LoadingActionStateNone LoadingActionType = "none" LoadingActionStatePending LoadingActionType = "pending" LoadingActionStateComplete LoadingActionType = "completed" ) type LoadingActionType string type LoadingAction struct { Text Translator `json:"text"` State LoadingActionType `json:"state"` } type OnCompleteAction struct { ToastReturnMessage bool `json:"toast_return_message"` RefreshFormConfig bool `json:"refresh_form_config"` } // TagSelectorOption represents a tag option in the tag selector config value field type TagSelectorOption struct { TagID string `json:"tag_id"` SlugName string `json:"slug_name"` DisplayName string `json:"display_name"` Recommend bool `json:"recommend"` Reserved bool `json:"reserved"` } type Config interface { Base // ConfigFields returns the list of config fields ConfigFields() []ConfigField // ConfigReceiver receives the config data, it calls when the config is saved or initialized. // We recommend to unmarshal the data to a struct, and then use the struct to do something. // The config is encoded in JSON format. // It depends on the definition of ConfigFields. ConfigReceiver(config []byte) error } var ( // CallConfig is a function that calls all registered config plugins CallConfig, registerConfig = MakePlugin[Config](true) ) ================================================ FILE: plugin/connector.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin type Connector interface { Base // ConnectorLogoSVG presents the logo in svg format ConnectorLogoSVG() string // ConnectorName presents the name of the connector // e.g. Facebook, Twitter, Instagram ConnectorName() Translator // ConnectorSlugName presents the slug name of the connector // Please use lowercase and hyphen as the separator // e.g. facebook, twitter, instagram ConnectorSlugName() string // ConnectorSender presents the sender of the connector // It handles the start endpoint of the connector // receiverURL is the whole URL of the receiver ConnectorSender(ctx *GinContext, receiverURL string) (redirectURL string) // ConnectorReceiver presents the receiver of the connector // It handles the callback endpoint of the connector, and returns the ConnectorReceiver(ctx *GinContext, receiverURL string) (userInfo ExternalLoginUserInfo, err error) } // ExternalLoginUserInfo external login user info type ExternalLoginUserInfo struct { // required. The unique user ID provided by the third-party login ExternalID string // optional. This name is used preferentially during registration DisplayName string // optional. This username is used preferentially during registration Username string // optional. If email exist will bind the existing user // IMPORTANT: The email must have been verified. If the plugin can't guarantee the email is verified, please leave it empty. Email string // optional. The avatar URL provided by the third-party login platform Avatar string // optional. The original user information provided by the third-party login platform MetaInfo string } var ( // CallConnector is a function that calls all registered connectors CallConnector, registerConnector = MakePlugin[Connector](false) ) ================================================ FILE: plugin/embed.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin import "github.com/gin-gonic/gin" type EmbedConfig struct { Platform string `json:"platform"` Enable bool `json:"enable"` } type Embed interface { Base GetEmbedConfigs(ctx *gin.Context) (embedConfigs []*EmbedConfig, err error) } var ( // CallEmbed is a function that calls all registered parsers CallEmbed, registerEmbed = MakePlugin[Embed](false) ) ================================================ FILE: plugin/filter.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin type Filter interface { Base FilterText(text string) (err error) } var ( // CallFilter is a function that calls all registered parsers CallFilter, registerFilter = MakePlugin[Filter](false) ) ================================================ FILE: plugin/importer.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin import ( "context" ) type QuestionImporterInfo struct { Title string `json:"title"` Content string `json:"content"` Tags []string `json:"tags"` UserEmail string `json:"user_email"` } type Importer interface { Base RegisterImporterFunc(ctx context.Context, importer ImporterFunc) } type ImporterFunc interface { AddQuestion(ctx context.Context, questionInfo QuestionImporterInfo) (err error) } var ( // CallImporter is a function that calls all registered parsers CallImporter, registerImporter = MakePlugin[Importer](false) ) func ImporterEnabled() (enabled bool) { _ = CallImporter(func(fn Importer) error { enabled = true return nil }) return } func GetImporter() (ip Importer, ok bool) { _ = CallImporter(func(fn Importer) error { ip = fn ok = true return nil }) return } ================================================ FILE: plugin/kv_storage.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin import ( "context" "fmt" "math/rand/v2" "time" "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/cache" "github.com/segmentfault/pacman/log" "xorm.io/builder" "xorm.io/xorm" ) // Error variables for KV storage operations var ( // ErrKVKeyNotFound is returned when the requested key does not exist in the KV storage ErrKVKeyNotFound = fmt.Errorf("key not found in KV storage") // ErrKVGroupEmpty is returned when a required group name is empty ErrKVGroupEmpty = fmt.Errorf("group name is empty") // ErrKVKeyEmpty is returned when a required key name is empty ErrKVKeyEmpty = fmt.Errorf("key name is empty") // ErrKVKeyAndGroupEmpty is returned when both key and group names are empty ErrKVKeyAndGroupEmpty = fmt.Errorf("both key and group are empty") // ErrKVTransactionFailed is returned when a KV storage transaction operation fails ErrKVTransactionFailed = fmt.Errorf("KV storage transaction failed") ) // KVParams is the parameters for KV storage operations type KVParams struct { Group string Key string Value string Page int PageSize int } // KVOperator provides methods to interact with the key-value storage system for plugins type KVOperator struct { data *Data session *xorm.Session pluginSlugName string cacheTTL time.Duration } // KVStorageOption defines a function type that configures a KVOperator type KVStorageOption func(*KVOperator) // WithCacheTTL is the option to set the cache TTL; the default value is 30 minutes. // If ttl is less than 0, the cache will not be used func WithCacheTTL(ttl time.Duration) KVStorageOption { return func(kv *KVOperator) { kv.cacheTTL = ttl } } // Option is used to set the options for the KV storage func (kv *KVOperator) Option(opts ...KVStorageOption) { for _, opt := range opts { opt(kv) } } func (kv *KVOperator) getSession(ctx context.Context) (*xorm.Session, func()) { session := kv.session cleanup := func() {} if session == nil { session = kv.data.DB.NewSession().Context(ctx) cleanup = func() { if session != nil { _ = session.Close() } } } return session, cleanup } func (kv *KVOperator) getCacheKey(params KVParams) string { return fmt.Sprintf("plugin_kv_storage:%s:group:%s:key:%s", kv.pluginSlugName, params.Group, params.Key) } func (kv *KVOperator) setCache(ctx context.Context, params KVParams) { if kv.cacheTTL < 0 { return } ttl := kv.cacheTTL if ttl > 10 { ttl += time.Duration(float64(ttl) * 0.1 * (1 - rand.Float64())) } cacheKey := kv.getCacheKey(params) if err := kv.data.Cache.SetString(ctx, cacheKey, params.Value, ttl); err != nil { log.Warnf("cache set failed: %v, key: %s", err, cacheKey) } } func (kv *KVOperator) getCache(ctx context.Context, params KVParams) (string, bool, error) { if kv.cacheTTL < 0 { return "", false, nil } cacheKey := kv.getCacheKey(params) return kv.data.Cache.GetString(ctx, cacheKey) } func (kv *KVOperator) cleanCache(ctx context.Context, params KVParams) { if kv.cacheTTL < 0 { return } if err := kv.data.Cache.Del(ctx, kv.getCacheKey(params)); err != nil { log.Warnf("Failed to delete cache for key %s: %v", params.Key, err) } } // Get retrieves a value from KV storage by group and key. // Returns the value as a string or an error if the key is not found. func (kv *KVOperator) Get(ctx context.Context, params KVParams) (string, error) { if params.Key == "" { return "", ErrKVKeyEmpty } if value, exist, err := kv.getCache(ctx, params); err == nil && exist { return value, nil } // query data := entity.PluginKVStorage{} query, cleanup := kv.getSession(ctx) defer cleanup() query.Where(builder.Eq{ "plugin_slug_name": kv.pluginSlugName, "`group`": params.Group, "`key`": params.Key, }) has, err := query.Get(&data) if err != nil { return "", err } if !has { return "", ErrKVKeyNotFound } params.Value = data.Value kv.setCache(ctx, params) return data.Value, nil } // Set stores a value in KV storage with the specified group and key. // Updates the value if it already exists. func (kv *KVOperator) Set(ctx context.Context, params KVParams) error { if params.Key == "" { return ErrKVKeyEmpty } query, cleanup := kv.getSession(ctx) defer cleanup() data := &entity.PluginKVStorage{ PluginSlugName: kv.pluginSlugName, Group: params.Group, Key: params.Key, Value: params.Value, } kv.cleanCache(ctx, params) affected, err := query.Where(builder.Eq{ "plugin_slug_name": kv.pluginSlugName, "`group`": params.Group, "`key`": params.Key, }).Cols("value").Update(data) if err != nil { return err } if affected == 0 { _, err = query.Insert(data) if err != nil { return err } } return nil } // Del removes values from KV storage by group and/or key. // If both group and key are provided, only that specific entry is deleted. // If only group is provided, all entries in that group are deleted. // At least one of group or key must be provided. func (kv *KVOperator) Del(ctx context.Context, params KVParams) error { if params.Key == "" && params.Group == "" { return ErrKVKeyAndGroupEmpty } kv.cleanCache(ctx, params) session, cleanup := kv.getSession(ctx) defer cleanup() session.Where(builder.Eq{ "plugin_slug_name": kv.pluginSlugName, }) if params.Group != "" { session.Where(builder.Eq{"`group`": params.Group}) } if params.Key != "" { session.Where(builder.Eq{"`key`": params.Key}) } _, err := session.Delete(&entity.PluginKVStorage{}) return err } // GetByGroup retrieves all key-value pairs for a specific group with pagination support. // Returns a map of keys to values or an error if the group is empty or not found. func (kv *KVOperator) GetByGroup(ctx context.Context, params KVParams) (map[string]string, error) { if params.Group == "" { return nil, ErrKVGroupEmpty } if params.Page < 1 { params.Page = 1 } if params.PageSize < 1 { params.PageSize = 10 } query, cleanup := kv.getSession(ctx) defer cleanup() var items []entity.PluginKVStorage err := query.Where(builder.Eq{"plugin_slug_name": kv.pluginSlugName, "`group`": params.Group}). Limit(params.PageSize, (params.Page-1)*params.PageSize). OrderBy("id ASC"). Find(&items) if err != nil { return nil, err } result := make(map[string]string, len(items)) for _, item := range items { result[item.Key] = item.Value } return result, nil } // Tx executes a function within a transaction context. If the KVOperator already has a session, // it will use that session. Otherwise, it creates a new transaction session. // The transaction will be committed if the function returns nil, or rolled back if it returns an error. func (kv *KVOperator) Tx(ctx context.Context, fn func(ctx context.Context, kv *KVOperator) error) error { var ( txKv = kv shouldCommit bool ) if kv.session == nil { session := kv.data.DB.NewSession().Context(ctx) if err := session.Begin(); err != nil { _ = session.Close() return fmt.Errorf("%w: begin transaction failed: %v", ErrKVTransactionFailed, err) } defer func() { if !shouldCommit { if rollbackErr := session.Rollback(); rollbackErr != nil { log.Errorf("rollback failed: %v", rollbackErr) } } _ = session.Close() }() txKv = &KVOperator{ session: session, data: kv.data, pluginSlugName: kv.pluginSlugName, } shouldCommit = true } if err := fn(ctx, txKv); err != nil { return fmt.Errorf("%w: %v", ErrKVTransactionFailed, err) } if shouldCommit { if err := txKv.session.Commit(); err != nil { return fmt.Errorf("%w: commit failed: %v", ErrKVTransactionFailed, err) } } return nil } // KVStorage defines the interface for plugins that need data storage capabilities type KVStorage interface { Info() Info SetOperator(operator *KVOperator) } var ( CallKVStorage, registerKVStorage = MakePlugin[KVStorage](true) ) // NewKVOperator creates a new KV storage operator with the specified database engine, cache and plugin name. // It returns a KVOperator instance that can be used to interact with the plugin's storage. func NewKVOperator(db *xorm.Engine, cache cache.Cache, pluginSlugName string) *KVOperator { return &KVOperator{ data: &Data{DB: db, Cache: cache}, pluginSlugName: pluginSlugName, cacheTTL: 30 * time.Minute, } } ================================================ FILE: plugin/notification.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin // NotificationType is the type of the notification type NotificationType string const ( NotificationUpdateQuestion NotificationType = "notification.action.update_question" NotificationAnswerTheQuestion NotificationType = "notification.action.answer_the_question" NotificationUpVotedTheQuestion NotificationType = "notification.action.up_voted_question" NotificationDownVotedTheQuestion NotificationType = "notification.action.down_voted_question" NotificationUpdateAnswer NotificationType = "notification.action.update_answer" NotificationAcceptAnswer NotificationType = "notification.action.accept_answer" NotificationUpVotedTheAnswer NotificationType = "notification.action.up_voted_answer" NotificationDownVotedTheAnswer NotificationType = "notification.action.down_voted_answer" NotificationCommentQuestion NotificationType = "notification.action.comment_question" NotificationCommentAnswer NotificationType = "notification.action.comment_answer" NotificationUpVotedTheComment NotificationType = "notification.action.up_voted_comment" NotificationReplyToYou NotificationType = "notification.action.reply_to_you" NotificationMentionYou NotificationType = "notification.action.mention_you" NotificationYourQuestionIsClosed NotificationType = "notification.action.your_question_is_closed" NotificationYourQuestionWasDeleted NotificationType = "notification.action.your_question_was_deleted" NotificationYourAnswerWasDeleted NotificationType = "notification.action.your_answer_was_deleted" NotificationYourCommentWasDeleted NotificationType = "notification.action.your_comment_was_deleted" NotificationInvitedYouToAnswer NotificationType = "notification.action.invited_you_to_answer" NotificationNewQuestion NotificationType = "notification.action.new_question" NotificationNewQuestionFollowedTag NotificationType = "notification.action.new_question_followed_tag" ) type Notification interface { Base // GetNewQuestionSubscribers returns the subscribers of the new question notification GetNewQuestionSubscribers() (userIDs []string) // Notify sends a notification to the user Notify(msg NotificationMessage) } type NotificationMessage struct { // the type of the notification Type NotificationType `json:"notification_type"` // the receiver user id ReceiverUserID string `json:"receiver_user_id"` // the receiver user using language ReceiverLang string `json:"receiver_lang"` // the receiver user external id (optional) ReceiverExternalID string `json:"receiver_external_id"` // Who triggered the notification (optional, admin or system operation will not have this field) TriggerUserID string `json:"trigger_user_id"` // The trigger user's display name (optional, admin or system operation will not have this field) TriggerUserDisplayName string `json:"trigger_user_display_name"` // The trigger user's url (optional, admin or system operation will not have this field) TriggerUserUrl string `json:"trigger_user_url"` // the question title QuestionTitle string `json:"question_title"` // the question url QuestionUrl string `json:"question_url"` // the question tags (comma separated, optional, only for new question notification) QuestionTags string `json:"tags"` // the answer url (optional, only for new answer notification) AnswerUrl string `json:"answer_url"` // the comment url (optional, only for new comment notification) CommentUrl string `json:"comment_url"` } var ( // CallNotification is a function that calls all registered notification plugins CallNotification, registerNotification = MakePlugin[Notification](false) ) ================================================ FILE: plugin/parser.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin type Parser interface { Base Parse(text string) (string, error) } var ( // CallParser is a function that calls all registered parsers CallParser, registerParser = MakePlugin[Parser](false) ) ================================================ FILE: plugin/plugin.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin import ( "encoding/json" "sync" "github.com/segmentfault/pacman/cache" "github.com/segmentfault/pacman/i18n" "xorm.io/xorm" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" "github.com/gin-gonic/gin" ) // Data is defined here to avoid circular dependency with internal/base/data type Data struct { DB *xorm.Engine Cache cache.Cache } // GinContext is a wrapper of gin.Context // We export it to make it easy to use in plugins type GinContext = gin.Context // StatusManager is a manager that manages the status of plugins // Init Plugins: // json.Unmarshal([]byte(`{"plugin1": true, "plugin2": false}`), &plugin.StatusManager) // Dump Status: // json.Marshal(plugin.StatusManager) var StatusManager = statusManager{ status: make(map[string]bool), } // Register registers a plugin func Register(p Base) { registerBase(p) if _, ok := p.(Config); ok { registerConfig(p.(Config)) } if _, ok := p.(UserConfig); ok { registerUserConfig(p.(UserConfig)) } if _, ok := p.(Connector); ok { registerConnector(p.(Connector)) } if _, ok := p.(Parser); ok { registerParser(p.(Parser)) } if _, ok := p.(Filter); ok { registerFilter(p.(Filter)) } if _, ok := p.(Storage); ok { registerStorage(p.(Storage)) } if _, ok := p.(Cache); ok { registerCache(p.(Cache)) } if _, ok := p.(UserCenter); ok { registerUserCenter(p.(UserCenter)) } if _, ok := p.(Agent); ok { registerAgent(p.(Agent)) } if _, ok := p.(Search); ok { registerSearch(p.(Search)) } if _, ok := p.(Notification); ok { registerNotification(p.(Notification)) } if _, ok := p.(Reviewer); ok { registerReviewer(p.(Reviewer)) } if _, ok := p.(Captcha); ok { registerCaptcha(p.(Captcha)) } if _, ok := p.(Embed); ok { registerEmbed(p.(Embed)) } if _, ok := p.(Render); ok { registerRender(p.(Render)) } if _, ok := p.(CDN); ok { registerCDN(p.(CDN)) } if _, ok := p.(Importer); ok { registerImporter(p.(Importer)) } if _, ok := p.(KVStorage); ok { registerKVStorage(p.(KVStorage)) } if _, ok := p.(Sidebar); ok { registerSidebar(p.(Sidebar)) } } type Stack[T Base] struct { plugins []T } type RegisterFn[T Base] func(p T) type Caller[T Base] func(p T) error type CallFn[T Base] func(fn Caller[T]) error // MakePlugin creates a plugin caller and register stack manager // The parameter super presents if the plugin can be disabled. // It returns a register function and a caller function // The register function is used to register a plugin, it will be called in the plugin's init function // The caller function is used to call all registered plugins func MakePlugin[T Base](super bool) (CallFn[T], RegisterFn[T]) { stack := Stack[T]{} call := func(fn Caller[T]) error { for _, p := range stack.plugins { // If the plugin is disabled, skip it if !super && !StatusManager.IsEnabled(p.Info().SlugName) { continue } if err := fn(p); err != nil { return err } } return nil } register := func(p T) { for _, plugin := range stack.plugins { if plugin.Info().SlugName == p.Info().SlugName { panic("plugin " + p.Info().SlugName + " is already registered") } } stack.plugins = append(stack.plugins, p) } return call, register } type statusManager struct { lock sync.Mutex status map[string]bool } func (m *statusManager) Enable(name string, enabled bool) { m.lock.Lock() defer m.lock.Unlock() if !enabled { m.status[name] = enabled return } m.status[name] = enabled for _, slugName := range coordinatedCaptchaPlugins(name) { m.status[slugName] = false } for _, slugName := range coordinatedCDNPlugins(name) { m.status[slugName] = false } } func (m *statusManager) IsEnabled(name string) bool { if status, ok := m.status[name]; ok { return status } return false } // MarshalJSON implements the json.Marshaler interface. func (m *statusManager) MarshalJSON() ([]byte, error) { return json.Marshal(m.status) } // UnmarshalJSON implements the json.Unmarshaler interface. func (m *statusManager) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &m.status) } // Translate translates the key to the current language of the context func Translate(ctx *GinContext, key string) string { return translator.Tr(handler.GetLangByCtx(ctx), key) } // TranslateWithData translates the key to the language with data func TranslateWithData(lang i18n.Language, key string, data any) string { return translator.TrWithData(lang, key, data) } // TranslateFn presents a generator of translated string. // We use it to delegate the translation work outside the plugin. type TranslateFn func(ctx *GinContext) string // Translator contains a function that translates the key to the current language of the context type Translator struct { Fn TranslateFn } // MakeTranslator generates a translator from the key func MakeTranslator(key string) Translator { t := func(ctx *GinContext) string { return Translate(ctx, key) } return Translator{Fn: t} } // Translate translates the key to the current language of the context func (t Translator) Translate(ctx *GinContext) string { if t.Fn == nil { return "" } return t.Fn(ctx) } ================================================ FILE: plugin/plugin_test/kv_storage_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin_test import ( "context" "fmt" "math/rand" "sync" "testing" "time" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/log" _ "modernc.org/sqlite" ) var ( testPlugin *TestKVStoragePlugin ) // Helper functions for testing func mustSet(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key, value string) { if err := kv.Set(ctx, plugin.KVParams{Group: group, Key: key, Value: value}); err != nil { t.Fatalf("Failed to set %s/%s: %v", group, key, err) } } func mustGet(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key, expected string) { val, err := kv.Get(ctx, plugin.KVParams{Group: group, Key: key}) if err != nil { t.Fatalf("Failed to get %s/%s: %v", group, key, err) } if val != expected { t.Errorf("Expected '%s' for %s/%s, got '%s'", expected, group, key, val) } } func mustDel(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key string) { if err := kv.Del(ctx, plugin.KVParams{Group: group, Key: key}); err != nil { t.Fatalf("Failed to delete %s/%s: %v", group, key, err) } } func assertNotFound(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key string) { val, err := kv.Get(ctx, plugin.KVParams{Group: group, Key: key}) if err != plugin.ErrKVKeyNotFound { t.Errorf("Expected ErrKVKeyNotFound for %s/%s, got: %v", group, key, err) } if val != "" { t.Errorf("Expected empty value for %s/%s, got: '%s'", group, key, val) } } func assertError(t *testing.T, err error, expected error, msg string) { if err != expected { t.Errorf("%s: expected %v, got %v", msg, expected, err) } } // TestKVStoragePlugin implements KVStorage interface for testing type TestKVStoragePlugin struct { operator *plugin.KVOperator } // Info returns plugin information func (p *TestKVStoragePlugin) Info() plugin.Info { return plugin.Info{ Name: plugin.MakeTranslator("test_kv_storage_name"), SlugName: "test_kv_storage", Description: plugin.MakeTranslator("test_kv_storage_desc"), Author: "Answer Team", Version: "1.0.0", Link: "https://github.com/apache/answer", } } // SetOperator sets KV operator func (p *TestKVStoragePlugin) SetOperator(operator *plugin.KVOperator) { p.operator = operator } // setupTestEnvironment sets up test environment func setupTestEnvironment() { // Initialize only once if testPlugin != nil { return } // Create and register test plugin testPlugin = &TestKVStoragePlugin{} plugin.Register(testPlugin) // Enable plugin plugin.StatusManager.Enable("test_kv_storage", true) // Initialize plugin data, refer to plugin_common_service.go implementation _ = plugin.CallKVStorage(func(k plugin.KVStorage) error { k.SetOperator(plugin.NewKVOperator( testDataSource.DB, testDataSource.Cache, k.Info().SlugName, )) return nil }) } // Test basic operations including CRUD and edge cases func TestBasicOperations(t *testing.T) { setupTestEnvironment() kv := testPlugin.operator ctx := context.Background() t.Run("BasicCRUD", func(t *testing.T) { // Set/Get mustSet(t, kv, ctx, "group1", "key1", "value1") mustGet(t, kv, ctx, "group1", "key1", "value1") // Update mustSet(t, kv, ctx, "group1", "key1", "new_value") mustGet(t, kv, ctx, "group1", "key1", "new_value") // Delete mustDel(t, kv, ctx, "group1", "key1") assertNotFound(t, kv, ctx, "group1", "key1") // Group operation mustSet(t, kv, ctx, "group1", "key2", "value2") mustSet(t, kv, ctx, "group1", "key3", "value3") groupData, err := kv.GetByGroup(ctx, plugin.KVParams{Group: "group1", Page: 1, PageSize: 10}) if err != nil { t.Fatalf("Failed to get group data: %v", err) } // the groupData should only have key2 and key3 because key1 is deleted if len(groupData) != 2 { t.Errorf("Expected 2 items, got %d", len(groupData)) } if groupData["key2"] != "value2" || groupData["key3"] != "value3" { t.Errorf("Unexpected group data: %v", groupData) } }) t.Run("EdgeCases", func(t *testing.T) { // Empty key err := kv.Set(ctx, plugin.KVParams{Group: "group", Key: "", Value: "value"}) assertError(t, err, plugin.ErrKVKeyEmpty, "Empty key test") // Empty group query _, err = kv.GetByGroup(ctx, plugin.KVParams{Group: "", Page: 1, PageSize: 10}) assertError(t, err, plugin.ErrKVGroupEmpty, "Empty group test") // Non-existent key assertNotFound(t, kv, ctx, "non_exist_group", "non_exist_key") // Cache penetration protection key := fmt.Sprintf("non_exist_key_%d", time.Now().UnixNano()) assertNotFound(t, kv, ctx, "cache_penetration", key) }) t.Run("CacheConsistency", func(t *testing.T) { mustSet(t, kv, ctx, "cache_group", "cache_key", "cache_value") mustGet(t, kv, ctx, "cache_group", "cache_key", "cache_value") // Update and verify immediate consistency mustSet(t, kv, ctx, "cache_group", "cache_key", "updated_value") mustGet(t, kv, ctx, "cache_group", "cache_key", "updated_value") }) } // Test transactions including rollback and nested transactions func TestTransactions(t *testing.T) { setupTestEnvironment() kv := testPlugin.operator ctx := context.Background() t.Run("SuccessfulTransaction", func(t *testing.T) { err := kv.Tx(ctx, func(ctx context.Context, txKv *plugin.KVOperator) error { if err := txKv.Set(ctx, plugin.KVParams{Group: "tx_group", Key: "tx_key1", Value: "tx_value1"}); err != nil { return err } if err := txKv.Set(ctx, plugin.KVParams{Group: "tx_group", Key: "tx_key2", Value: "tx_value2"}); err != nil { return err } return nil }) if err != nil { t.Fatalf("Successful transaction failed: %v", err) } mustGet(t, kv, ctx, "tx_group", "tx_key1", "tx_value1") mustGet(t, kv, ctx, "tx_group", "tx_key2", "tx_value2") }) t.Run("TransactionRollback", func(t *testing.T) { err := kv.Tx(ctx, func(ctx context.Context, txKv *plugin.KVOperator) error { if err := txKv.Set(ctx, plugin.KVParams{Group: "tx_group", Key: "tx_key3", Value: "tx_value3"}); err != nil { return err } return fmt.Errorf("mock error") }) if err == nil { t.Error("Expected transaction to fail but it succeeded") } assertNotFound(t, kv, ctx, "tx_group", "tx_key3") }) t.Run("NestedTransactions", func(t *testing.T) { err := kv.Tx(ctx, func(ctx context.Context, txKv *plugin.KVOperator) error { if err := txKv.Set(ctx, plugin.KVParams{Group: "nested", Key: "key1", Value: "value1"}); err != nil { return err } return txKv.Tx(ctx, func(ctx context.Context, nestedKv *plugin.KVOperator) error { if err := nestedKv.Set(ctx, plugin.KVParams{Group: "nested", Key: "key2", Value: "value2"}); err != nil { return err } return fmt.Errorf("mock nested error") }) }) if err == nil { t.Error("Expected nested transaction to fail but it succeeded") } // Verify outer transaction also rolled back assertNotFound(t, kv, ctx, "nested", "key1") assertNotFound(t, kv, ctx, "nested", "key2") }) } // Test pagination in GetByGroup func TestPagination(t *testing.T) { setupTestEnvironment() kv := testPlugin.operator ctx := context.Background() totalItems := 25 for i := range totalItems { mustSet(t, kv, ctx, "pagination", fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i)) } // Test pagination page1, err := kv.GetByGroup(ctx, plugin.KVParams{Group: "pagination", Page: 1, PageSize: 10}) if err != nil { t.Fatalf("Failed to get page 1: %v", err) } if len(page1) != 10 { t.Errorf("Page 1: expected 10 items, got %d", len(page1)) } page2, err := kv.GetByGroup(ctx, plugin.KVParams{Group: "pagination", Page: 2, PageSize: 10}) if err != nil { t.Fatalf("Failed to get page 2: %v", err) } if len(page2) != 10 { t.Errorf("Page 2: expected 10 items, got %d", len(page2)) } page3, err := kv.GetByGroup(ctx, plugin.KVParams{Group: "pagination", Page: 3, PageSize: 10}) if err != nil { t.Fatalf("Failed to get page 3: %v", err) } if len(page3) != 5 { t.Errorf("Page 3: expected 5 items, got %d", len(page3)) } // Verify different keys on different pages for i := range 10 { key := fmt.Sprintf("key%d", i) if _, ok := page1[key]; !ok { t.Errorf("Pagination test failed, key %s should be on page 1", key) } } for i := range 10 { key := fmt.Sprintf("key%d", i+10) if _, ok := page2[key]; !ok { t.Errorf("Pagination test failed, key %s should be on page 2", key) } } } // Test concurrent operations and performance func TestConcurrency(t *testing.T) { setupTestEnvironment() kv := testPlugin.operator ctx := context.Background() t.Run("BasicConcurrency", func(t *testing.T) { parallel := 10 var wg sync.WaitGroup wg.Add(parallel) for i := range parallel { go func(index int) { defer wg.Done() time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond) mustSet(t, kv, ctx, "concurrent", fmt.Sprintf("key%d", index), "value") }(i) } wg.Wait() // Verify results wg.Add(parallel) for i := range parallel { go func(index int) { defer wg.Done() time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond) mustGet(t, kv, ctx, "concurrent", fmt.Sprintf("key%d", index), "value") }(i) } wg.Wait() }) t.Run("StressTest", func(t *testing.T) { if testing.Short() { t.Skip("Skipping stress test in short mode") } totalOps := 1000 workerCount := 20 prefix := "stress_test" opsPerWorker := totalOps / workerCount log.Info("Starting KV storage stress test...") startTime := time.Now() // Concurrent write test var wg sync.WaitGroup errorCount := int64(0) for w := range workerCount { wg.Add(1) go func(workerID int) { defer wg.Done() startIdx := workerID * opsPerWorker for i := range opsPerWorker { i := startIdx + i err := kv.Set(ctx, plugin.KVParams{ Group: prefix, Key: fmt.Sprintf("key%d", i), Value: fmt.Sprintf("value%d", i), }) if err != nil { log.Warnf("Write error: %v", err) errorCount++ } } }(w) } wg.Wait() writeTime := time.Since(startTime) // Verify data integrity groupData, err := kv.GetByGroup(ctx, plugin.KVParams{Group: prefix, Page: 1, PageSize: totalOps}) if err != nil { t.Fatalf("Failed to verify data: %v", err) } if len(groupData) != totalOps { t.Errorf("Data loss: expected %d items, got %d", totalOps, len(groupData)) } // Concurrent read test startTime = time.Now() readErrors := int64(0) wg.Add(workerCount) for range workerCount { go func() { defer wg.Done() for range opsPerWorker { keyIdx := rand.Intn(totalOps) key := fmt.Sprintf("key%d", keyIdx) expected := fmt.Sprintf("value%d", keyIdx) val, err := kv.Get(ctx, plugin.KVParams{Group: prefix, Key: key}) if err != nil { readErrors++ } else if val != expected { t.Errorf("Data inconsistency: key=%s, expected=%s, got=%s", key, expected, val) } } }() } wg.Wait() readTime := time.Since(startTime) log.Infof("Stress test completed:") log.Infof(" Write: %d ops in %v (%.1f ops/sec), %d errors", totalOps, writeTime, float64(totalOps)/writeTime.Seconds(), errorCount) log.Infof(" Read: %d ops in %v (%.1f ops/sec), %d errors", totalOps, readTime, float64(totalOps)/readTime.Seconds(), readErrors) }) } ================================================ FILE: plugin/plugin_test/plugin_main_test.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin_test import ( "context" "database/sql" "fmt" "os" "path/filepath" "testing" "time" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/migrations" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" "github.com/segmentfault/pacman/cache" "github.com/segmentfault/pacman/log" "xorm.io/xorm" "xorm.io/xorm/schemas" ) var ( mysqlDBSetting = TestDBSetting{ Driver: string(schemas.MYSQL), ImageName: "mariadb", ImageVersion: "10.4.7", ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=answer", "MYSQL_ROOT_HOST=%"}, PortID: "3306/tcp", Connection: "root:root@(localhost:%s)/answer?parseTime=true", // port is not fixed, it will be got by port id } postgresDBSetting = TestDBSetting{ Driver: string(schemas.POSTGRES), ImageName: "postgres", ImageVersion: "14", ENV: []string{"POSTGRES_USER=root", "POSTGRES_PASSWORD=root", "POSTGRES_DB=answer", "LISTEN_ADDRESSES='*'"}, PortID: "5432/tcp", Connection: "host=localhost port=%s user=root password=root dbname=answer sslmode=disable", } sqlite3DBSetting = TestDBSetting{ Driver: string(schemas.SQLITE), Connection: filepath.Join(os.TempDir(), "answer-test-data.db"), } dbSettingMapping = map[string]TestDBSetting{ mysqlDBSetting.Driver: mysqlDBSetting, sqlite3DBSetting.Driver: sqlite3DBSetting, postgresDBSetting.Driver: postgresDBSetting, } // after all test down will execute tearDown function to clean-up tearDown func() // testDataSource used for repo testing testDataSource *data.Data testCache cache.Cache ) func TestMain(t *testing.M) { dbSetting, ok := dbSettingMapping[os.Getenv("TEST_DB_DRIVER")] if !ok { // Use sqlite3 to test. dbSetting = dbSettingMapping[string(schemas.SQLITE)] } if dbSetting.Driver == string(schemas.SQLITE) { _ = os.RemoveAll(dbSetting.Connection) } if err := initTestDataSource(dbSetting); err != nil { panic(err) } log.Info("init test database successfully") ret := t.Run() if tearDown != nil { tearDown() } os.Exit(ret) } type TestDBSetting struct { Driver string ImageName string ImageVersion string ENV []string PortID string Connection string } func initTestDataSource(dbSetting TestDBSetting) error { connection, imageCleanUp, err := initDatabaseImage(dbSetting) if err != nil { return err } dbSetting.Connection = connection dbEngine, err := initDatabase(dbSetting) if err != nil { return err } newCache, err := initCache() if err != nil { return err } newData, dbCleanUp, err := data.NewData(dbEngine, newCache) if err != nil { return err } testDataSource = newData testCache = newCache tearDown = func() { dbCleanUp() log.Info("cleanup test database successfully") imageCleanUp() log.Info("cleanup test database image successfully") } return nil } func initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func(), err error) { // sqlite3 don't need to set up image if dbSetting.Driver == string(schemas.SQLITE) { return dbSetting.Connection, func() { log.Info("remove database", dbSetting.Connection) err = os.Remove(dbSetting.Connection) if err != nil { log.Error(err) } }, nil } pool, err := dockertest.NewPool("") pool.MaxWait = time.Minute * 5 if err != nil { return "", nil, fmt.Errorf("could not connect to docker: %s", err) } // resource, err := pool.Run(dbSetting.ImageName, dbSetting.ImageVersion, dbSetting.ENV) resource, err := pool.RunWithOptions(&dockertest.RunOptions{ Repository: dbSetting.ImageName, Tag: dbSetting.ImageVersion, Env: dbSetting.ENV, }, func(config *docker.HostConfig) { config.AutoRemove = true config.RestartPolicy = docker.RestartPolicy{Name: "no"} }) if err != nil { return "", nil, fmt.Errorf("could not pull resource: %s", err) } connection = fmt.Sprintf(dbSetting.Connection, resource.GetPort(dbSetting.PortID)) if err := pool.Retry(func() error { db, err := sql.Open(dbSetting.Driver, connection) if err != nil { return err } return db.Ping() }); err != nil { return "", nil, fmt.Errorf("could not connect to database: %s", err) } return connection, func() { _ = pool.Purge(resource) }, nil } func initDatabase(dbSetting TestDBSetting) (dbEngine *xorm.Engine, err error) { dataConf := &data.Database{Driver: dbSetting.Driver, Connection: dbSetting.Connection} dbEngine, err = data.NewDB(true, dataConf) if err != nil { return nil, fmt.Errorf("connection to database failed: %s", err) } if err := migrations.NewMentor(context.TODO(), dbEngine, &migrations.InitNeedUserInputData{ Language: "en_US", SiteName: "ANSWER", SiteURL: "http://127.0.0.1:8080/", ContactEmail: "answer@answer.com", AdminName: "admin", AdminPassword: "admin", AdminEmail: "answer@answer.com", }).InitDB(); err != nil { return nil, fmt.Errorf("migrations init database failed: %s", err) } return dbEngine, nil } func initCache() (newCache cache.Cache, err error) { newCache, _, err = data.NewCache(&data.CacheConf{}) return newCache, err } ================================================ FILE: plugin/render.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin import "github.com/gin-gonic/gin" type RenderConfig struct { SelectTheme string `json:"select_theme"` } // select_theme type Render interface { Base GetRenderConfig(ctx *gin.Context) (renderConfig *RenderConfig) } var ( // CallRender is a function that calls all registered parsers CallRender, registerRender = MakePlugin[Render](false) ) ================================================ FILE: plugin/reviewer.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin type Reviewer interface { Base Review(content *ReviewContent) (result *ReviewResult) } // ReviewContent is a struct that contains the content of a review type ReviewContent struct { // The type of the content, e.g. question, answer ObjectType string // The title of the content, only available for the question Title string // The content of the review, always available Content string // The tags of the content, only available for the question Tags []string // The author of the content Author ReviewContentAuthor // Review Language, the site language. e.g. en_US // The plugin may reply the review result according to the language Language string // The user agent of the request web browser UserAgent string // The IP address of the request IP string } type ReviewContentAuthor struct { // The user's reputation Rank int // The amount of questions that has approved ApprovedQuestionAmount int64 // The amount of answers that has approved ApprovedAnswerAmount int64 // 1:User 2:Admin 3:Moderator Role int } type ReviewStatus string const ( ReviewStatusApproved ReviewStatus = "approved" ReviewStatusDeleteDirectly ReviewStatus = "delete_directly" ReviewStatusNeedReview ReviewStatus = "need_review" ) // ReviewResult is a struct that contains the result of a review type ReviewResult struct { // If the review is approved Approved bool // The status of the review ReviewStatus ReviewStatus // The reason for the result Reason string } var ( // CallReviewer is a function that calls all registered parsers CallReviewer, registerReviewer = MakePlugin[Reviewer](false) ) ================================================ FILE: plugin/search.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin import ( "context" ) type SearchResult struct { // ID content ID ID string // Type content type, example: "answer", "question" Type string } type SearchContent struct { ObjectID string `json:"objectID"` Title string `json:"title"` Type string `json:"type"` Content string `json:"content"` Answers int64 `json:"answers"` Status SearchContentStatus `json:"status"` Tags []string `json:"tags"` QuestionID string `json:"questionID"` UserID string `json:"userID"` Views int64 `json:"views"` Created int64 `json:"created"` Active int64 `json:"active"` Score int64 `json:"score"` HasAccepted bool `json:"hasAccepted"` } type SearchBasicCond struct { // From zero-based page number Page int // Page size PageSize int // The keywords for search. Words []string // TagIDs is a list of tag IDs. TagIDs [][]string // The object's owner user ID. UserID string // The order of the search result. Order SearchOrderCond // Weathers the question is accepted or not. Only support search question. QuestionAccepted SearchAcceptedCond // Weathers the answer is accepted or not. Only support search answer. AnswerAccepted SearchAcceptedCond // Only support search answer. QuestionID string // greater than or equal to the number of votes. VoteAmount int // greater than or equal to the number of views. ViewAmount int // greater than or equal to the number of answers. Only support search question. AnswerAmount int } type SearchAcceptedCond int type SearchContentStatus int type SearchOrderCond string const ( AcceptedCondAll SearchAcceptedCond = iota AcceptedCondTrue AcceptedCondFalse ) const ( SearchContentStatusAvailable = 1 SearchContentStatusDeleted = 10 ) const ( SearchNewestOrder SearchOrderCond = "newest" SearchActiveOrder SearchOrderCond = "active" SearchScoreOrder SearchOrderCond = "score" SearchRelevanceOrder SearchOrderCond = "relevance" ) type Search interface { Base Description() SearchDesc RegisterSyncer(ctx context.Context, syncer SearchSyncer) SearchContents(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error) SearchQuestions(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error) SearchAnswers(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error) UpdateContent(ctx context.Context, content *SearchContent) (err error) DeleteContent(ctx context.Context, objectID string) (err error) } type SearchDesc struct { // A svg icon it wil be display in search result page. optional Icon string `json:"icon"` // The link address of the search engine. optional Link string `json:"link"` } type SearchSyncer interface { GetAnswersPage(ctx context.Context, page, pageSize int) (answerList []*SearchContent, err error) GetQuestionsPage(ctx context.Context, page, pageSize int) (questionList []*SearchContent, err error) } var ( // CallSearch is a function that calls all registered parsers CallSearch, registerSearch = MakePlugin[Search](false) ) ================================================ FILE: plugin/sidebar.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Package plugin package plugin type SidebarConfig struct { Tags []*TagSelectorOption `json:"tags"` LinksText string `json:"links_text"` } type Sidebar interface { Base GetSidebarConfig() (sidebarConfig *SidebarConfig, err error) } var ( // CallRender is a function that calls all registered parsers CallSidebar, registerSidebar = MakePlugin[Sidebar](false) ) ================================================ FILE: plugin/storage.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin type UploadSource string const ( UserAvatar UploadSource = "user_avatar" UserPost UploadSource = "user_post" UserPostAttachment UploadSource = "user_post_attachment" AdminBranding UploadSource = "admin_branding" ) var ( DefaultFileTypeCheckMapping = map[UploadSource]map[string]bool{ UserAvatar: { ".jpg": true, ".jpeg": true, ".png": true, ".webp": true, }, UserPost: { ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, }, AdminBranding: { ".jpg": true, ".jpeg": true, ".png": true, ".ico": true, }, } ) type UploadFileCondition struct { // Source is the source of the file Source UploadSource // MaxImageSize is the maximum size of the image in MB MaxImageSize int // MaxAttachmentSize is the maximum size of the attachment in MB MaxAttachmentSize int // MaxImageMegapixel is the maximum megapixel of the image MaxImageMegapixel int // AuthorizedImageExtensions is the list of authorized image extensions AuthorizedImageExtensions []string // AuthorizedAttachmentExtensions is the list of authorized attachment extensions AuthorizedAttachmentExtensions []string } type UploadFileResponse struct { // FullURL is the URL that can be used to access the file FullURL string // OriginalError is the error returned by the storage plugin. It is used for debugging. OriginalError error // DisplayErrorMsg is the error message that will be displayed to the user. DisplayErrorMsg Translator } type Storage interface { Base // UploadFile uploads a file to storage. // The file is in the Form of the ctx and the key is "file" UploadFile(ctx *GinContext, condition UploadFileCondition) UploadFileResponse } var ( // CallStorage is a function that calls all registered storage CallStorage, registerStorage = MakePlugin[Storage](false) ) ================================================ FILE: plugin/user_center.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin type UserCenter interface { Base // Description returns the description of the user center, including the name, icon, url, etc. Description() UserCenterDesc // ControlCenterItems returns the items that will be displayed in the control center ControlCenterItems() []ControlCenter // LoginCallback is called when the user center login callback is called LoginCallback(ctx *GinContext) (userInfo *UserCenterBasicUserInfo, err error) // SignUpCallback is called when the user center sign up callback is called SignUpCallback(ctx *GinContext) (userInfo *UserCenterBasicUserInfo, err error) // UserInfo returns the user information UserInfo(externalID string) (userInfo *UserCenterBasicUserInfo, err error) // UserStatus returns the latest user status UserStatus(externalID string) (userStatus UserStatus) // UserList returns the user list information UserList(externalIDs []string) (userInfo []*UserCenterBasicUserInfo, err error) // UserSettings returns the user settings UserSettings(externalID string) (userSettings *SettingInfo, err error) // PersonalBranding returns the personal branding information PersonalBranding(externalID string) (branding []*PersonalBranding) // AfterLogin is called after the user logs in AfterLogin(externalID, accessToken string) } type UserCenterDesc struct { Name string `json:"name"` DisplayName Translator `json:"display_name"` Icon string `json:"icon"` Url string `json:"url"` LoginRedirectURL string `json:"login_redirect_url"` SignUpRedirectURL string `json:"sign_up_redirect_url"` RankAgentEnabled bool `json:"rank_agent_enabled"` UserStatusAgentEnabled bool `json:"user_status_agent_enabled"` UserRoleAgentEnabled bool `json:"user_role_agent_enabled"` MustAuthEmailEnabled bool `json:"must_auth_email_enabled"` EnabledOriginalUserSystem bool `json:"enabled_original_user_system"` } type UserStatus int const ( UserStatusAvailable UserStatus = 1 UserStatusSuspended UserStatus = 9 UserStatusDeleted UserStatus = 10 ) type UserCenterBasicUserInfo struct { ExternalID string `json:"external_id"` Username string `json:"username"` DisplayName string `json:"display_name"` Email string `json:"email"` Rank int `json:"rank"` Avatar string `json:"avatar"` Mobile string `json:"mobile"` Bio string `json:"bio"` Status UserStatus `json:"status"` } type ControlCenter struct { Name string `json:"name"` Label string `json:"label"` Url string `json:"url"` } type SettingInfo struct { ProfileSettingRedirectURL string `json:"profile_setting_redirect_url"` AccountSettingRedirectURL string `json:"account_setting_redirect_url"` } type PersonalBranding struct { Icon string `json:"icon"` Name string `json:"name"` Label string `json:"label"` Url string `json:"url"` } var ( // CallUserCenter is a function that calls all registered parsers CallUserCenter, registerUserCenter = MakePlugin[UserCenter](false) ) func UserCenterEnabled() (enabled bool) { _ = CallUserCenter(func(fn UserCenter) error { enabled = true return nil }) return } func RankAgentEnabled() (enabled bool) { _ = CallUserCenter(func(fn UserCenter) error { enabled = fn.Description().RankAgentEnabled return nil }) return } func GetUserCenter() (uc UserCenter, ok bool) { _ = CallUserCenter(func(fn UserCenter) error { uc = fn ok = true return nil }) return } ================================================ FILE: plugin/user_config.go ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin type UserConfig interface { Base // UserConfigFields returns the list of config fields UserConfigFields() []ConfigField // UserConfigReceiver receives the config data, it calls when the config is saved or initialized. // We recommend to unmarshal the data to a struct, and then use the struct to do something. // The config is encoded in JSON format. // It depends on the definition of ConfigFields. UserConfigReceiver(userID string, config []byte) error } var ( // CallUserConfig is a function that calls all registered config plugins CallUserConfig, registerUserConfig = MakePlugin[UserConfig](false) getPluginUserConfigFn func(userID, pluginSlugName string) []byte ) // GetPluginUserConfig returns the user config of the given user id func GetPluginUserConfig(userID, pluginSlugName string) []byte { if getPluginUserConfigFn != nil { return getPluginUserConfigFn(userID, pluginSlugName) } return nil } // RegisterGetPluginUserConfigFunc registers a function to get the user config of the given user id func RegisterGetPluginUserConfigFunc(fn func(userID, pluginSlugName string) []byte) { getPluginUserConfigFn = fn } ================================================ FILE: script/build_plugin.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. set -e echo "begin build plugin" plugin_file=./script/plugin_list if [ ! -f "$plugin_file" ]; then echo "plugin_list is not exist" exit 0 fi echo "plugin_list exist" cmd="./answer build " for repo in `cat $plugin_file` do echo ${repo} cmd=$cmd" --with "${repo} done echo "cmd is "$cmd $cmd if [ ! -f "./new_answer" ]; then echo "new_answer is not exist build failed" exit 1 fi rm answer mv new_answer answer ./answer plugin ================================================ FILE: script/check-asf-header.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # check if docker or podman is installed if command -v docker >/dev/null 2>&1; then CONTAINER_RUNTIME="docker" elif command -v podman >/dev/null 2>&1; then CONTAINER_RUNTIME="podman" else echo "Neither Docker nor Podman is installed. Please install either Docker or Podman." exit 1 fi $CONTAINER_RUNTIME run --rm -v "$(pwd)":/github/workspace ghcr.io/korandoru/hawkeye-native format gofmt -w -l . ================================================ FILE: script/entrypoint.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. /usr/bin/answer init /usr/bin/answer upgrade /usr/bin/answer run -C /data/ ================================================ FILE: script/gen-api.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. cd ../ swag init --generalInfo ./cmd/answer/main.go ================================================ FILE: script/plugin_list ================================================ github.com/apache/answer-plugins/connector-basic@latest github.com/apache/answer-plugins/reviewer-basic@latest github.com/apache/answer-plugins/captcha-basic@latest github.com/apache/answer-plugins/quick-links@latest ================================================ FILE: ui/.browserslistrc ================================================ [production] > 20% not dead not op_mini all [development] last 1 chrome version last 1 firefox version last 1 safari version [ssr] node 16 ================================================ FILE: ui/.editorconfig ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true max_line_length = 80 [*.md] trim_trailing_whitespace = false [Makefile] indent_style = tab ================================================ FILE: ui/.eslintignore ================================================ public config-overrides.js commitlint.config.js build .eslintrc.js node_modules/ src/types/ scripts/ src/plugins/** !src/plugins/builtin ================================================ FILE: ui/.eslintrc.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ module.exports = { root: true, env: { browser: true, es2021: true, }, extends: [ 'react-app/jest', 'plugin:react/recommended', 'airbnb', 'airbnb-typescript', 'plugin:import/typescript', 'plugin:prettier/recommended', ], overrides: [], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 'latest', sourceType: 'module', tsconfigRootDir: __dirname, project: ['./tsconfig.json'], }, plugins: ['react', '@typescript-eslint', 'prettier'], rules: { 'prettier/prettier': 'error', 'no-unused-vars': 'off', 'no-console': 'off', 'import/prefer-default-export': 'off', 'no-param-reassign': 'off', 'react/react-in-jsx-scope': 'off', 'react/function-component-definition': 'off', 'react/button-has-type': 'off', 'react/no-unescaped-entities': 'off', 'react/require-default-props': 'off', 'arrow-body-style': 'off', "global-require": "off", 'react/prop-types': 0, 'react/no-danger': 'off', 'jsx-a11y/no-static-element-interactions': 'off', 'jsx-a11y/label-has-associated-control': 'off', 'jsx-a11y/tabindex-no-positive': 'off', 'jsx-a11y/control-has-associated-label': 'off', 'func-names': 'off', 'no-alert': 'off', 'prefer-promise-reject-errors': 'off', '@typescript-eslint/naming-convention': 'off', 'no-debugger': 'off', 'max-len': 'off', 'import/extensions': 'off', 'react-hooks/exhaustive-deps': 'off', 'react/jsx-props-no-spreading': 'off', '@typescript-eslint/default-param-last': 'off', 'no-nested-ternary': 'off', 'class-methods-use-this': 'off', 'import/order': [ 'error', { groups: [ 'builtin', 'external', ['internal', 'parent', 'sibling', 'index'], 'unknown', ], pathGroups: [ { pattern: 'react*', group: 'external', position: 'before', }, { pattern: '@/**', group: 'internal', }, { pattern: './**', group: 'internal', position: 'after', }, { pattern: '*.scss', patternOptions: { matchBase: true }, group: 'unknown', position: 'after', }, ], pathGroupsExcludedImportTypes: ['react'], 'newlines-between': 'always', }, ], 'jsx-a11y/click-events-have-key-events': 'off', 'jsx-a11y/no-noninteractive-tabindex': 'off', }, }; ================================================ FILE: ui/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies node_modules /.pnp .pnp.js # testing /coverage # production /build/*/*/* /build/*.json /build/*.html /build/*.txt # misc .DS_Store .env*.local npm-debug.log* yarn-debug.log* yarn-error.log* Thumbs.db .idea/ *.sublime-project *.sublime-workspace *.log yarn.lock package-lock.json .eslintcache /.vscode/ /* !/src/plugins /src/plugins/* !/src/plugins/builtin !/src/plugins/Demo /src/plugins/*/*.go ================================================ FILE: ui/.lintstagedrc.json ================================================ { "src/**/*.{ts,tsx}": [ "eslint --fix", "prettier --write" ], "src/**/*.{scss,md}": [ "prettier --write" ] } ================================================ FILE: ui/.npmrc ================================================ strict-peer-dependencies = true auto-install-peers = true ================================================ FILE: ui/.prettierrc.json ================================================ { "trailingComma": "all", "tabWidth": 2, "singleQuote": true, "jsxBracketSameLine": true, "printWidth": 80, "endOfLine": "auto", "bracketSameLine": true } ================================================ FILE: ui/commitlint.config.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ module.exports = { extends: ['@commitlint/config-conventional'], }; ================================================ FILE: ui/config-overrides.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const { addWebpackModuleRule, addWebpackAlias, setWebpackOptimizationSplitChunks, addWebpackPlugin, } = require("customize-cra"); const webpack = require('webpack'); const path = require("path"); const i18nPath = path.resolve(__dirname, "../i18n"); module.exports = { webpack: function(config, env) { addWebpackAlias({ "@": path.resolve(__dirname, "src"), "@i18n": i18nPath, buffer: 'buffer', })(config); addWebpackModuleRule({ test: /\.ya?ml$/, use: "yaml-loader" })(config); addWebpackPlugin( new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'], }) )(config); setWebpackOptimizationSplitChunks({ maxInitialRequests: 20, minSize: 20 * 1024, minChunks: 2, cacheGroups: { automaticNamePrefix: 'chunk', mix1: { test: (module, chunks) => { return ( module.resource && (module.resource.includes('components') || /\/node_modules\/react-bootstrap\//.test(module.resource)) ); }, name: 'chunk-mix1', filename: 'static/js/[name].[contenthash:8].chunk.js', priority: 14, reuseExistingChunk: true, minChunks: process.env.NODE_ENV === 'production' ? 1 : 2, chunks: 'initial', }, mix2: { name: 'chunk-mix2', test: /[\/]node_modules[\/](i18next|lodash|marked|next-share)[\/]/, filename: 'static/js/[name].[contenthash:8].chunk.js', priority: 13, reuseExistingChunk: true, minChunks: 1, chunks: 'initial', }, mix3: { name: 'chunk-mix3', test: /[\/]node_modules[\/](@remix-run|@restart|axios|diff)[\/]/, filename: 'static/js/[name].[contenthash:8].chunk.js', priority: 12, reuseExistingChunk: true, minChunks: 1, chunks: 'initial', }, codemirror: { name: 'codemirror', test: /[\/]node_modules[\/](\@codemirror)[\/]/, priority: 10, reuseExistingChunk: true, minChunks: process.env.NODE_ENV === 'production' ? 1 : 2, chunks: 'initial', enforce: true, }, lezer: { name: 'lezer', test: /[\/]node_modules[\/](\@lezer)[\/]/, priority: 9, reuseExistingChunk: true, minChunks: process.env.NODE_ENV === 'production' ? 1 : 2, chunks: 'initial', enforce: true, }, reactDom: { name: 'react-dom', test: /[\/]node_modules[\/](react-dom)[\/]/, filename: 'static/js/[name].[contenthash:8].chunk.js', priority: 8, reuseExistingChunk: true, chunks: 'all', enforce: true, }, nodesInitial: { name: 'chunk-nodesInitial', filename: 'static/js/[name].[contenthash:8].chunk.js', test: /[\/]node_modules[\/]/, priority: 1, minChunks: 1, chunks: 'initial', reuseExistingChunk: true, }, }, })(config); // add i18n dir to ModuleScopePlugin allowedPaths const moduleScopePlugin = config.resolve.plugins.find(_ => _.constructor.name === "ModuleScopePlugin"); if (moduleScopePlugin) { moduleScopePlugin.allowedPaths.push(i18nPath); } return config; }, devServer: function(configFunction) { return function(proxy, allowedHost) { const config = configFunction(proxy, allowedHost); config.proxy = [ { context: ['/answer', '/installation'], target: process.env.REACT_APP_API_URL, changeOrigin: true, secure: false, }, { context: ['/custom.css'], target: process.env.REACT_APP_API_URL, } ]; return config; }; } }; ================================================ FILE: ui/package.json ================================================ { "name": "answer-static", "version": "0.1.0", "private": true, "homepage": "/", "scripts": { "start": "react-app-rewired start", "build": "node ./scripts/env.js && react-app-rewired build", "pre-install": "node ./scripts/importPlugins.js && pnpm install && node ./scripts/preinstall.js ", "prepare": "pnpm build:packages", "build:packages": "pnpm -r --filter=./src/plugins/* run build", "clean": "rm -rf node_modules && rm -rf src/plugins/**/node_modules", "analyze": "source-map-explorer 'build/static/js/*.js'", "setup-lint": "node scripts/setup-eslint.js && cd .. && husky install", "lint": "eslint . --cache --fix --ext .ts,.tsx", "prettier": "prettier --write \"src/**/*.{ts,tsx,css,scss,md}\"", "lint-staged": "lint-staged" }, "dependencies": { "@codemirror/lang-markdown": "^6.2.4", "@codemirror/language-data": "^6.5.0", "@codemirror/state": "^6.5.0", "@codemirror/view": "^6.26.1", "axios": "^1.7.7", "bootstrap": "^5.3.2", "bootstrap-icons": "^1.10.5", "classnames": "^2.3.1", "codemirror": "^6.0.1", "color": "^4.2.3", "copy-to-clipboard": "^3.3.2", "dayjs": "^1.11.5", "diff": "^5.1.0", "front-matter": "^4.0.2", "i18next": "^21.9.0", "js-sha256": "0.11.0", "lodash": "^4.17.21", "marked": "^4.0.19", "next-share": "^0.18.1", "qrcode": "^1.5.1", "qs": "^6.11.0", "react": "^18.2.0", "react-bootstrap": "^2.10.0", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", "react-i18next": "^11.18.3", "react-router-dom": "^7.0.2", "semver": "^7.3.8", "swr": "^1.3.0", "uuid": "13.0.0", "zustand": "^5.0.2" }, "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.2.0", "@fullhuman/postcss-purgecss": "^4.1.3", "@testing-library/dom": "^8.17.1", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^13.5.0", "@types/color": "^3.0.3", "@types/dompurify": "^2.4.0", "@types/jest": "^27.5.2", "@types/lodash": "^4.14.184", "@types/marked": "^4.0.6", "@types/node": "^16.11.47", "@types/qs": "^6.9.7", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", "@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/parser": "^6.11.0", "buffer": "6.0.3", "customize-cra": "^1.0.0", "eslint": "^8.53.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-prettier": "^9.0.0", "eslint-config-standard-with-typescript": "^39.1.1", "eslint-plugin-import": "^2.25.2", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "husky": "^9.1.7", "js-yaml": "^4.1.0", "lint-staged": "^15.5.0", "postcss": "^8.0.0", "prettier": "^3.1.0", "purgecss-webpack-plugin": "^4.1.3", "react-app-rewired": "^2.2.1", "react-scripts": "5.0.1", "sass": "1.54.4", "source-map-explorer": "^2.5.3", "typescript": "^4.9.5", "yaml-loader": "^0.8.0" }, "packageManager": "pnpm@9.7.0", "engines": { "node": ">=20", "pnpm": ">=9" }, "license": "MIT" } ================================================ FILE: ui/pnpm-workspace.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. packages: - "src/plugins/**" - "!src/plugins/builtin/**" ================================================ FILE: ui/public/index.html ================================================
================================================ FILE: ui/public/manifest.json ================================================ { "short_name": "Answer", "name": "Apache Answer", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: ui/public/robots.txt ================================================ ==== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ==== # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: ui/scripts/env.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const fs = require('fs'); const path = require('path'); const yaml = require('js-yaml'); const configFilePath = path.resolve(__dirname, '../../configs/config.yaml'); const envFilePath = path.resolve(__dirname, '../.env.production'); // Read config.yaml file const config = yaml.load(fs.readFileSync(configFilePath, 'utf8')); // Generate .env file content let envContent = 'TSC_COMPILE_ON_ERROR=true\nESLINT_NO_DEV_ERRORS=true\n'; for (const key in config.ui) { const value = config.ui[key]; envContent += `${key !== 'public_url' ? 'REACT_APP_' : ''}${key.toUpperCase()}=${value}\n`; } // Write .env file fs.writeFileSync(envFilePath, envContent); ================================================ FILE: ui/scripts/importPlugins.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const path = require('path'); const fs = require('fs'); const pluginPath = path.join(__dirname, '../src/plugins'); const pluginFolders = fs.readdirSync(pluginPath); function resetPackageJson() { const packageJsonPath = path.join(__dirname, '..', 'package.json'); const packageJsonContent = require(packageJsonPath); const dependencies = packageJsonContent.dependencies; for (const key in dependencies) { if (dependencies[key].startsWith('workspace')) { delete dependencies[key]; } } fs.writeFileSync( packageJsonPath, JSON.stringify(packageJsonContent, null, 2), ); } function addPluginToPackageJson(packageName) { const packageJsonPath = path.join(__dirname, '..', 'package.json'); const packageJsonContent = require(packageJsonPath); packageJsonContent.dependencies[packageName] = 'workspace:*'; fs.writeFileSync( packageJsonPath, JSON.stringify(packageJsonContent, null, 2), ); } resetPackageJson(); pluginFolders.forEach((folder) => { const pluginFolder = path.join(pluginPath, folder); const stat = fs.statSync(pluginFolder); if (stat.isDirectory() && folder !== 'builtin') { if (!fs.existsSync(path.join(pluginFolder, 'index.ts'))) { return; } const packageJson = require(path.join(pluginFolder, 'package.json')); const packageName = packageJson.name; addPluginToPackageJson(packageName); } }); ================================================ FILE: ui/scripts/loadPlugins.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const path = require('path'); const fs = require('fs'); const yaml = require('js-yaml'); const pluginPath = path.join(__dirname, '../src/plugins'); const pluginFolders = fs.readdirSync(pluginPath); function pascalize(str) { return str.split(/[_-]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(''); } function resetIndexTs() { const indexTsPath = path.join(pluginPath, 'index.ts'); fs.writeFileSync(indexTsPath, ''); } function addPluginToIndexTs(packageName, pluginFolder) { const indexTsPath = path.join(pluginPath, 'index.ts'); const indexTsContent = fs.readFileSync(indexTsPath, 'utf-8'); const lines = indexTsContent.split('\n'); const ComponentName = pascalize(packageName); const importLine = `const load${ComponentName} = () => import('${packageName}').then(module => module.default);`; const info = yaml.load(fs.readFileSync(path.join(pluginFolder, 'info.yaml'), 'utf8')); const exportLine = `export const ${info.slug_name} = load${ComponentName}`; if (!lines.includes(exportLine)) { lines.push(importLine); lines.push(exportLine); } fs.writeFileSync(indexTsPath, lines.join('\n')); } const pluginLength = pluginFolders.filter((folder) => { const pluginFolder = path.join(pluginPath, folder); const stat = fs.statSync(pluginFolder); return stat.isDirectory() && folder !== 'builtin'; }).length; if (pluginLength > 0) { resetIndexTs(); } pluginFolders.forEach((folder) => { const pluginFolder = path.join(pluginPath, folder); const stat = fs.statSync(pluginFolder); if (stat.isDirectory() && folder !== 'builtin') { if (!fs.existsSync(path.join(pluginFolder, 'index.ts'))) { return; } const packageJson = require(path.join(pluginFolder, 'package.json')); const packageName = packageJson.name; addPluginToIndexTs(packageName, pluginFolder); } }); ================================================ FILE: ui/scripts/preinstall.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // There is a bug when using npm to install: the execution of preinstall is after install, so when this prompt is displayed, the dependent packages have already been installed. require('./loadPlugins'); if (!/pnpm/.test(process.env.npm_execpath)) { console.warn( `\u001b[33mThis repository requires using pnpm as the package manager for scripts to work properly.\u001b[39m\n`, ); process.exit(1); } ================================================ FILE: ui/scripts/setup-eslint.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const UI_DIR = path.resolve(__dirname, '..'); // UI directory const ROOT_DIR = path.resolve(UI_DIR, '..'); // Project root directory const GIT_DIR = getGitDir(ROOT_DIR); // Git root directory const HUSKY_DIR = path.join(GIT_DIR, '.husky'); // Find Git directory function getGitDir(startDir) { let currentDir = startDir; while (currentDir !== path.parse(currentDir).root) { const gitDir = path.join(currentDir, '.git'); if (fs.existsSync(gitDir)) { return currentDir; } currentDir = path.dirname(currentDir); } throw new Error('Could not find Git directory'); } if (!fs.existsSync(HUSKY_DIR)) { console.log(`Creating husky directory: ${HUSKY_DIR}`); fs.mkdirSync(HUSKY_DIR, { recursive: true }); } if (!fs.existsSync(path.join(HUSKY_DIR, '_'))) { console.log(`Creating husky _ directory: ${path.join(HUSKY_DIR, '_')}`); fs.mkdirSync(path.join(HUSKY_DIR, '_'), { recursive: true }); } // init husky try { console.log('Initializing husky...'); execSync('npx husky install', { cwd: GIT_DIR, stdio: 'inherit' }); } catch (error) { console.error(`❌ Failed to initialize husky: ${error.message}`); process.exit(1); } // create lint-staged config file const lintStagedConfig = { "src/**/*.{ts,tsx}": [ "eslint --fix", "prettier --write" ], "src/**/*.{scss,md}": [ "prettier --write" ] }; console.log(`Creating lint-staged config: ${path.join(UI_DIR, '.lintstagedrc.json')}`); fs.writeFileSync( path.join(UI_DIR, '.lintstagedrc.json'), JSON.stringify(lintStagedConfig, null, 2) ); // create pre-commit hook const preCommitContent = `#!/bin/sh . "$(dirname "$0")/_/husky.sh" echo "🔍 Start running the code check..." # Getting the Git Root Directory GIT_ROOT=$(git rev-parse --show-toplevel) # Get a list of staging files STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR) # Check for files in the ui/ directory UI_FILES=$(echo "$STAGED_FILES" | grep '^ui/' || echo "") if [ -n "$UI_FILES" ]; then echo "🔎 Discover ui file changes, run code checks..." # Switch to the ui directory cd "$GIT_ROOT/ui" || { echo "❌ Unable to access the UI catalog" exit 1 } # 运行 lint-staged echo "✨ Running ESLint and Prettier Formatting..." npx lint-staged --verbose LINT_STAGED_RESULT=$? if [ $LINT_STAGED_RESULT -ne 0 ]; then echo "❌ Code check failed, please fix the above problem" exit $LINT_STAGED_RESULT fi echo "✅ Code check passes!" else echo "ℹ️ No front-end file changes found, skip code checking" fi echo "🎉 Pre-submission check completed" `; console.log(`Creating pre-commit hook: ${path.join(HUSKY_DIR, 'pre-commit')}`); fs.writeFileSync(path.join(HUSKY_DIR, 'pre-commit'), preCommitContent); execSync(`chmod +x ${path.join(HUSKY_DIR, 'pre-commit')}`); // create husky.sh const huskyShContent = `#!/bin/sh if [ -z "$husky_skip_init" ]; then debug () { if [ "$HUSKY_DEBUG" = "1" ]; then echo "husky (debug) - $1" fi } readonly hook_name="$(basename "$0")" debug "starting $hook_name..." if [ "$HUSKY" = "0" ]; then debug "HUSKY=0, skip hook" exit 0 fi if [ -f ~/.huskyrc ]; then debug "sourcing ~/.huskyrc" . ~/.huskyrc fi export readonly husky_skip_init=1 sh -e "$0" "$@" exitCode="$?" if [ $exitCode != 0 ]; then echo "husky - $hook_name hook exited with code $exitCode (error)" fi exit $exitCode fi `; console.log(`Creating husky.sh: ${path.join(HUSKY_DIR, '_', 'husky.sh')}`); fs.writeFileSync( path.join(HUSKY_DIR, '_', 'husky.sh'), huskyShContent ); execSync(`chmod +x ${path.join(HUSKY_DIR, '_', 'husky.sh')}`); console.log('Lint setup complete! Husky and lint-staged have been configured.'); console.log(`Git root directory: ${GIT_DIR}`); console.log(`Husky directory: ${HUSKY_DIR}`); ================================================ FILE: ui/src/App.test.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React from 'react'; import { render, screen } from '@testing-library/react'; import App from './App'; test('renders learn react link', () => { render(); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); }); ================================================ FILE: ui/src/App.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import './i18n/init'; import '@/utils/pluginKit'; import { useMergeRoutes } from '@/router'; import InitialLoadingPlaceholder from '@/components/InitialLoadingPlaceholder'; function App() { const routes = useMergeRoutes(); if (routes.length === 0) { return ; } const router = createBrowserRouter(routes, { basename: process.env.REACT_APP_BASE_URL, }); return ; } export default App; ================================================ FILE: ui/src/behaviour/useLegalClick.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { MouseEvent, useCallback } from 'react'; import { useLegalPrivacy, useLegalTos } from '@/services/client/legal'; export const useLegalClick = () => { const { data: tos } = useLegalTos(); const { data: privacy } = useLegalPrivacy(); const legalClick = useCallback( (evt: MouseEvent, type: 'tos' | 'privacy') => { evt.stopPropagation(); const contentText = type === 'tos' ? tos?.terms_of_service_original_text : privacy?.privacy_policy_original_text; let matchUrl: URL | undefined; try { if (contentText) { matchUrl = new URL(contentText); } // eslint-disable-next-line no-empty } catch (ex) {} if (matchUrl) { evt.preventDefault(); window.open(matchUrl.toString()); } }, [tos, privacy], ); return legalClick; }; ================================================ FILE: ui/src/common/_variable.scss ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ $link-hover-decoration: none; $enable-negative-margins: true; $blue: #0033ff !default; $placeholder-opacity-max: 0.2; $placeholder-opacity-min: 0.1; $enable-smooth-scroll: false; ================================================ FILE: ui/src/common/color.scss ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ :root { --an-side-nav-link: rgba(0, 0, 0, 0.65); --an-toolbar-divider: rgba(0, 0, 0, 0.1); --an-ced4da: #ced4da; --an-e9ecef: #e9ecef; --an-pre: #f8f9fa; --an-6c757d: #6c757d; --an-212529: #212529; --an-gray-300: var(--bs-gray-300); --an-white: #fff; --an-inbox-warning: #fff3cd80; --an-f5: #f5f5f5; --an-answer-item-border-top: rgba(0, 0, 0, 0.125); --an-answer-inbox-nav-border-top: var(--bs-border-color); --an-comment-item-border-bottom: var(--bs-colors-gray-200, #e9ecef); --an-editor-toolbar-hover: #f8f9fa; --ans-editor-toolbar-focus: #dae0e5; --an-editor-placeholder-color: #6c757d; --an-side-nav-link-hover-color: rgba(0, 0, 0, 0.85); --an-invite-answer-item-active: #e9ecef; --an-alert-exist-color: #055160; } [data-bs-theme='dark'] { --an-side-nav-link: rgba(255, 255, 255, 0.65); --an-toolbar-divider: rgba(255, 255, 255, 0.3); --an-ced4da: var(--bs-border-color); --an-e9ecef: #161b22; --an-pre: #161b22; --an-6c757d: var(--bs-body-color); --an-212529: var(--bs-body-color); --an-gray-300: #161b22; --an-white: #000; --an-inbox-warning: #38363180; --an-f5: var(--bs-body-bg); --an-answer-item-border-top: var(--bs-border-color); --an-answer-inbox-nav-border-top: var(--bs-border-color); --an-comment-item-border-bottom: var(--bs-border-color); --an-editor-toolbar-hover: var(--bs-tertiary-bg); --ans-editor-toolbar-focus: var(--bs-tertiary-bg); --an-editor-placeholder-color: var(--bs-body-color); --an-side-nav-link-hover-color: var(--bs-body-color); --an-invite-answer-item-active: var(--bs-tertiary-bg); --an-alert-exist-color: #60cee4; } [data-bs-theme='dark'] { .link-dark { color: rgba(var(--bs-emphasis-color-rgb), 0.8) !important; } /** CodeMirror **/ .cm-editor { background: var(--bs-body-bg); color: var(--bs-body-color); } .cm-cursor { border-left-color: var(--bs-body-color); } .ͼ2.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground { background-color: #3e4451; } /** link color **/ .ͼc { color: rgb(60, 138, 233); } .ͼ5 { color: var(--bs-body-color); } .ͼ2 .cm-selectionBackground { background: #3e4451; } /** CodeMirror end **/ .bg-light { background-color: rgba(0, 0, 0, 0.5) !important; } .text-bg-dark { color: #000 !important; background-color: RGBA(255, 255, 255, var(--bs-bg-opacity, 1)) !important; } /** side nav **/ #sideNav, #answerAccordion { .nav-link:hover, .nav-link.active { background-color: #2b3035 !important; } } /** tag **/ .badge-tag { background: $gray-800; color: $gray-300; &:hover { background: $gray-600; } } .badge-tag-reserved { color: $orange-200; background: $orange-800; &:hover { background: $orange-700; } } .view-level1 { color: $orange-300; } .view-level2 { color: $orange-200; } .view-level3 { color: $orange-100; } } ================================================ FILE: ui/src/common/constants.ts ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export const DEFAULT_SITE_NAME = 'Answer'; export const DEFAULT_LANG = 'en_US'; export const CURRENT_LANG_STORAGE_KEY = '_a_lang_'; export const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_'; export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_'; export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_'; export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_'; export const DRAFT_QUESTION_STORAGE_KEY = '_a_dq_'; export const DRAFT_ANSWER_STORAGE_KEY = '_a_da_'; export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|'; export const DEFAULT_THEME = 'system'; export const ADMIN_PRIVILEGE_CUSTOM_LEVEL = 99; export const SKELETON_SHOW_TIME = 1000; export const LIST_VIEW_STORAGE_KEY = '_a_list_view_'; export const EXTERNAL_CONTENT_DISPLAY_MODE = '_a_ecd_'; export const USER_AGENT_NAMES = { SegmentFault: 'SegmentFault', WeChat: 'WeChat', WeCom: 'WeCom', DingTalk: 'DingTalk', }; export const ADMIN_LIST_STATUS = { // normal; 1: { variant: 'text-bg-success', name: 'normal', }, // closed; 2: { variant: 'text-bg-warning', name: 'closed', }, // deleted 10: { variant: 'text-bg-danger', name: 'deleted', }, // pending 11: { variant: 'text-bg-warning', name: 'pending', }, normal: { variant: 'text-bg-success', name: 'normal', }, closed: { variant: 'text-bg-warning', name: 'closed', }, deleted: { variant: 'text-bg-danger', name: 'deleted', }, pending: { variant: 'text-bg-warning', name: 'pending', }, unlisted: { variant: 'text-bg-secondary', name: 'unlisted', }, }; /** * ADMIN_NAV_MENUS is the navigation menu for the admin panel. * pathPrefix is used to activate the menu item when the activeKey starts with the pathPrefix. */ export const ADMIN_NAV_MENUS = [ { name: 'dashboard', icon: 'speedometer', children: [], }, { name: 'contents', icon: 'file-earmark-text-fill', children: [ { name: 'questions', path: 'qa/questions', pathPrefix: 'qa/' }, { name: 'tags', path: 'tags/settings', pathPrefix: 'tags/' }, ], }, { name: 'intelligence', icon: 'robot', children: [ { name: 'ai_settings', path: 'ai-settings' }, { name: 'ai_assistant', path: 'ai-assistant' }, { name: 'mcp' }, ], }, { name: 'community', icon: 'people-fill', children: [ { name: 'users', pathPrefix: 'users/' }, { name: 'badges' }, { name: 'rules', path: 'rules/privileges', pathPrefix: 'rules/' }, ], }, { name: 'apperance', icon: 'palette-fill', children: [ { name: 'themes', }, { name: 'customize', }, { name: 'branding' }, { name: 'interface' }, ], }, { name: 'advanced', icon: 'gear-fill', children: [ { name: 'general' }, { name: 'security' }, { name: 'files' }, { name: 'login' }, { name: 'seo' }, { name: 'smtp' }, { name: 'apikeys' }, ], }, { name: 'plugins', icon: 'plugin', children: [ { name: 'installed_plugins', path: 'installed-plugins', }, ], }, ]; export const ADMIN_QA_NAV_MENUS = [ { name: 'questions', path: '/admin/qa/questions' }, { name: 'answers', path: '/admin/qa/answers' }, { name: 'settings', path: '/admin/qa/settings' }, ]; export const ADMIN_TAGS_NAV_MENUS = [ // { name: 'tags', path: '/admin/tags' }, { name: 'settings', path: '/admin/tags/settings', }, ]; export const ADMIN_USERS_NAV_MENUS = [ { name: 'users', path: '/admin/users' }, { name: 'settings', path: '/admin/users/settings' }, ]; export const ADMIN_RULES_NAV_MENUS = [ { name: 'privileges', path: '/admin/rules/privileges' }, { name: 'policies', path: '/admin/rules/policies' }, ]; export const TIMEZONES = [ { label: 'Africa', options: [ { value: 'Africa/Abidjan', label: 'Abidjan' }, { value: 'Africa/Accra', label: 'Accra' }, { value: 'Africa/Addis_Ababa', label: 'Addis Ababa' }, { value: 'Africa/Algiers', label: 'Algiers' }, { value: 'Africa/Asmara', label: 'Asmara' }, { value: 'Africa/Bamako', label: 'Bamako' }, { value: 'Africa/Bangui', label: 'Bangui' }, { value: 'Africa/Banjul', label: 'Banjul' }, { value: 'Africa/Bissau', label: 'Bissau' }, { value: 'Africa/Blantyre', label: 'Blantyre' }, { value: 'Africa/Brazzaville', label: 'Brazzaville' }, { value: 'Africa/Bujumbura', label: 'Bujumbura' }, { value: 'Africa/Cairo', label: 'Cairo' }, { value: 'Africa/Casablanca', label: 'Casablanca' }, { value: 'Africa/Ceuta', label: 'Ceuta' }, { value: 'Africa/Conakry', label: 'Conakry' }, { value: 'Africa/Dakar', label: 'Dakar' }, { value: 'Africa/Dar_es_Salaam', label: 'Dar es Salaam' }, { value: 'Africa/Djibouti', label: 'Djibouti' }, { value: 'Africa/Douala', label: 'Douala' }, { value: 'Africa/El_Aaiun', label: 'El Aaiun' }, { value: 'Africa/Freetown', label: 'Freetown' }, { value: 'Africa/Gaborone', label: 'Gaborone' }, { value: 'Africa/Harare', label: 'Harare' }, { value: 'Africa/Johannesburg', label: 'Johannesburg' }, { value: 'Africa/Juba', label: 'Juba' }, { value: 'Africa/Kampala', label: 'Kampala' }, { value: 'Africa/Khartoum', label: 'Khartoum' }, { value: 'Africa/Kigali', label: 'Kigali' }, { value: 'Africa/Kinshasa', label: 'Kinshasa' }, { value: 'Africa/Lagos', label: 'Lagos' }, { value: 'Africa/Libreville', label: 'Libreville' }, { value: 'Africa/Lome', label: 'Lome' }, { value: 'Africa/Luanda', label: 'Luanda' }, { value: 'Africa/Lubumbashi', label: 'Lubumbashi' }, { value: 'Africa/Lusaka', label: 'Lusaka' }, { value: 'Africa/Malabo', label: 'Malabo' }, { value: 'Africa/Maputo', label: 'Maputo' }, { value: 'Africa/Maseru', label: 'Maseru' }, { value: 'Africa/Mbabane', label: 'Mbabane' }, { value: 'Africa/Mogadishu', label: 'Mogadishu' }, { value: 'Africa/Monrovia', label: 'Monrovia' }, { value: 'Africa/Nairobi', label: 'Nairobi' }, { value: 'Africa/Ndjamena', label: 'Ndjamena' }, { value: 'Africa/Niamey', label: 'Niamey' }, { value: 'Africa/Nouakchott', label: 'Nouakchott' }, { value: 'Africa/Ouagadougou', label: 'Ouagadougou' }, { value: 'Africa/Porto-Novo', label: 'Porto-Novo' }, { value: 'Africa/Sao_Tome', label: 'Sao Tome' }, { value: 'Africa/Tripoli', label: 'Tripoli' }, { value: 'Africa/Tunis', label: 'Tunis' }, { value: 'Africa/Windhoek', label: 'Windhoek' }, ], }, { label: 'America', options: [ { value: 'America/Adak', label: 'Adak' }, { value: 'America/Anchorage', label: 'Anchorage' }, { value: 'America/Anguilla', label: 'Anguilla' }, { value: 'America/Antigua', label: 'Antigua' }, { value: 'America/Araguaina', label: 'Araguaina' }, { value: 'America/Argentina/Buenos_Aires', label: 'Argentina - Buenos Aires', }, { value: 'America/Argentina/Catamarca', label: 'Argentina - Catamarca' }, { value: 'America/Argentina/Cordoba', label: 'Argentina - Cordoba' }, { value: 'America/Argentina/Jujuy', label: 'Argentina - Jujuy' }, { value: 'America/Argentina/La_Rioja', label: 'Argentina - La Rioja' }, { value: 'America/Argentina/Mendoza', label: 'Argentina - Mendoza' }, { value: 'America/Argentina/Rio_Gallegos', label: 'Argentina - Rio Gallegos', }, { value: 'America/Argentina/Salta', label: 'Argentina - Salta' }, { value: 'America/Argentina/San_Juan', label: 'Argentina - San Juan' }, { value: 'America/Argentina/San_Luis', label: 'Argentina - San Luis' }, { value: 'America/Argentina/Tucuman', label: 'Argentina - Tucuman' }, { value: 'America/Argentina/Ushuaia', label: 'Argentina - Ushuaia' }, { value: 'America/Aruba', label: 'Aruba' }, { value: 'America/Asuncion', label: 'Asuncion' }, { value: 'America/Atikokan', label: 'Atikokan' }, { value: 'America/Bahia', label: 'Bahia' }, { value: 'America/Bahia_Banderas', label: 'Bahia Banderas' }, { value: 'America/Barbados', label: 'Barbados' }, { value: 'America/Belem', label: 'Belem' }, { value: 'America/Belize', label: 'Belize' }, { value: 'America/Blanc-Sablon', label: 'Blanc-Sablon' }, { value: 'America/Boa_Vista', label: 'Boa Vista' }, { value: 'America/Bogota', label: 'Bogota' }, { value: 'America/Boise', label: 'Boise' }, { value: 'America/Cambridge_Bay', label: 'Cambridge Bay' }, { value: 'America/Campo_Grande', label: 'Campo Grande' }, { value: 'America/Cancun', label: 'Cancun' }, { value: 'America/Caracas', label: 'Caracas' }, { value: 'America/Cayenne', label: 'Cayenne' }, { value: 'America/Cayman', label: 'Cayman' }, { value: 'America/Chicago', label: 'Chicago' }, { value: 'America/Chihuahua', label: 'Chihuahua' }, { value: 'America/Costa_Rica', label: 'Costa Rica' }, { value: 'America/Creston', label: 'Creston' }, { value: 'America/Cuiaba', label: 'Cuiaba' }, { value: 'America/Curacao', label: 'Curacao' }, { value: 'America/Danmarkshavn', label: 'Danmarkshavn' }, { value: 'America/Dawson', label: 'Dawson' }, { value: 'America/Dawson_Creek', label: 'Dawson Creek' }, { value: 'America/Denver', label: 'Denver' }, { value: 'America/Detroit', label: 'Detroit' }, { value: 'America/Dominica', label: 'Dominica' }, { value: 'America/Edmonton', label: 'Edmonton' }, { value: 'America/Eirunepe', label: 'Eirunepe' }, { value: 'America/El_Salvador', label: 'El Salvador' }, { value: 'America/Fort_Nelson', label: 'Fort Nelson' }, { value: 'America/Fortaleza', label: 'Fortaleza' }, { value: 'America/Glace_Bay', label: 'Glace Bay' }, { value: 'America/Godthab', label: 'Godthab' }, { value: 'America/Goose_Bay', label: 'Goose Bay' }, { value: 'America/Grand_Turk', label: 'Grand Turk' }, { value: 'America/Grenada', label: 'Grenada' }, { value: 'America/Guadeloupe', label: 'Guadeloupe' }, { value: 'America/Guatemala', label: 'Guatemala' }, { value: 'America/Guayaquil', label: 'Guayaquil' }, { value: 'America/Guyana', label: 'Guyana' }, { value: 'America/Halifax', label: 'Halifax' }, { value: 'America/Havana', label: 'Havana' }, { value: 'America/Hermosillo', label: 'Hermosillo' }, { value: 'America/Indiana/Indianapolis', label: 'Indiana - Indianapolis', }, { value: 'America/Indiana/Knox', label: 'Indiana - Knox' }, { value: 'America/Indiana/Marengo', label: 'Indiana - Marengo' }, { value: 'America/Indiana/Petersburg', label: 'Indiana - Petersburg' }, { value: 'America/Indiana/Tell_City', label: 'Indiana - Tell City' }, { value: 'America/Indiana/Vevay', label: 'Indiana - Vevay' }, { value: 'America/Indiana/Vincennes', label: 'Indiana - Vincennes' }, { value: 'America/Indiana/Winamac', label: 'Indiana - Winamac' }, { value: 'America/Inuvik', label: 'Inuvik' }, { value: 'America/Iqaluit', label: 'Iqaluit' }, { value: 'America/Jamaica', label: 'Jamaica' }, { value: 'America/Juneau', label: 'Juneau' }, { value: 'America/Kentucky/Louisville', label: 'Kentucky - Louisville' }, { value: 'America/Kentucky/Monticello', label: 'Kentucky - Monticello' }, { value: 'America/Kralendijk', label: 'Kralendijk' }, { value: 'America/La_Paz', label: 'La Paz' }, { value: 'America/Lima', label: 'Lima' }, { value: 'America/Los_Angeles', label: 'Los Angeles' }, { value: 'America/Lower_Princes', label: 'Lower Princes' }, { value: 'America/Maceio', label: 'Maceio' }, { value: 'America/Managua', label: 'Managua' }, { value: 'America/Manaus', label: 'Manaus' }, { value: 'America/Marigot', label: 'Marigot' }, { value: 'America/Martinique', label: 'Martinique' }, { value: 'America/Matamoros', label: 'Matamoros' }, { value: 'America/Mazatlan', label: 'Mazatlan' }, { value: 'America/Miquelon', label: 'Miquelon' }, { value: 'America/Moncton', label: 'Moncton' }, { value: 'America/Monterrey', label: 'Monterrey' }, { value: 'America/Montevideo', label: 'Montevideo' }, { value: 'America/Montserrat', label: 'Montserrat' }, { value: 'America/Nassau', label: 'Nassau' }, { value: 'America/New_York', label: 'New York' }, { value: 'America/Nipigon', label: 'Nipigon' }, { value: 'America/Nome', label: 'Nome' }, { value: 'America/Noronha', label: 'Noronha' }, { value: 'America/North_Dakota/Beulah', label: 'North Dakota - Beulah' }, { value: 'America/North_Dakota/Center', label: 'North Dakota - Center' }, { value: 'America/North_Dakota/New_Salem', label: 'North Dakota - New Salem', }, { value: 'America/Ojinaga', label: 'Ojinaga' }, { value: 'America/Panama', label: 'Panama' }, { value: 'America/Pangnirtung', label: 'Pangnirtung' }, { value: 'America/Paramaribo', label: 'Paramaribo' }, { value: 'America/Phoenix', label: 'Phoenix' }, { value: 'America/Port-au-Prince', label: 'Port-au-Prince' }, { value: 'America/Port_of_Spain', label: 'Port of Spain' }, { value: 'America/Porto_Velho', label: 'Porto Velho' }, { value: 'America/Puerto_Rico', label: 'Puerto Rico' }, { value: 'America/Punta_Arenas', label: 'Punta Arenas' }, { value: 'America/Rainy_River', label: 'Rainy River' }, { value: 'America/Rankin_Inlet', label: 'Rankin Inlet' }, { value: 'America/Recife', label: 'Recife' }, { value: 'America/Regina', label: 'Regina' }, { value: 'America/Resolute', label: 'Resolute' }, { value: 'America/Rio_Branco', label: 'Rio Branco' }, { value: 'America/Santarem', label: 'Santarem' }, { value: 'America/Santiago', label: 'Santiago' }, { value: 'America/Santo_Domingo', label: 'Santo Domingo' }, { value: 'America/Sao_Paulo', label: 'Sao Paulo' }, { value: 'America/Scoresbysund', label: 'Scoresbysund' }, { value: 'America/Sitka', label: 'Sitka' }, { value: 'America/St_Barthelemy', label: 'St Barthelemy' }, { value: 'America/St_Johns', label: 'St Johns' }, { value: 'America/St_Kitts', label: 'St Kitts' }, { value: 'America/St_Lucia', label: 'St Lucia' }, { value: 'America/St_Thomas', label: 'St Thomas' }, { value: 'America/St_Vincent', label: 'St Vincent' }, { value: 'America/Swift_Current', label: 'Swift Current' }, { value: 'America/Tegucigalpa', label: 'Tegucigalpa' }, { value: 'America/Thule', label: 'Thule' }, { value: 'America/Thunder_Bay', label: 'Thunder Bay' }, { value: 'America/Tijuana', label: 'Tijuana' }, { value: 'America/Toronto', label: 'Toronto' }, { value: 'America/Tortola', label: 'Tortola' }, { value: 'America/Vancouver', label: 'Vancouver' }, { value: 'America/Whitehorse', label: 'Whitehorse' }, { value: 'America/Winnipeg', label: 'Winnipeg' }, { value: 'America/Yakutat', label: 'Yakutat' }, { value: 'America/Yellowknife', label: 'Yellowknife' }, ], }, { label: 'Antarctica', options: [ { value: 'Antarctica/Casey', label: 'Casey' }, { value: 'Antarctica/Davis', label: 'Davis' }, { value: 'Antarctica/DumontDUrville', label: 'DumontDUrville' }, { value: 'Antarctica/Macquarie', label: 'Macquarie' }, { value: 'Antarctica/Mawson', label: 'Mawson' }, { value: 'Antarctica/McMurdo', label: 'McMurdo' }, { value: 'Antarctica/Palmer', label: 'Palmer' }, { value: 'Antarctica/Rothera', label: 'Rothera' }, { value: 'Antarctica/Syowa', label: 'Syowa' }, { value: 'Antarctica/Troll', label: 'Troll' }, { value: 'Antarctica/Vostok', label: 'Vostok' }, ], }, { label: 'Arctic', options: [{ value: 'Arctic/Longyearbyen', label: 'Longyearbyen' }], }, { label: 'Asia', options: [ { value: 'Asia/Aden', label: 'Aden' }, { value: 'Asia/Almaty', label: 'Almaty' }, { value: 'Asia/Amman', label: 'Amman' }, { value: 'Asia/Anadyr', label: 'Anadyr' }, { value: 'Asia/Aqtau', label: 'Aqtau' }, { value: 'Asia/Aqtobe', label: 'Aqtobe' }, { value: 'Asia/Ashgabat', label: 'Ashgabat' }, { value: 'Asia/Atyrau', label: 'Atyrau' }, { value: 'Asia/Baghdad', label: 'Baghdad' }, { value: 'Asia/Bahrain', label: 'Bahrain' }, { value: 'Asia/Baku', label: 'Baku' }, { value: 'Asia/Bangkok', label: 'Bangkok' }, { value: 'Asia/Barnaul', label: 'Barnaul' }, { value: 'Asia/Beirut', label: 'Beirut' }, { value: 'Asia/Bishkek', label: 'Bishkek' }, { value: 'Asia/Brunei', label: 'Brunei' }, { value: 'Asia/Chita', label: 'Chita' }, { value: 'Asia/Choibalsan', label: 'Choibalsan' }, { value: 'Asia/Colombo', label: 'Colombo' }, { value: 'Asia/Damascus', label: 'Damascus' }, { value: 'Asia/Dhaka', label: 'Dhaka' }, { value: 'Asia/Dili', label: 'Dili' }, { value: 'Asia/Dubai', label: 'Dubai' }, { value: 'Asia/Dushanbe', label: 'Dushanbe' }, { value: 'Asia/Famagusta', label: 'Famagusta' }, { value: 'Asia/Gaza', label: 'Gaza' }, { value: 'Asia/Hebron', label: 'Hebron' }, { value: 'Asia/Ho_Chi_Minh', label: 'Ho Chi Minh' }, { value: 'Asia/Hong_Kong', label: 'Hong Kong' }, { value: 'Asia/Hovd', label: 'Hovd' }, { value: 'Asia/Irkutsk', label: 'Irkutsk' }, { value: 'Asia/Jakarta', label: 'Jakarta' }, { value: 'Asia/Jayapura', label: 'Jayapura' }, { value: 'Asia/Jerusalem', label: 'Jerusalem' }, { value: 'Asia/Kabul', label: 'Kabul' }, { value: 'Asia/Kamchatka', label: 'Kamchatka' }, { value: 'Asia/Karachi', label: 'Karachi' }, { value: 'Asia/Kathmandu', label: 'Kathmandu' }, { value: 'Asia/Khandyga', label: 'Khandyga' }, { value: 'Asia/Kolkata', label: 'Kolkata' }, { value: 'Asia/Krasnoyarsk', label: 'Krasnoyarsk' }, { value: 'Asia/Kuala_Lumpur', label: 'Kuala Lumpur' }, { value: 'Asia/Kuching', label: 'Kuching' }, { value: 'Asia/Kuwait', label: 'Kuwait' }, { value: 'Asia/Macau', label: 'Macau' }, { value: 'Asia/Magadan', label: 'Magadan' }, { value: 'Asia/Makassar', label: 'Makassar' }, { value: 'Asia/Manila', label: 'Manila' }, { value: 'Asia/Muscat', label: 'Muscat' }, { value: 'Asia/Nicosia', label: 'Nicosia' }, { value: 'Asia/Novokuznetsk', label: 'Novokuznetsk' }, { value: 'Asia/Novosibirsk', label: 'Novosibirsk' }, { value: 'Asia/Omsk', label: 'Omsk' }, { value: 'Asia/Oral', label: 'Oral' }, { value: 'Asia/Phnom_Penh', label: 'Phnom Penh' }, { value: 'Asia/Pontianak', label: 'Pontianak' }, { value: 'Asia/Pyongyang', label: 'Pyongyang' }, { value: 'Asia/Qatar', label: 'Qatar' }, { value: 'Asia/Qostanay', label: 'Qostanay' }, { value: 'Asia/Qyzylorda', label: 'Qyzylorda' }, { value: 'Asia/Riyadh', label: 'Riyadh' }, { value: 'Asia/Sakhalin', label: 'Sakhalin' }, { value: 'Asia/Samarkand', label: 'Samarkand' }, { value: 'Asia/Seoul', label: 'Seoul' }, { value: 'Asia/Shanghai', label: 'Shanghai' }, { value: 'Asia/Singapore', label: 'Singapore' }, { value: 'Asia/Srednekolymsk', label: 'Srednekolymsk' }, { value: 'Asia/Taipei', label: 'Taipei' }, { value: 'Asia/Tashkent', label: 'Tashkent' }, { value: 'Asia/Tbilisi', label: 'Tbilisi' }, { value: 'Asia/Tehran', label: 'Tehran' }, { value: 'Asia/Thimphu', label: 'Thimphu' }, { value: 'Asia/Tokyo', label: 'Tokyo' }, { value: 'Asia/Tomsk', label: 'Tomsk' }, { value: 'Asia/Ulaanbaatar', label: 'Ulaanbaatar' }, { value: 'Asia/Urumqi', label: 'Urumqi' }, { value: 'Asia/Ust-Nera', label: 'Ust-Nera' }, { value: 'Asia/Vientiane', label: 'Vientiane' }, { value: 'Asia/Vladivostok', label: 'Vladivostok' }, { value: 'Asia/Yakutsk', label: 'Yakutsk' }, { value: 'Asia/Yangon', label: 'Yangon' }, { value: 'Asia/Yekaterinburg', label: 'Yekaterinburg' }, { value: 'Asia/Yerevan', label: 'Yerevan' }, ], }, { label: 'Atlantic', options: [ { value: 'Atlantic/Azores', label: 'Azores' }, { value: 'Atlantic/Bermuda', label: 'Bermuda' }, { value: 'Atlantic/Canary', label: 'Canary' }, { value: 'Atlantic/Cape_Verde', label: 'Cape Verde' }, { value: 'Atlantic/Faroe', label: 'Faroe' }, { value: 'Atlantic/Madeira', label: 'Madeira' }, { value: 'Atlantic/Reykjavik', label: 'Reykjavik' }, { value: 'Atlantic/South_Georgia', label: 'South Georgia' }, { value: 'Atlantic/Stanley', label: 'Stanley' }, { value: 'Atlantic/St_Helena', label: 'St Helena' }, ], }, { label: 'Australia', options: [ { value: 'Australia/Adelaide', label: 'Adelaide' }, { value: 'Australia/Brisbane', label: 'Brisbane' }, { value: 'Australia/Broken_Hill', label: 'Broken Hill' }, { value: 'Australia/Currie', label: 'Currie' }, { value: 'Australia/Darwin', label: 'Darwin' }, { value: 'Australia/Eucla', label: 'Eucla' }, { value: 'Australia/Hobart', label: 'Hobart' }, { value: 'Australia/Lindeman', label: 'Lindeman' }, { value: 'Australia/Lord_Howe', label: 'Lord Howe' }, { value: 'Australia/Melbourne', label: 'Melbourne' }, { value: 'Australia/Perth', label: 'Perth' }, { value: 'Australia/Sydney', label: 'Sydney' }, ], }, { label: 'Europe', options: [ { value: 'Europe/Amsterdam', label: 'Amsterdam' }, { value: 'Europe/Andorra', label: 'Andorra' }, { value: 'Europe/Astrakhan', label: 'Astrakhan' }, { value: 'Europe/Athens', label: 'Athens' }, { value: 'Europe/Belgrade', label: 'Belgrade' }, { value: 'Europe/Berlin', label: 'Berlin' }, { value: 'Europe/Bratislava', label: 'Bratislava' }, { value: 'Europe/Brussels', label: 'Brussels' }, { value: 'Europe/Bucharest', label: 'Bucharest' }, { value: 'Europe/Budapest', label: 'Budapest' }, { value: 'Europe/Busingen', label: 'Busingen' }, { value: 'Europe/Chisinau', label: 'Chisinau' }, { value: 'Europe/Copenhagen', label: 'Copenhagen' }, { value: 'Europe/Dublin', label: 'Dublin' }, { value: 'Europe/Gibraltar', label: 'Gibraltar' }, { value: 'Europe/Guernsey', label: 'Guernsey' }, { value: 'Europe/Helsinki', label: 'Helsinki' }, { value: 'Europe/Isle_of_Man', label: 'Isle of Man' }, { value: 'Europe/Istanbul', label: 'Istanbul' }, { value: 'Europe/Jersey', label: 'Jersey' }, { value: 'Europe/Kaliningrad', label: 'Kaliningrad' }, { value: 'Europe/Kiev', label: 'Kiev' }, { value: 'Europe/Kirov', label: 'Kirov' }, { value: 'Europe/Lisbon', label: 'Lisbon' }, { value: 'Europe/Ljubljana', label: 'Ljubljana' }, { value: 'Europe/London', label: 'London' }, { value: 'Europe/Luxembourg', label: 'Luxembourg' }, { value: 'Europe/Madrid', label: 'Madrid' }, { value: 'Europe/Malta', label: 'Malta' }, { value: 'Europe/Mariehamn', label: 'Mariehamn' }, { value: 'Europe/Minsk', label: 'Minsk' }, { value: 'Europe/Monaco', label: 'Monaco' }, { value: 'Europe/Moscow', label: 'Moscow' }, { value: 'Europe/Oslo', label: 'Oslo' }, { value: 'Europe/Paris', label: 'Paris' }, { value: 'Europe/Podgorica', label: 'Podgorica' }, { value: 'Europe/Prague', label: 'Prague' }, { value: 'Europe/Riga', label: 'Riga' }, { value: 'Europe/Rome', label: 'Rome' }, { value: 'Europe/Samara', label: 'Samara' }, { value: 'Europe/San_Marino', label: 'San Marino' }, { value: 'Europe/Sarajevo', label: 'Sarajevo' }, { value: 'Europe/Saratov', label: 'Saratov' }, { value: 'Europe/Simferopol', label: 'Simferopol' }, { value: 'Europe/Skopje', label: 'Skopje' }, { value: 'Europe/Sofia', label: 'Sofia' }, { value: 'Europe/Stockholm', label: 'Stockholm' }, { value: 'Europe/Tallinn', label: 'Tallinn' }, { value: 'Europe/Tirane', label: 'Tirane' }, { value: 'Europe/Ulyanovsk', label: 'Ulyanovsk' }, { value: 'Europe/Uzhgorod', label: 'Uzhgorod' }, { value: 'Europe/Vaduz', label: 'Vaduz' }, { value: 'Europe/Vatican', label: 'Vatican' }, { value: 'Europe/Vienna', label: 'Vienna' }, { value: 'Europe/Vilnius', label: 'Vilnius' }, { value: 'Europe/Volgograd', label: 'Volgograd' }, { value: 'Europe/Warsaw', label: 'Warsaw' }, { value: 'Europe/Zagreb', label: 'Zagreb' }, { value: 'Europe/Zaporozhye', label: 'Zaporozhye' }, { value: 'Europe/Zurich', label: 'Zurich' }, ], }, { label: 'Indian', options: [ { value: 'Indian/Antananarivo', label: 'Antananarivo' }, { value: 'Indian/Chagos', label: 'Chagos' }, { value: 'Indian/Christmas', label: 'Christmas' }, { value: 'Indian/Cocos', label: 'Cocos' }, { value: 'Indian/Comoro', label: 'Comoro' }, { value: 'Indian/Kerguelen', label: 'Kerguelen' }, { value: 'Indian/Mahe', label: 'Mahe' }, { value: 'Indian/Maldives', label: 'Maldives' }, { value: 'Indian/Mauritius', label: 'Mauritius' }, { value: 'Indian/Mayotte', label: 'Mayotte' }, { value: 'Indian/Reunion', label: 'Reunion' }, ], }, { label: 'Pacific', options: [ { value: 'Pacific/Apia', label: 'Apia' }, { value: 'Pacific/Auckland', label: 'Auckland' }, { value: 'Pacific/Bougainville', label: 'Bougainville' }, { value: 'Pacific/Chatham', label: 'Chatham' }, { value: 'Pacific/Chuuk', label: 'Chuuk' }, { value: 'Pacific/Easter', label: 'Easter' }, { value: 'Pacific/Efate', label: 'Efate' }, { value: 'Pacific/Enderbury', label: 'Enderbury' }, { value: 'Pacific/Fakaofo', label: 'Fakaofo' }, { value: 'Pacific/Fiji', label: 'Fiji' }, { value: 'Pacific/Funafuti', label: 'Funafuti' }, { value: 'Pacific/Galapagos', label: 'Galapagos' }, { value: 'Pacific/Gambier', label: 'Gambier' }, { value: 'Pacific/Guadalcanal', label: 'Guadalcanal' }, { value: 'Pacific/Guam', label: 'Guam' }, { value: 'Pacific/Honolulu', label: 'Honolulu' }, { value: 'Pacific/Kiritimati', label: 'Kiritimati' }, { value: 'Pacific/Kosrae', label: 'Kosrae' }, { value: 'Pacific/Kwajalein', label: 'Kwajalein' }, { value: 'Pacific/Majuro', label: 'Majuro' }, { value: 'Pacific/Marquesas', label: 'Marquesas' }, { value: 'Pacific/Midway', label: 'Midway' }, { value: 'Pacific/Nauru', label: 'Nauru' }, { value: 'Pacific/Niue', label: 'Niue' }, { value: 'Pacific/Norfolk', label: 'Norfolk' }, { value: 'Pacific/Noumea', label: 'Noumea' }, { value: 'Pacific/Pago_Pago', label: 'Pago Pago' }, { value: 'Pacific/Palau', label: 'Palau' }, { value: 'Pacific/Pitcairn', label: 'Pitcairn' }, { value: 'Pacific/Pohnpei', label: 'Pohnpei' }, { value: 'Pacific/Port_Moresby', label: 'Port Moresby' }, { value: 'Pacific/Rarotonga', label: 'Rarotonga' }, { value: 'Pacific/Saipan', label: 'Saipan' }, { value: 'Pacific/Tahiti', label: 'Tahiti' }, { value: 'Pacific/Tarawa', label: 'Tarawa' }, { value: 'Pacific/Tongatapu', label: 'Tongatapu' }, { value: 'Pacific/Wake', label: 'Wake' }, { value: 'Pacific/Wallis', label: 'Wallis' }, ], }, { label: 'UTC', options: [{ value: 'UTC', label: 'UTC' }], }, ]; export const DEFAULT_TIMEZONE = 'UTC'; export const TIMELINE_NORMAL_ACTIVITY_TYPE = [ 'undeleted', 'deleted', 'downvote', 'upvote', 'reopened', 'closed', 'pin', 'unpin', 'show', 'hide', ]; export const SYSTEM_AVATAR_OPTIONS = [ { label: 'System', value: 'system', }, { label: 'Gravatar', value: 'gravatar', }, ]; export const TAG_SLUG_NAME_MAX_LENGTH = 35; export const DEFAULT_THEME_COLOR = '#0033ff'; export const SUSPENSE_USER_TIME = [ { label: 'hours', time: '24', value: '24h', }, { label: 'hours', time: '48', value: '48h', }, { label: 'hours', time: '72', value: '72h', }, { label: 'days', time: '7', value: '7d', }, { label: 'days', time: '14', value: '14d', }, { label: 'months', time: '1', value: '1m', }, { label: 'months', time: '2', value: '2m', }, { label: 'months', time: '3', value: '3m', }, { label: 'months', time: '6', value: '6m', }, { label: 'year', time: '1', value: '1y', }, ]; ================================================ FILE: ui/src/common/interface.ts ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface FormValue { value: T; isInvalid: boolean; errorMsg: string; [prop: string]: any; } export interface FormDataType { [prop: string]: FormValue; } export interface FieldError { error_field: string; error_msg: string; } export interface Paging { page: number; page_size?: number; } export type ReportType = 'question' | 'answer' | 'comment' | 'user'; export type ReportAction = 'close' | 'flag' | 'review'; export interface ReportParams { type: ReportType; action: ReportAction; } export interface TagBase { display_name: string; slug_name: string; original_text?: string; recommend?: boolean; reserved?: boolean; } export interface Tag extends TagBase { main_tag_slug_name?: string; parsed_text?: string; tag_id?: string; } export interface SynonymsTag extends Tag { tag_id: string; tag?: string; } export interface TagInfo extends TagBase { tag_id: string; original_text: string; parsed_text: string; follow_count: number; question_count: number; is_follower: boolean; member_actions; created_at?; updated_at?; main_tag_slug_name?: string; excerpt?; status: string; } export interface QuestionParams extends ImgCodeReq { title: string; url_title?: string; content: string; tags: Tag[]; } export interface QuestionWithAnswer extends QuestionParams { answer_content: string; } export interface ListResult { count: number; list: T[]; } export interface AnswerParams extends ImgCodeReq { content: string; html: string; question_id: string; id: string; edit_summary?: string; } export interface LoginReqParams { e_mail: string; /** password */ pass: string; captcha_id?: string; captcha_code?: string; } export interface RegisterReqParams extends LoginReqParams { name: string; } export interface ModifyPasswordReq { old_pass: string; pass: string; } /** User */ export interface ModifyUserReq { display_name: string; username?: string; avatar: any; bio: string; bio_html?: string; location: string; website: string; } enum RoleId { User = 1, Admin = 2, Moderator = 3, } export interface User { username: string; rank: number; vote_count: number; display_name: string; avatar: string; } export interface UserInfoBase { id?: string; avatar: any; username: string; display_name: string; rank: number; website: string; location: string; ip_info?: string; status?: 'normal' | 'suspended' | 'deleted' | 'inactive'; /** roles */ role_id?: RoleId; } export interface UserInfoRes extends UserInfoBase { bio: string; bio_html: string; create_time?: string; /** * value = 1 active; * value = 2 inactivated */ mail_status: number; language: string; e_mail?: string; have_password: boolean; [prop: string]: any; } export type UploadType = 'post' | 'avatar' | 'branding' | 'post_attachment'; export interface UploadReq { file: FormData; } export interface ImgCodeReq { captcha_id?: string; captcha_code?: string; } export interface ImgCodeRes { captcha_id: string; captcha_img: string; verify: boolean; } export interface PasswordResetReq extends ImgCodeReq { e_mail: string; } export interface PasswordReplaceReq extends ImgCodeReq { code: string; pass: string; } export interface CaptchaReq extends ImgCodeReq { verify: ImgCodeRes['verify']; } export type CaptchaKey = | 'email' | 'password' | 'edit_userinfo' | 'question' | 'answer' | 'comment' | 'edit' | 'invitation_answer' | 'search' | 'report' | 'delete' | 'vote'; export interface SetNoticeReq { notice_switch: boolean; } export interface NotificationBadgeAward { notification_id: string; badge_id: string; name: string; icon: string; level: number; } export interface NotificationStatus { inbox: number; achievement: number; revision: number; can_revision: boolean; badge_award: NotificationBadgeAward | null; } export interface QuestionDetailRes { id: string; title: string; content: string; html: string; tags: any[]; view_count: number; unique_view_count?: number; answer_count: number; favorites_count: number; follow_counts: 0; accepted_answer_id: string; last_answer_id: string; create_time: string; update_time: string; user_info: UserInfoBase; answered: boolean; collected: boolean; answer_ids: string[]; [prop: string]: any; } export interface AnswersReq extends Paging { order?: 'default' | 'updated' | 'created'; question_id: string; } export interface AnswerItem { id: string; question_id: string; content: string; html: string; create_time: string; update_time: string; user_info: UserInfoBase; [prop: string]: any; } export interface PostAnswerReq extends ImgCodeReq { content: string; html?: string; question_id: string; } export interface PageUser { id?; displayName; userName?; avatar_url?; } export interface LangsType { label: string; value: string; } /** * @description interface for Question */ export type QuestionOrderBy = | 'recommend' | 'newest' | 'active' | 'hot' | 'score' | 'unanswered' | 'frequent'; export interface QueryQuestionsReq extends Paging { order: QuestionOrderBy; tag?: string; in_days?: number; } export type AdminQuestionStatus = | 'available' | 'pending' | 'closed' | 'deleted'; export type AdminContentsFilterBy = 'normal' | 'pending' | 'closed' | 'deleted'; export interface AdminContentsReq extends Paging { status: AdminContentsFilterBy; query?: string; } /** * @description interface for Answer */ export type AdminAnswerStatus = 'available' | 'deleted'; /** * @description interface for Users */ export type UserFilterBy = | 'normal' | 'staff' | 'inactive' | 'suspended' | 'deleted'; export type BadgeFilterBy = 'all' | 'active' | 'inactive'; export type InstalledPluginsFilterBy = | 'all' | 'active' | 'inactive' | 'outdated'; /** * @description interface for Flags */ export type FlagStatus = 'pending' | 'completed'; export type FlagType = 'all' | 'question' | 'answer' | 'comment'; export interface AdminFlagsReq extends Paging { status: FlagStatus; object_type: FlagType; } /** * @description interface for Admin Settings */ export interface AdminSettingsGeneral { name: string; short_description: string; description: string; site_url: string; contact_email: string; permalink?: number; } export interface HelmetBase { pageTitle?: string; description?: string; keywords?: string; } export interface HelmetUpdate extends Omit { title?: string; subtitle?: string; } export interface AdminSettingsInterface { language: string; time_zone?: string; } export interface AdminSettingsSmtp { encryption: string; from_email: string; from_name: string; smtp_authentication: boolean; smtp_host: string; smtp_password?: string; smtp_port: number; smtp_username?: string; test_email_recipient?: string; } export interface AdminSettingsUsers { allow_update_avatar: boolean; allow_update_bio: boolean; allow_update_display_name: boolean; allow_update_location: boolean; allow_update_username: boolean; allow_update_website: boolean; default_avatar: string; gravatar_base_url: string; } export interface AdminSettingsSecurity { external_content_display: string; check_update: boolean; login_required: boolean; } export interface SiteSettings { branding: AdminSettingBranding; general: AdminSettingsGeneral; interface: AdminSettingsInterface; login: AdminSettingsLogin; custom_css_html: AdminSettingsCustom; theme: AdminSettingsTheme; site_seo: AdminSettingsSeo; site_users: AdminSettingsUsers; site_advanced: AdminSettingsWrite; site_questions: AdminQuestionSetting; site_tags: AdminTagsSetting; version: string; revision: string; site_security: AdminSettingsSecurity; ai_enabled: boolean; } export interface AdminSettingBranding { logo: string; square_icon: string; mobile_logo?: string; favicon?: string; } export interface AdminSettingsLegal { privacy_policy_original_text?: string; privacy_policy_parsed_text?: string; terms_of_service_original_text?: string; terms_of_service_parsed_text?: string; } export interface AdminSettingsWrite { max_image_size?: number; max_attachment_size?: number; max_image_megapixel?: number; authorized_image_extensions?: string[]; authorized_attachment_extensions?: string[]; } export interface AdminSettingsSeo { robots: string; /** * 0: not set * 1:with title * 2: no title */ permalink: number; } export type themeConfig = { navbar_style: string; primary_color: string; [k: string]: string | number; }; export interface AdminSettingsTheme { theme: string; color_scheme: string; layout: string; theme_options?: { label: string; value: string }[]; theme_config: Record; } export interface AdminSettingsCustom { custom_css: string; custom_head: string; custom_header: string; custom_footer: string; custom_sidebar: string; } export interface AdminSettingsLogin { allow_new_registrations: boolean; allow_email_registrations: boolean; allow_email_domains: string[]; allow_password_login: boolean; } /** * @description interface for Activity */ export interface FollowParams { is_cancel: boolean; object_id: string; } /** * @description search request params */ export interface SearchParams extends ImgCodeReq { q: string; order: string; page: number; size?: number; } /** * @description search response data */ export interface SearchResItem { object_type: string; object: { url_title?: string; id: string; question_id?: string; title: string; excerpt: string; created_at: number; user_info: UserInfoBase; vote_count: number; answer_count: number; accepted: boolean; tags: TagBase[]; status?: string; }; } export interface SearchRes extends ListResult { extra: any; } export interface AdminDashboard { info: { question_count: number; resolved_count: number; resolved_rate: string; unanswered_count: number; unanswered_rate: string; answer_count: number; comment_count: number; vote_count: number; user_count: number; report_count: number; uploading_files: boolean; smtp: 'enabled' | 'disabled' | 'not_configured'; time_zone: string; occupying_storage_space: string; app_start_time: number; https: boolean; login_required: boolean; go_version: string; database_version: string; database_size: string; version_info: { remote_version: string; version: string; }; }; } export interface TimelineReq { show_vote: boolean; object_id: string; } export interface TimelineItem { activity_id: number; revision_id: number; created_at: number; activity_type: string; comment: string; object_id: string; object_type: string; cancelled: boolean; cancelled_at: any; user_info: UserInfoBase; } export interface TimelineObject { title: string; url_title?: string; object_type: string; question_id: string; answer_id: string; main_tag_slug_name?: string; display_name?: string; } export interface TimelineRes { object_info: TimelineObject; timeline: TimelineItem[]; } export interface SuggestReviewItem { type: 'question' | 'answer' | 'tag'; info: { url_title?: string; object_id: string; title: string; content: string; html: string; tags: Tag[]; }; unreviewed_info: { id: string; use_id: string; object_id: string; title: string; status: 0 | 1; create_at: number; user_info: UserInfoBase; reason: string; content: Tag | QuestionDetailRes | AnswerItem; }; } export interface SuggestReviewResp { count: number; list: SuggestReviewItem[]; } export interface ReasonItem { content_type: string; description: string; name: string; placeholder: string; reason_type: number; } export interface BaseReviewItem { object_type: 'question' | 'answer' | 'comment' | 'user'; object_id: string; object_show_status: number; object_status: number; tags: Tag[]; title: string; original_text: string; author_user_info: UserInfoBase; created_at: number; submit_at: number; comment_id: string; question_id: string; answer_id: string; answer_count: number; answer_accepted?: boolean; flag_id: string; url_title: string; parsed_text: string; } export interface FlagReviewItem extends BaseReviewItem { reason: ReasonItem; reason_content: string; submitter_user: UserInfoBase; } export interface FlagReviewResp { count: number; list: FlagReviewItem[]; } export interface QueuedReviewItem extends BaseReviewItem { review_id: number; reason: string; submitter_display_name: string; } export interface QueuedReviewResp { count: number; list: QueuedReviewItem[]; } export interface UserRoleItem { id: number; name: string; description: string; } export interface MemberActionItem { action: string; name: string; type: string; } export interface QuestionOperationReq { id: string; operation: 'pin' | 'unpin' | 'hide' | 'show'; } export interface OauthBindEmailReq { binding_key: string; email: string; must: boolean; } export interface UserOauthConnectorItem { icon: string; name: string; link: string; binding: boolean; external_id: string; } export interface NotificationConfigItem { enable: boolean; key: string; } export interface NotificationConfig { all_new_question: NotificationConfigItem; all_new_question_for_following_tags: NotificationConfigItem; inbox: NotificationConfigItem; } export interface ActivatedPlugin { slug_name: string; enabled: boolean; } export interface UserPluginsConfigRes { name: string; slug_name: string; } export interface ReviewTypeItem { label: string; name: string; todo_amount: number; } export interface PutFlagReviewParams { operation_type: | 'edit_post' | 'close_post' | 'delete_post' | 'unlist_post' | 'ignore_report'; flag_id: string; close_msg?: string; close_type?: number; title?: string; content?: string; tags?: Tag[]; // mention_username_list?: any; captcha_code?: any; captcha_id?: any; } /** * @description response for reaction */ export interface ReactionItems { reaction_summary: ReactionItem[]; } export interface ReactionItem { emoji: string; count: number; tooltip: string; is_active: boolean; } export interface BadgeListItem { id: string; name: string; icon: string; award_count: number; earned: boolean; /** 1: bronze 2: silver 3:gold */ level: number; earned_count?: number; } export interface BadgeListGroupItem { badges: BadgeListItem[]; group_name: string; } export interface BadgeInfo extends BadgeListItem { description: string; earned_count: number; is_single: boolean; } export interface AdminBadgeListItem extends BadgeListItem { group_name: string; status: string; description: string; } export interface BadgeDetailListReq { page: number; page_size: number; badge_id: string; username?: string | null; } export interface BadgeDetailListItem { created_at: number; author_user_info: UserInfoBase; object_type: string; object_id: string; url_title: string; question_id: string; answer_id: string; comment_id: string; } export interface BadgeDetailListRes { count: number; list: BadgeDetailListItem[]; } export interface AdminApiKeysItem { access_key: string; created_at: number; description: string; id: number; last_used_at: number; scope: string; } export interface AddOrEditApiKeyParams { description: string; scope?: string; id?: number; } export interface AiConfig { enabled: boolean; chosen_provider: string; ai_providers: Array<{ provider: string; api_host: string; api_key: string; model: string; }>; } export interface AiProviderItem { name: string; display_name: string; default_api_host: string; } export interface ConversationListItem { conversation_id: string; created_at: number; topic: string; } export interface AdminConversationListItem { id: string; topic: string; helpful_count: number; unhelpful_count: number; created_at: number; user_info: UserInfoBase; } export interface ConversationDetailItem { chat_completion_id: string; content: string; role: string; helpful: number; unhelpful: number; created_at: number; } export interface ConversationDetail { conversation_id: string; created_at: number; records: ConversationDetailItem[]; topic: string; updated_at: number; } export interface VoteConversationParams { cancel: boolean; chat_completion_id: string; vote_type: 'helpful' | 'unhelpful'; } export interface AdminQuestionSetting { min_tags: number; min_content: number; restrict_answer: boolean; } export interface AdminTagsSetting { recommend_tags: Tag[]; required_tag: boolean; reserved_tags: Tag[]; } ================================================ FILE: ui/src/common/pattern.ts ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const pattern = { email: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/, search: /(\[.*\])|(is:answer)|(is:question)|(score:\d*)|(user:\S*)|(answers:\d*)/g, uaWeChat: /micromessenger/i, uaWeCom: /wxwork/i, uaDingTalk: /dingtalk/i, }; export default pattern; ================================================ FILE: ui/src/common/sideNavLayout.scss ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .main-mx-with { width: 100%; max-width: 1072px; } .answer-container { min-height: calc(100vh - 53px - 62px); } .page-right-side { flex: none; width: 300px; box-sizing: content-box; } // lg @media screen and (max-width: 1199.9px) { .main-mx-with { padding-left: 12px; padding-right: 12px; } .page-main { max-width: 100%; } .page-right-side { width: 100%; box-sizing: border-box; } } ================================================ FILE: ui/src/components/AccordionNav/index.css ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .collapse-indicator { transition: all 0.2s ease; } .expanding .collapse-indicator { transform: rotate(90deg); } #answerAccordion { max-width: 208px; .nav-link { color: var(--an-side-nav-link); } .nav-link:focus-visible { box-shadow: none; } .nav-link:hover { color: var(--an-side-nav-link-hover-color); background-color: var(--bs-gray-100); } .nav-link.active { color: var(--an-side-nav-link-hover-color); background-color: var(--bs-gray-200); } } ================================================ FILE: ui/src/components/AccordionNav/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React, { FC, useEffect, useState } from 'react'; import { Accordion, Nav } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useNavigate, useMatch, NavLink } from 'react-router-dom'; import classNames from 'classnames'; import { floppyNavigation } from '@/utils'; import { Icon } from '@/components'; import './index.css'; export interface MenuItem { name: string; path?: string; pathPrefix?: string; icon?: string; displayName?: string; badgeContent?: string | number; children?: MenuItem[]; } function MenuNode({ menu, callback, activeKey, expanding = false, path = '/', }: { menu: MenuItem; callback: (evt: any, menu: MenuItem, href: string, isLeaf: boolean) => void; activeKey: string; expanding?: boolean; path?: string; }) { const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); const isLeaf = !menu.children || menu.children.length === 0; const href = isLeaf ? `${path}${menu.path || ''}` : '#'; return ( {isLeaf ? ( { callback(evt, menu, href, isLeaf); }} className={classNames( 'text-nowrap d-flex flex-nowrap align-items-center w-100', { expanding, active: activeKey === menu.path || (menu.path && activeKey.startsWith(`${menu.path}/`)) || // if pathPrefix is set, activate when activeKey starts with the pathPrefix (menu.pathPrefix && activeKey.startsWith(menu.pathPrefix)), }, )}> {menu?.icon && } {menu.displayName ? menu.displayName : t(menu.name)} {menu.badgeContent ? ( {menu.badgeContent} ) : null} {!isLeaf && ( )} ) : ( { callback(evt, menu, href, isLeaf); }} className={classNames( 'text-nowrap d-flex flex-nowrap align-items-center w-100', { expanding, active: activeKey === menu.path || (menu.path && activeKey.startsWith(`${menu.path}/`)) || (menu.pathPrefix && activeKey.startsWith(menu.pathPrefix)), }, )}> {menu?.icon && } {menu.displayName ? menu.displayName : t(menu.name)} {menu.badgeContent ? ( {menu.badgeContent} ) : null} {!isLeaf && ( )} )} {menu.children && menu.children.length > 0 ? ( <> {menu.children.map((leaf) => { return ( ); })} ) : null} ); } interface AccordionProps { menus: MenuItem[]; path?: string; } const AccordionNav: FC = ({ menus = [], path = '/' }) => { const navigate = useNavigate(); const pathMatch = useMatch(`${path}*`); // auto set menu fields menus.forEach((m) => { if (!m.path) { m.path = m.name; } if (!Array.isArray(m.children)) { m.children = []; } m.children.forEach((sm) => { if (!sm.path) { sm.path = sm.name; } if (!Array.isArray(sm.children)) { sm.children = []; } }); }); const splat = pathMatch && pathMatch.params['*']; let activeKey: string = menus[0]?.path || menus[0]?.name || ''; if (splat) { activeKey = splat; } const getOpenKey = () => { let openKey = ''; menus.forEach((li) => { if (li.children && li.children.length > 0) { const matchedChild = li.children.find((el) => { // exact match or path prefix match return ( el.path === activeKey || (el.path && activeKey.startsWith(`${el.path}/`)) || // if pathPrefix is set, activate when activeKey starts with the pathPrefix (el.pathPrefix && activeKey.startsWith(el.pathPrefix)) ); }); if (matchedChild) { openKey = li.path || li.name || ''; } } }); return openKey; }; const [openKey, setOpenKey] = useState(getOpenKey()); const menuClick = (evt, menu, href, isLeaf) => { evt.stopPropagation(); if (isLeaf) { if (floppyNavigation.shouldProcessLinkClick(evt)) { evt.preventDefault(); navigate(href); } } else { setOpenKey(openKey === menu.path ? '' : menu.path); } }; useEffect(() => { setOpenKey(getOpenKey()); }, [activeKey, menus]); return ( ); }; export default AccordionNav; ================================================ FILE: ui/src/components/Actions/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { memo, FC, useState, useEffect } from 'react'; import { Button, ButtonGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import { Icon } from '@/components'; import { loggedUserInfoStore } from '@/stores'; import { useToast } from '@/hooks'; import { useCaptchaPlugin } from '@/utils/pluginKit'; import { tryNormalLogged } from '@/utils/guard'; import { bookmark, postVote } from '@/services'; import * as Types from '@/common/interface'; interface Props { className?: string; source: 'question' | 'answer'; data: { id: string; votesCount: number; isLike: boolean; isHate: boolean; hideCollect?: boolean; collected: boolean; collectCount: number; username: string; }; } const Index: FC = ({ className, data, source }) => { const [votes, setVotes] = useState(0); const [like, setLike] = useState(false); const [hate, setHated] = useState(false); const [bookmarkState, setBookmark] = useState({ state: data?.collected, count: data?.collectCount, }); const { username = '' } = loggedUserInfoStore((state) => state.user); const toast = useToast(); const { t } = useTranslation(); const vCaptcha = useCaptchaPlugin('vote'); useEffect(() => { if (data) { setVotes(data.votesCount); setLike(data.isLike); setHated(data.isHate); setBookmark({ state: data?.collected, count: data?.collectCount, }); } }, []); const submitVote = (type) => { const isCancel = (type === 'up' && like) || (type === 'down' && hate); const imgCode: Types.ImgCodeReq = { captcha_id: undefined, captcha_code: undefined, }; vCaptcha?.resolveCaptchaReq?.(imgCode); postVote( { object_id: data?.id, is_cancel: isCancel, ...imgCode, }, type, ) .then(async (res) => { await vCaptcha?.close(); setVotes(res.votes); setLike(res.vote_status === 'vote_up'); setHated(res.vote_status === 'vote_down'); }) .catch((err) => { if (err?.isError) { vCaptcha?.handleCaptchaError(err.list); } const errMsg = err?.value; if (errMsg) { toast.onShow({ msg: errMsg, variant: 'danger', }); } }); }; const handleVote = (type: 'up' | 'down') => { if (!tryNormalLogged(true)) { return; } if (data.username === username) { toast.onShow({ msg: t('cannot_vote_for_self'), variant: 'danger', }); return; } if (!vCaptcha) { submitVote(type); return; } vCaptcha.check(() => { submitVote(type); }); }; const handleBookmark = () => { if (!tryNormalLogged(true)) { return; } bookmark({ group_id: '0', object_id: data?.id, bookmark: !bookmarkState.state, }).then((res) => { setBookmark({ state: !bookmarkState.state, count: res.object_collection_count, }); }); }; return (
{!data?.hideCollect && ( )}
); }; export default memo(Index); ================================================ FILE: ui/src/components/AdminSideNav/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { useEffect } from 'react'; import { NavLink } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import cloneDeep from 'lodash/cloneDeep'; import { AccordionNav, Icon } from '@/components'; import type { MenuItem } from '@/components/AccordionNav'; import { ADMIN_NAV_MENUS } from '@/common/constants'; import { useQueryPlugins } from '@/services'; import { interfaceStore } from '@/stores'; const AdminSideNav = () => { const { t } = useTranslation('translation', { keyPrefix: 'btns' }); const interfaceLang = interfaceStore((_) => _.interface.language); const { data: configurablePlugins, mutate: updateConfigurablePlugins } = useQueryPlugins({ status: 'active', have_config: true, }); const menus = cloneDeep(ADMIN_NAV_MENUS) as MenuItem[]; if (configurablePlugins && configurablePlugins.length > 0) { menus.forEach((item) => { if (item.name === 'plugins' && item.children) { item.children = [ ...item.children, ...configurablePlugins.map( (plugin): MenuItem => ({ name: plugin.slug_name, displayName: plugin.name, }), ), ]; } }); } const observePlugins = (evt) => { if (evt.data.msgType === 'refreshConfigurablePlugins') { updateConfigurablePlugins(); } }; useEffect(() => { window.addEventListener('message', observePlugins); return () => { window.removeEventListener('message', observePlugins); }; }, []); useEffect(() => { updateConfigurablePlugins(); }, [interfaceLang]); return (
{t('back_sites')}
); }; export default AdminSideNav; ================================================ FILE: ui/src/components/Avatar/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { memo, FC } from 'react'; import classNames from 'classnames'; import DefaultAvatar from '@/assets/images/default-avatar.svg'; interface IProps { /** avatar url */ avatar: string | { type: string; gravatar: string; custom: string }; /** size 48 96 128 256 */ size: string; searchStr?: string; className?: string; alt: string; } const Index: FC = ({ avatar, size, className, searchStr = '', alt, }) => { let url = ''; if (typeof avatar === 'string') { if (avatar.length > 1) { url = `${avatar}?${searchStr}${ avatar?.includes('gravatar') ? '&d=identicon' : '' }`; } } else if (avatar?.type === 'gravatar' && avatar.gravatar) { url = `${avatar.gravatar}?${searchStr}&d=identicon`; } else if (avatar?.type === 'custom' && avatar.custom) { url = `${avatar.custom}?${searchStr}`; } const roundedCls = className && className.indexOf('rounded') !== -1 ? '' : 'rounded-circle'; return ( {alt} ); }; export default memo(Index); ================================================ FILE: ui/src/components/BaseUserCard/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { memo, FC } from 'react'; import { Link } from 'react-router-dom'; import { Avatar } from '@/components'; import { formatCount } from '@/utils'; interface Props { data: any; showAvatar?: boolean; avatarSize?: string; showReputation?: boolean; avatarSearchStr?: string; className?: string; avatarClass?: string; nameMaxWidth?: string; } const Index: FC = ({ data, showAvatar = true, avatarClass = '', avatarSize = '24px', className = 'small', avatarSearchStr = 's=48', showReputation = true, nameMaxWidth = '300px', }) => { return (
{data?.status !== 'deleted' ? ( { e.stopPropagation(); }} className="d-flex align-items-center"> {showAvatar && ( )} {data?.display_name} ) : ( <> {showAvatar && ( )} {data?.display_name} )} {showReputation && ( {formatCount(data?.rank)} )}
); }; export default memo(Index); ================================================ FILE: ui/src/components/BrandUpload/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { FC } from 'react'; import { ButtonGroup, Button } from 'react-bootstrap'; import classNames from 'classnames'; import { Icon, UploadImg } from '@/components'; import { UploadType } from '@/common/interface'; interface Props { type: UploadType; value: string; onChange: (value: string) => void; acceptType?: string; readOnly?: boolean; imgClassNames?: classNames.Argument; } const Index: FC = ({ type = 'post', value, onChange, acceptType, readOnly = false, imgClassNames = '', }) => { const onUpload = (imgPath: string) => { onChange(imgPath); }; const onRemove = () => { onChange(''); }; return (
); }; export default Index; ================================================ FILE: ui/src/components/BubbleAi/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { FC, useEffect, useState, useRef } from 'react'; import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { marked } from 'marked'; import copy from 'copy-to-clipboard'; import { voteConversation } from '@/services'; import { Icon, htmlRender } from '@/components'; interface IProps { canType?: boolean; chatId: string; isLast: boolean; isCompleted: boolean; content: string; minHeight?: number; actionData: { helpful: number; unhelpful: number; }; } const BubbleAi: FC = ({ canType = false, isLast, isCompleted, content, chatId = '', actionData, minHeight = 0, }) => { const { t } = useTranslation('translation', { keyPrefix: 'ai_assistant' }); const [displayContent, setDisplayContent] = useState(''); const [copyText, setCopyText] = useState(t('copy')); const [isHelpful, setIsHelpful] = useState(false); const [isUnhelpful, setIsUnhelpful] = useState(false); const [canShowAction, setCanShowAction] = useState(false); const typewriterRef = useRef<{ timer: NodeJS.Timeout | null; index: number; isTyping: boolean; }>({ timer: null, index: 0, isTyping: false, }); const fmtContainer = useRef(null); // add ref for ScrollIntoView const containerRef = useRef(null); const handleCopy = () => { const res = copy(displayContent); if (res) { setCopyText(t('copied', { keyPrefix: 'messages' })); setTimeout(() => { setCopyText(t('copy')); }, 1200); } }; const handleVote = (voteType: 'helpful' | 'unhelpful') => { const isCancel = (voteType === 'helpful' && isHelpful) || (voteType === 'unhelpful' && isUnhelpful); voteConversation({ chat_completion_id: chatId, cancel: isCancel, vote_type: voteType, }).then(() => { setIsHelpful(voteType === 'helpful' && !isCancel); setIsUnhelpful(voteType === 'unhelpful' && !isCancel); }); }; useEffect(() => { if ((!canType || !isLast) && content) { // 如果不是最后一个消息,直接返回,不进行打字效果 if (typewriterRef.current.timer) { clearInterval(typewriterRef.current.timer); typewriterRef.current.timer = null; } setDisplayContent(content); setCanShowAction(true); typewriterRef.current.timer = null; typewriterRef.current.isTyping = false; return; } // 当内容变化时,清理之前的计时器 if (typewriterRef.current.timer) { clearInterval(typewriterRef.current.timer); typewriterRef.current.timer = null; } // 如果内容为空,则直接返回 if (!content) { setDisplayContent(''); return; } // 如果内容比当前显示的短,则重置 if (content.length < displayContent.length) { setDisplayContent(''); typewriterRef.current.index = 0; } // 如果内容与显示内容相同,不需要做任何事 if (content === displayContent) { return; } typewriterRef.current.isTyping = true; // start typing animation typewriterRef.current.timer = setInterval(() => { const currentIndex = typewriterRef.current.index; if (currentIndex < content.length) { const remainingLength = content.length - currentIndex; const baseRandomNum = Math.floor(Math.random() * 3) + 2; let randomNum = Math.min(baseRandomNum, remainingLength); // 简单的单词边界检查(可选) const nextChar = content[currentIndex + randomNum]; const prevChar = content[currentIndex + randomNum - 1]; // 如果下一个字符是字母,当前字符也是字母,尝试调整到空格处 if ( nextChar && /[a-zA-Z]/.test(nextChar) && /[a-zA-Z]/.test(prevChar) ) { // 向前找1-2个字符,看看有没有空格 for ( let i = 1; i <= 2 && currentIndex + randomNum - i > currentIndex; i += 1 ) { if (content[currentIndex + randomNum - i] === ' ') { randomNum = randomNum - i + 1; break; } } // 向后找1-2个字符,看看有没有空格 for ( let i = 1; i <= 2 && currentIndex + randomNum + i < content.length; i += 1 ) { if (content[currentIndex + randomNum + i] === ' ') { randomNum = randomNum + i + 1; break; } } } const nextIndex = currentIndex + randomNum; const newContent = content.substring(0, nextIndex); setDisplayContent(newContent); typewriterRef.current.index = nextIndex; setCanShowAction(false); } else { clearInterval(typewriterRef.current.timer as NodeJS.Timeout); typewriterRef.current.timer = null; typewriterRef.current.isTyping = false; setCanShowAction(false); } }, 30); // eslint-disable-next-line consistent-return return () => { if (typewriterRef.current.timer) { clearInterval(typewriterRef.current.timer); typewriterRef.current.timer = null; } }; }, [content, isCompleted]); useEffect(() => { setIsHelpful(actionData.helpful > 0); setIsUnhelpful(actionData.unhelpful > 0); }, [actionData]); useEffect(() => { if (fmtContainer.current && isCompleted) { htmlRender(fmtContainer.current, { copySuccessText: t('copied', { keyPrefix: 'messages' }), copyText: t('copy', { keyPrefix: 'messages' }), }); const links = fmtContainer.current.querySelectorAll('a'); links.forEach((link) => { link.setAttribute('target', '_blank'); }); setCanShowAction(true); } }, [isCompleted, fmtContainer.current]); return (
{canShowAction && (
)}
); }; export default BubbleAi; ================================================ FILE: ui/src/components/BubbleUser/index.scss ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .bubble-user-wrap { scroll-margin-top: 88px; } .bubble-user { background-color: var(--bs-gray-200); } ================================================ FILE: ui/src/components/BubbleUser/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { FC } from 'react'; import './index.scss'; interface BubbleUserProps { content?: string; } const BubbleUser: FC = ({ content }) => { return (
{content}
); }; export default BubbleUser; ================================================ FILE: ui/src/components/CardBadge/index.scss ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .badge-card { .label { position: absolute; top: 1rem; right: 1rem; } } ================================================ FILE: ui/src/components/CardBadge/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { useTranslation } from 'react-i18next'; import { FC } from 'react'; import { Card, Badge } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import classnames from 'classnames'; import { Icon } from '@/components'; import * as Type from '@/common/interface'; import { formatCount } from '@/utils'; import './index.scss'; interface IProps { data: Type.BadgeListItem; showAwardedCount?: boolean; urlSearchParams?: string; badgePillType?: 'earned' | 'count'; } const Index: FC = ({ data, badgePillType = 'earned', showAwardedCount = false, urlSearchParams, }) => { const { t } = useTranslation('translation', { keyPrefix: 'badges' }); return ( {Number(data?.earned_count) > 0 && badgePillType === 'earned' && ( {`${t('earned')}${ Number(data?.earned_count) > 1 ? ` ×${data.earned_count}` : '' }`} )} {badgePillType === 'count' && Number(data?.earned_count) > 1 && ( ×{data.earned_count} )} {data.icon.startsWith('http') ? ( {data.name} ) : ( )}
{data.name}
{showAwardedCount && (
{t('×_awarded', { number: formatCount(data.award_count) })}
)}
); }; export default Index; ================================================ FILE: ui/src/components/Comment/components/ActionBar/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { memo } from 'react'; import { Button, Dropdown } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { Icon, FormatTime } from '@/components'; const ActionBar = ({ nickName, username, createdAt, isVote, voteCount = 0, memberActions, onReply, onVote, onAction, userStatus = '', }) => { const { t } = useTranslation('translation', { keyPrefix: 'comment' }); return (
{userStatus !== 'deleted' ? ( {nickName} ) : ( {nickName} )}
{memberActions.map((action, index) => { return ( ); })}
{memberActions.map((action) => { return ( onAction(action)}> {action.name} ); })}
); }; export default memo(ActionBar); ================================================ FILE: ui/src/components/Comment/components/Form/index.tsx ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { useState, useEffect, memo } from 'react'; import { Button, Form } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import { TextArea, Mentions } from '@/components'; import { usePageUsers, usePromptWithUnload } from '@/hooks'; import { parseEditMentionUser } from '@/utils'; const Index = ({ className = '', value: initialValue = '', onSendReply, type = '', onCancel, mode, }) => { const [value, setValue] = useState(''); const [immData, setImmData] = useState(''); const pageUsers = usePageUsers(); const { t } = useTranslation('translation', { keyPrefix: 'comment' }); const [validationErrorMsg, setValidationErrorMsg] = useState(''); usePromptWithUnload({ when: type === 'edit' ? immData !== value : Boolean(value), }); useEffect(() => { if (!initialValue) { return; } setImmData(initialValue); setValue(initialValue); }, [initialValue]); const handleChange = (e) => { setValue(e.target.value); }; const handleSelected = (val) => { setValue(val); }; const handleSendReply = () => { onSendReply(value).catch((ex) => { if (ex.isError) { setValidationErrorMsg(ex.msg); } }); }; return (