This commit is contained in:
おさむのひと 2024-12-21 10:06:18 +09:00
parent 7fcf8af096
commit 2b32b16355
6 changed files with 194 additions and 84 deletions

47
locales/index.d.ts vendored
View file

@ -1670,10 +1670,6 @@ export interface Locale extends ILocale {
* Captchaを使用すると干渉を起こす可能性がありますCaptchaを無効にしますかCaptchaを有効化したままにすることも可能です * Captchaを使用すると干渉を起こす可能性がありますCaptchaを無効にしますかCaptchaを有効化したままにすることも可能です
*/ */
"avoidMultiCaptchaConfirm": string; "avoidMultiCaptchaConfirm": string;
/**
* "{testSiteKey}"
*/
"testSiteKeyMessage": ParameterizedString<"testSiteKey">;
/** /**
* *
*/ */
@ -10605,6 +10601,49 @@ export interface Locale extends ILocale {
*/ */
"sent": string; "sent": string;
}; };
"_captcha": {
/**
* CAPTCHAを通過してください
*/
"verify": string;
/**
*
*
*/
"testSiteKeyMessage": string;
"_error": {
"_requestFailed": {
/**
* CAPTCHAのリクエストに失敗しました
*/
"title": string;
/**
*
*/
"text": string;
};
"_verificationFailed": {
/**
* CAPTCHAの検証に失敗しました
*/
"title": string;
/**
*
*/
"text": string;
};
"_unknown": {
/**
* CAPTCHAエラー
*/
"title": string;
/**
*
*/
"text": string;
};
};
};
} }
declare const locales: { declare const locales: {
[lang: string]: Locale; [lang: string]: Locale;

View file

@ -413,7 +413,6 @@ enableTurnstile: "Turnstileを有効にする"
turnstileSiteKey: "サイトキー" turnstileSiteKey: "サイトキー"
turnstileSecretKey: "シークレットキー" turnstileSecretKey: "シークレットキー"
avoidMultiCaptchaConfirm: "複数のCaptchaを使用すると干渉を起こす可能性があります。他のCaptchaを無効にしますかキャンセルして複数のCaptchaを有効化したままにすることも可能です。" avoidMultiCaptchaConfirm: "複数のCaptchaを使用すると干渉を起こす可能性があります。他のCaptchaを無効にしますかキャンセルして複数のCaptchaを有効化したままにすることも可能です。"
testSiteKeyMessage: "サイトキーに\"{testSiteKey}\"と入力することでプレビューを確認できます。"
antennas: "アンテナ" antennas: "アンテナ"
manageAntennas: "アンテナの管理" manageAntennas: "アンテナの管理"
name: "名前" name: "名前"
@ -2827,3 +2826,17 @@ _selfXssPrevention:
_followRequest: _followRequest:
recieved: "受け取った申請" recieved: "受け取った申請"
sent: "送った申請" sent: "送った申請"
_captcha:
verify: "CAPTCHAを通過してください"
testSiteKeyMessage: "サイトキーとシークレットキーにテスト用の値を入力することでプレビューを確認できます。\n詳細は下記ページをご確認ください。"
_error:
_requestFailed:
title: "CAPTCHAのリクエストに失敗しました"
text: "しばらく後に実行するか、設定をもう一度ご確認ください。"
_verificationFailed:
title: "CAPTCHAの検証に失敗しました"
text: "設定が正しいかどうかもう一度確認ください。"
_unknown:
title: "CAPTCHAエラー"
text: "想定外のエラーが発生しました。"

View file

@ -21,37 +21,37 @@ export const meta = {
invalidProvider: { invalidProvider: {
message: 'Invalid provider.', message: 'Invalid provider.',
code: 'INVALID_PROVIDER', code: 'INVALID_PROVIDER',
id: '14BF7AE1-80CC-4363-ACB2-4FD61D086AF0', id: '14bf7ae1-80cc-4363-acb2-4fd61d086af0',
httpStatusCode: 400, httpStatusCode: 400,
}, },
invalidParameters: { invalidParameters: {
message: 'Invalid parameters.', message: 'Invalid parameters.',
code: 'INVALID_PARAMETERS', code: 'INVALID_PARAMETERS',
id: '26654194-410E-44E2-B42E-460FF6F92476', id: '26654194-410e-44e2-b42e-460ff6f92476',
httpStatusCode: 400, httpStatusCode: 400,
}, },
noResponseProvided: { noResponseProvided: {
message: 'No response provided.', message: 'No response provided.',
code: 'NO_RESPONSE_PROVIDED', code: 'NO_RESPONSE_PROVIDED',
id: '40ACBBA8-0937-41FB-BB3F-474514D40AFE', id: '40acbba8-0937-41fb-bb3f-474514d40afe',
httpStatusCode: 400, httpStatusCode: 400,
}, },
requestFailed: { requestFailed: {
message: 'Request failed.', message: 'Request failed.',
code: 'REQUEST_FAILED', code: 'REQUEST_FAILED',
id: '0F4FE2F1-2C15-4D6E-B714-EFBFCDE231CD', id: '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd',
httpStatusCode: 500, httpStatusCode: 500,
}, },
verificationFailed: { verificationFailed: {
message: 'Verification failed.', message: 'Verification failed.',
code: 'VERIFICATION_FAILED', code: 'VERIFICATION_FAILED',
id: 'C41C067F-24F3-4150-84B2-B5A3AE8C2214', id: 'c41c067f-24f3-4150-84b2-b5a3ae8c2214',
httpStatusCode: 400, httpStatusCode: 400,
}, },
unknown: { unknown: {
message: 'unknown', message: 'unknown',
code: 'UNKNOWN', code: 'UNKNOWN',
id: 'F868D509-E257-42A9-99C1-42614B031A97', id: 'f868d509-e257-42a9-99c1-42614b031a97',
httpStatusCode: 500, httpStatusCode: 500,
}, },
}, },

View file

@ -30,6 +30,9 @@ import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmount
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
// APIs provided by Captcha services // APIs provided by Captcha services
// see: https://docs.hcaptcha.com/configuration/#javascript-api
// see: https://developers.google.com/recaptcha/docs/display?hl=ja
// see: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget
export type Captcha = { export type Captcha = {
render(container: string | Node, options: { render(container: string | Node, options: {
readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown; readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown;
@ -53,6 +56,7 @@ declare global {
const props = defineProps<{ const props = defineProps<{
provider: CaptchaProvider; provider: CaptchaProvider;
sitekey: string | null; // null will show error on request sitekey: string | null; // null will show error on request
secretKey?: string | null;
instanceUrl?: string | null; instanceUrl?: string | null;
modelValue?: string | null; modelValue?: string | null;
}>(); }>();
@ -64,7 +68,7 @@ const emit = defineEmits<{
const available = ref(false); const available = ref(false);
const captchaEl = shallowRef<HTMLDivElement | undefined>(); const captchaEl = shallowRef<HTMLDivElement | undefined>();
const captchaWidgetId = ref<string | undefined>(undefined);
const testcaptchaInput = ref(''); const testcaptchaInput = ref('');
const testcaptchaPassed = ref(false); const testcaptchaPassed = ref(false);
@ -94,10 +98,11 @@ const scriptId = computed(() => `script-${props.provider}`);
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha); const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
watch(() => [props.instanceUrl, props.sitekey], async () => { watch(() => [props.instanceUrl, props.sitekey, props.secretKey], async () => {
// //
if (available.value) { if (available.value) {
callback(undefined); callback(undefined);
clearWidget();
await requestRender(); await requestRender();
} }
}); });
@ -114,9 +119,9 @@ if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha')
} }
function reset() { function reset() {
if (captcha.value.reset) { if (captcha.value.reset && captchaWidgetId.value !== undefined) {
try { try {
captcha.value.reset(); captcha.value.reset(captchaWidgetId.value);
} catch (error: unknown) { } catch (error: unknown) {
// ignore // ignore
if (_DEV_) console.warn(error); if (_DEV_) console.warn(error);
@ -126,28 +131,33 @@ function reset() {
testcaptchaInput.value = ''; testcaptchaInput.value = '';
} }
function remove() {
if (captcha.value.remove && captchaWidgetId.value) {
try {
if (_DEV_) console.log('remove', props.provider, captchaWidgetId.value);
captcha.value.remove(captchaWidgetId.value);
} catch (error: unknown) {
// ignore
if (_DEV_) console.warn(error);
}
}
}
async function requestRender() { async function requestRender() {
if (captcha.value.render && captchaEl.value instanceof Element) { if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) {
// // reCAPTCHAcaptchaEldiv.
reset(); // divrenderreCAPTCHA
captchaEl.value.innerHTML = ''; const elem = document.createElement('div');
captchaEl.value.appendChild(elem);
if (props.sitekey && props.sitekey.length > 0) { captchaWidgetId.value = captcha.value.render(elem, {
captcha.value.render(captchaEl.value, { sitekey: props.sitekey,
sitekey: props.sitekey, theme: defaultStore.state.darkMode ? 'dark' : 'light',
theme: defaultStore.state.darkMode ? 'dark' : 'light', callback: callback,
callback: callback, 'expired-callback': () => callback(undefined),
'expired-callback': () => callback(undefined), 'error-callback': () => callback(undefined),
'error-callback': () => callback(undefined), });
});
}
} else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) { } else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
//
const container = document.getElementById('mcaptcha__widget-container');
if (container) {
container.innerHTML = '';
}
const { default: Widget } = await import('@mcaptcha/vanilla-glue'); const { default: Widget } = await import('@mcaptcha/vanilla-glue');
new Widget({ new Widget({
siteKey: { siteKey: {
@ -160,6 +170,23 @@ async function requestRender() {
} }
} }
function clearWidget() {
if (props.provider === 'mcaptcha') {
const container = document.getElementById('mcaptcha__widget-container');
if (container) {
container.innerHTML = '';
}
} else {
reset();
remove();
if (captchaEl.value) {
//
captchaEl.value.innerHTML = '';
}
}
}
function callback(response?: string) { function callback(response?: string) {
emit('update:modelValue', typeof response === 'string' ? response : null); emit('update:modelValue', typeof response === 'string' ? response : null);
} }
@ -192,7 +219,7 @@ onUnmounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
reset(); clearWidget();
}); });
defineExpose({ defineExpose({

View file

@ -11,6 +11,7 @@ import * as Misskey from 'misskey-js';
import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/scripts/form.js'; import type { Form, GetFormResultType } from '@/scripts/form.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -28,15 +29,15 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
import { focusParent } from '@/scripts/focus.js'; import { focusParent } from '@/scripts/focus.js';
import type { PostFormProps } from '@/types/post-form.js';
export const openingWindowsCount = ref(0); export const openingWindowsCount = ref(0);
export type ApiWithDialogCustomErrors = Record<string, { title?: string; text: string; }>;
export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>( export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
endpoint: E, endpoint: E,
data: P, data: P,
token?: string | null | undefined, token?: string | null | undefined,
customErrors?: Record<string, { title?: string; text: string; }>, customErrors?: ApiWithDialogCustomErrors,
) => { ) => {
const promise = misskeyApi(endpoint, data, token); const promise = misskeyApi(endpoint, data, token);
promiseDialog(promise, null, async (err) => { promiseDialog(promise, null, async (err) => {

View file

@ -28,21 +28,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRadios> </MkRadios>
<template v-if="botProtectionForm.state.provider === 'hcaptcha'"> <template v-if="botProtectionForm.state.provider === 'hcaptcha'">
<MkInput v-model="botProtectionForm.state.hcaptchaSiteKey"> <MkInput v-model="botProtectionForm.state.hcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template> <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</MkInput> </MkInput>
<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey"> <MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template> <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</MkInput> </MkInput>
<FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey"> <FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
<template #label>{{ i18n.ts.preview }}</template> <template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha v-model="captchaResult" provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey"/> <MkCaptcha
v-model="captchaResult"
provider="hcaptcha"
:sitekey="botProtectionForm.state.hcaptchaSiteKey"
:secretKey="botProtectionForm.state.hcaptchaSecretKey"
/>
</FormSlot> </FormSlot>
<MkInfo> <MkInfo>
<div :class="$style.captchaInfoMsg"> <div :class="$style.captchaInfoMsg">
<div>{{ i18n.tsx.testSiteKeyMessage({ testSiteKey: '10000000-ffff-ffff-ffff-000000000001' }) }}</div> <div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
<div> <div>
<span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a> <span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a>
</div> </div>
@ -51,46 +56,51 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<template v-else-if="botProtectionForm.state.provider === 'mcaptcha'"> <template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
<MkInput v-model="botProtectionForm.state.mcaptchaSiteKey"> <MkInput v-model="botProtectionForm.state.mcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template> <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
</MkInput> </MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey"> <MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template> <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
</MkInput> </MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl"> <MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce>
<template #prefix><i class="ti ti-link"></i></template> <template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template> <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
</MkInput> </MkInput>
<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl"> <FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
<template #label>{{ i18n.ts.preview }}</template> <template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha <MkCaptcha
v-model="captchaResult" provider="mcaptcha" :sitekey="botProtectionForm.state.mcaptchaSiteKey" v-model="captchaResult"
provider="mcaptcha"
:sitekey="botProtectionForm.state.mcaptchaSiteKey"
:secretKey="botProtectionForm.state.mcaptchaSecretKey"
:instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl" :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"
/> />
</FormSlot> </FormSlot>
</template> </template>
<template v-else-if="botProtectionForm.state.provider === 'recaptcha'"> <template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
<MkInput v-model="botProtectionForm.state.recaptchaSiteKey"> <MkInput v-model="botProtectionForm.state.recaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template> <template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
</MkInput> </MkInput>
<MkInput v-model="botProtectionForm.state.recaptchaSecretKey"> <MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template> <template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
</MkInput> </MkInput>
<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey"> <FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
<template #label>{{ i18n.ts.preview }}</template> <template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha <MkCaptcha
v-model="captchaResult" provider="recaptcha" v-model="captchaResult"
provider="recaptcha"
:sitekey="botProtectionForm.state.recaptchaSiteKey" :sitekey="botProtectionForm.state.recaptchaSiteKey"
:secretKey="botProtectionForm.state.recaptchaSecretKey"
/> />
</FormSlot> </FormSlot>
<MkInfo> <MkInfo>
<div :class="$style.captchaInfoMsg"> <div :class="$style.captchaInfoMsg">
<div>{{ i18n.tsx.testSiteKeyMessage({ testSiteKey: '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI' }) }}</div> <div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
<div> <div>
<span>ref: </span> <span>ref: </span>
<a <a
@ -103,27 +113,29 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<template v-else-if="botProtectionForm.state.provider === 'turnstile'"> <template v-else-if="botProtectionForm.state.provider === 'turnstile'">
<MkInput v-model="botProtectionForm.state.turnstileSiteKey"> <MkInput v-model="botProtectionForm.state.turnstileSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSiteKey }}</template> <template #label>{{ i18n.ts.turnstileSiteKey }}</template>
</MkInput> </MkInput>
<MkInput v-model="botProtectionForm.state.turnstileSecretKey"> <MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSecretKey }}</template> <template #label>{{ i18n.ts.turnstileSecretKey }}</template>
</MkInput> </MkInput>
<FormSlot v-if="botProtectionForm.state.turnstileSiteKey"> <FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
<template #label>{{ i18n.ts.preview }}</template> <template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha <MkCaptcha
v-model="captchaResult" provider="turnstile" v-model="captchaResult"
provider="turnstile"
:sitekey="botProtectionForm.state.turnstileSiteKey" :sitekey="botProtectionForm.state.turnstileSiteKey"
:secretKey="botProtectionForm.state.turnstileSecretKey"
/> />
</FormSlot> </FormSlot>
<MkInfo> <MkInfo>
<div :class="$style.captchaInfoMsg"> <div :class="$style.captchaInfoMsg">
<div :class="$style.noSpace"> <div>
{{ i18n.tsx.testSiteKeyMessage({ testSiteKey: '1x00000000000000000000AA' }) }} {{ i18n.ts._captcha.testSiteKeyMessage }}
</div> </div>
<div :class="$style.noSpace"> <div>
<span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a> <span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a>
</div> </div>
</div> </div>
@ -133,7 +145,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'"> <template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo> <MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
<FormSlot> <FormSlot>
<template #label>{{ i18n.ts.preview }}</template> <template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/> <MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
</FormSlot> </FormSlot>
</template> </template>
@ -155,9 +167,28 @@ import { useForm } from '@/scripts/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue'; import MkFormFooter from '@/components/MkFormFooter.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { ApiWithDialogCustomErrors } from '@/os.js';
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue')); const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
const errorHandler: ApiWithDialogCustomErrors = {
//
'0f4fe2f1-2c15-4d6e-b714-efbfcde231cd': {
title: i18n.ts._captcha._error._requestFailed.title,
text: i18n.ts._captcha._error._requestFailed.text,
},
//
'c41c067f-24f3-4150-84b2-b5a3ae8c2214': {
title: i18n.ts._captcha._error._verificationFailed.title,
text: i18n.ts._captcha._error._verificationFailed.text,
},
//
'f868d509-e257-42a9-99c1-42614b031a97': {
title: i18n.ts._captcha._error._unknown.title,
text: i18n.ts._captcha._error._unknown.text,
},
};
const captchaResult = ref<string | null>(null); const captchaResult = ref<string | null>(null);
const meta = await misskeyApi('admin/captcha/current'); const meta = await misskeyApi('admin/captcha/current');
@ -174,32 +205,33 @@ const botProtectionForm = useForm({
turnstileSecretKey: meta.turnstile.secretKey, turnstileSecretKey: meta.turnstile.secretKey,
}, async (state) => { }, async (state) => {
const provider = state.provider; const provider = state.provider;
const sitekey = provider === 'hcaptcha'
? state.hcaptchaSiteKey
: provider === 'mcaptcha'
? state.mcaptchaSiteKey
: provider === 'recaptcha'
? state.recaptchaSiteKey
: provider === 'turnstile'
? state.turnstileSiteKey
: null;
const secret = provider === 'hcaptcha'
? state.hcaptchaSecretKey
: provider === 'mcaptcha'
? state.mcaptchaSecretKey
: provider === 'recaptcha'
? state.recaptchaSecretKey
: provider === 'turnstile'
? state.turnstileSecretKey
: null;
if (provider === 'none') { if (provider === 'none') {
await os.apiWithDialog( await os.apiWithDialog(
'admin/captcha/save', 'admin/captcha/save',
{ provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'] }, { provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'] },
undefined,
errorHandler,
); );
} else { } else {
const sitekey = provider === 'hcaptcha'
? state.hcaptchaSiteKey
: provider === 'mcaptcha'
? state.mcaptchaSiteKey
: provider === 'recaptcha'
? state.recaptchaSiteKey
: provider === 'turnstile'
? state.turnstileSiteKey
: null;
const secret = provider === 'hcaptcha'
? state.hcaptchaSecretKey
: provider === 'mcaptcha'
? state.mcaptchaSecretKey
: provider === 'recaptcha'
? state.recaptchaSecretKey
: provider === 'turnstile'
? state.turnstileSecretKey
: null;
await os.apiWithDialog( await os.apiWithDialog(
'admin/captcha/save', 'admin/captcha/save',
{ {
@ -209,6 +241,8 @@ const botProtectionForm = useForm({
instanceUrl: state.mcaptchaInstanceUrl, instanceUrl: state.mcaptchaInstanceUrl,
captchaResult: captchaResult.value, captchaResult: captchaResult.value,
}, },
undefined,
errorHandler,
); );
} }
@ -236,8 +270,4 @@ const canSaving = computed((): boolean => {
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
.noSpace {
white-space-collapse: discard;
}
</style> </style>