mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-23 00:29:22 +09:00
Compare commits
4 Commits
7fcf8af096
...
a060269419
Author | SHA1 | Date | |
---|---|---|---|
|
a060269419 | ||
|
6e8b672275 | ||
|
2b32b16355 | ||
|
f123be38b9 |
@ -6,6 +6,7 @@
|
||||
### Client
|
||||
- Enhance: PC画面でチャンネルが複数列で表示されるように
|
||||
(Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
|
||||
- Enhance: 照会に失敗した場合、その理由を表示するように
|
||||
- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
|
||||
- Fix: サーバー情報メニューに区切り線が不足していたのを修正
|
||||
- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正
|
||||
|
106
locales/index.d.ts
vendored
106
locales/index.d.ts
vendored
@ -1670,10 +1670,6 @@ export interface Locale extends ILocale {
|
||||
* 複数のCaptchaを使用すると干渉を起こす可能性があります。他のCaptchaを無効にしますか?キャンセルして複数のCaptchaを有効化したままにすることも可能です。
|
||||
*/
|
||||
"avoidMultiCaptchaConfirm": string;
|
||||
/**
|
||||
* サイトキーに"{testSiteKey}"と入力することでプレビューを確認できます。
|
||||
*/
|
||||
"testSiteKeyMessage": ParameterizedString<"testSiteKey">;
|
||||
/**
|
||||
* アンテナ
|
||||
*/
|
||||
@ -10605,6 +10601,108 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"sent": string;
|
||||
};
|
||||
"_remoteLookupErrors": {
|
||||
"_federationNotAllowed": {
|
||||
/**
|
||||
* このサーバーとは通信できません
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* このサーバーとの通信が無効化されているか、このサーバーをブロックしている・ブロックされている可能性があります。
|
||||
* サーバー管理者にお問い合わせください。
|
||||
*/
|
||||
"description": string;
|
||||
};
|
||||
"_uriInvalid": {
|
||||
/**
|
||||
* URIが不正です
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* 入力されたURIに問題があります。URIに使用できない文字を入力していないか確認してください。
|
||||
*/
|
||||
"description": string;
|
||||
};
|
||||
"_requestFailed": {
|
||||
/**
|
||||
* リクエストに失敗しました
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。
|
||||
*/
|
||||
"description": string;
|
||||
};
|
||||
"_responseInvalid": {
|
||||
/**
|
||||
* レスポンスが不正です
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* このサーバーと通信することはできましたが、得られたデータが不正なものでした。
|
||||
*/
|
||||
"description": string;
|
||||
};
|
||||
"_responseInvalidIdHostNotMatch": {
|
||||
/**
|
||||
* 入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。
|
||||
*/
|
||||
"description": string;
|
||||
};
|
||||
"_noSuchObject": {
|
||||
/**
|
||||
* 見つかりません
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* 要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。
|
||||
*/
|
||||
"description": 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: {
|
||||
[lang: string]: Locale;
|
||||
|
@ -413,7 +413,6 @@ enableTurnstile: "Turnstileを有効にする"
|
||||
turnstileSiteKey: "サイトキー"
|
||||
turnstileSecretKey: "シークレットキー"
|
||||
avoidMultiCaptchaConfirm: "複数のCaptchaを使用すると干渉を起こす可能性があります。他のCaptchaを無効にしますか?キャンセルして複数のCaptchaを有効化したままにすることも可能です。"
|
||||
testSiteKeyMessage: "サイトキーに\"{testSiteKey}\"と入力することでプレビューを確認できます。"
|
||||
antennas: "アンテナ"
|
||||
manageAntennas: "アンテナの管理"
|
||||
name: "名前"
|
||||
@ -2827,3 +2826,36 @@ _selfXssPrevention:
|
||||
_followRequest:
|
||||
recieved: "受け取った申請"
|
||||
sent: "送った申請"
|
||||
|
||||
_remoteLookupErrors:
|
||||
_federationNotAllowed:
|
||||
title: "このサーバーとは通信できません"
|
||||
description: "このサーバーとの通信が無効化されているか、このサーバーをブロックしている・ブロックされている可能性があります。\nサーバー管理者にお問い合わせください。"
|
||||
_uriInvalid:
|
||||
title: "URIが不正です"
|
||||
description: "入力されたURIに問題があります。URIに使用できない文字を入力していないか確認してください。"
|
||||
_requestFailed:
|
||||
title: "リクエストに失敗しました"
|
||||
description: "このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。"
|
||||
_responseInvalid:
|
||||
title: "レスポンスが不正です"
|
||||
description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。"
|
||||
_responseInvalidIdHostNotMatch:
|
||||
description: "入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。"
|
||||
_noSuchObject:
|
||||
title: "見つかりません"
|
||||
description: "要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。"
|
||||
|
||||
_captcha:
|
||||
verify: "CAPTCHAを通過してください"
|
||||
testSiteKeyMessage: "サイトキーとシークレットキーにテスト用の値を入力することでプレビューを確認できます。\n詳細は下記ページをご確認ください。"
|
||||
_error:
|
||||
_requestFailed:
|
||||
title: "CAPTCHAのリクエストに失敗しました"
|
||||
text: "しばらく後に実行するか、設定をもう一度ご確認ください。"
|
||||
_verificationFailed:
|
||||
title: "CAPTCHAの検証に失敗しました"
|
||||
text: "設定が正しいかどうかもう一度確認ください。"
|
||||
_unknown:
|
||||
title: "CAPTCHAエラー"
|
||||
text: "想定外のエラーが発生しました。"
|
||||
|
@ -20,6 +20,7 @@ import { ApDbResolverService } from './ApDbResolverService.js';
|
||||
import { ApRendererService } from './ApRendererService.js';
|
||||
import { ApRequestService } from './ApRequestService.js';
|
||||
import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
export class Resolver {
|
||||
private history: Set<string>;
|
||||
@ -66,7 +67,7 @@ export class Resolver {
|
||||
if (isCollectionOrOrderedCollection(collection)) {
|
||||
return collection;
|
||||
} else {
|
||||
throw new Error(`unrecognized collection type: ${collection.type}`);
|
||||
throw new IdentifiableError('f100eccf-f347-43fb-9b45-96a0831fb635', `unrecognized collection type: ${collection.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,15 +81,15 @@ export class Resolver {
|
||||
// URLs with fragment parts cannot be resolved correctly because
|
||||
// the fragment part does not get transmitted over HTTP(S).
|
||||
// Avoid strange behaviour by not trying to resolve these at all.
|
||||
throw new Error(`cannot resolve URL with fragment: ${value}`);
|
||||
throw new IdentifiableError('b94fd5b1-0e3b-4678-9df2-dad4cd515ab2', `cannot resolve URL with fragment: ${value}`);
|
||||
}
|
||||
|
||||
if (this.history.has(value)) {
|
||||
throw new Error('cannot resolve already resolved one');
|
||||
throw new IdentifiableError('0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5', 'cannot resolve already resolved one');
|
||||
}
|
||||
|
||||
if (this.history.size > this.recursionLimit) {
|
||||
throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
|
||||
throw new IdentifiableError('d592da9f-822f-4d91-83d7-4ceefabcf3d2', `hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
|
||||
}
|
||||
|
||||
this.history.add(value);
|
||||
@ -99,7 +100,7 @@ export class Resolver {
|
||||
}
|
||||
|
||||
if (!this.utilityService.isFederationAllowedHost(host)) {
|
||||
throw new Error('Instance is blocked');
|
||||
throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', 'Instance is blocked');
|
||||
}
|
||||
|
||||
if (this.config.signToActivityPubGet && !this.user) {
|
||||
@ -115,7 +116,7 @@ export class Resolver {
|
||||
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
|
||||
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
||||
) {
|
||||
throw new Error('invalid response');
|
||||
throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', 'invalid response');
|
||||
}
|
||||
|
||||
// HttpRequestService / ApRequestService have already checked that
|
||||
@ -123,11 +124,11 @@ export class Resolver {
|
||||
// object after redirects; here we double-check that no redirects
|
||||
// bounced between hosts
|
||||
if (object.id == null) {
|
||||
throw new Error('invalid AP object: missing id');
|
||||
throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', 'invalid AP object: missing id');
|
||||
}
|
||||
|
||||
if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) {
|
||||
throw new Error(`invalid AP object ${value}: id ${object.id} has different host`);
|
||||
throw new IdentifiableError('fd93c2fa-69a8-440f-880b-bf178e0ec877', `invalid AP object ${value}: id ${object.id} has different host`);
|
||||
}
|
||||
|
||||
return object;
|
||||
@ -136,7 +137,7 @@ export class Resolver {
|
||||
@bindThis
|
||||
private resolveLocal(url: string): Promise<IObject> {
|
||||
const parsed = this.apDbResolverService.parseUri(url);
|
||||
if (!parsed.local) throw new Error('resolveLocal: not local');
|
||||
if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', 'resolveLocal: not local');
|
||||
|
||||
switch (parsed.type) {
|
||||
case 'notes':
|
||||
@ -165,7 +166,7 @@ export class Resolver {
|
||||
case 'follows':
|
||||
return this.followRequestsRepository.findOneBy({ id: parsed.id })
|
||||
.then(async followRequest => {
|
||||
if (followRequest == null) throw new Error('resolveLocal: invalid follow request ID');
|
||||
if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', 'resolveLocal: invalid follow request ID');
|
||||
const [follower, followee] = await Promise.all([
|
||||
this.usersRepository.findOneBy({
|
||||
id: followRequest.followerId,
|
||||
@ -177,12 +178,12 @@ export class Resolver {
|
||||
}),
|
||||
]);
|
||||
if (follower == null || followee == null) {
|
||||
throw new Error('resolveLocal: follower or followee does not exist');
|
||||
throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', 'resolveLocal: follower or followee does not exist');
|
||||
}
|
||||
return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url));
|
||||
});
|
||||
default:
|
||||
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
|
||||
throw new IdentifiableError('7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0', `resolveLocal: type ${parsed.type} unhandled`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,37 +21,37 @@ export const meta = {
|
||||
invalidProvider: {
|
||||
message: 'Invalid provider.',
|
||||
code: 'INVALID_PROVIDER',
|
||||
id: '14BF7AE1-80CC-4363-ACB2-4FD61D086AF0',
|
||||
id: '14bf7ae1-80cc-4363-acb2-4fd61d086af0',
|
||||
httpStatusCode: 400,
|
||||
},
|
||||
invalidParameters: {
|
||||
message: 'Invalid parameters.',
|
||||
code: 'INVALID_PARAMETERS',
|
||||
id: '26654194-410E-44E2-B42E-460FF6F92476',
|
||||
id: '26654194-410e-44e2-b42e-460ff6f92476',
|
||||
httpStatusCode: 400,
|
||||
},
|
||||
noResponseProvided: {
|
||||
message: 'No response provided.',
|
||||
code: 'NO_RESPONSE_PROVIDED',
|
||||
id: '40ACBBA8-0937-41FB-BB3F-474514D40AFE',
|
||||
id: '40acbba8-0937-41fb-bb3f-474514d40afe',
|
||||
httpStatusCode: 400,
|
||||
},
|
||||
requestFailed: {
|
||||
message: 'Request failed.',
|
||||
code: 'REQUEST_FAILED',
|
||||
id: '0F4FE2F1-2C15-4D6E-B714-EFBFCDE231CD',
|
||||
id: '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd',
|
||||
httpStatusCode: 500,
|
||||
},
|
||||
verificationFailed: {
|
||||
message: 'Verification failed.',
|
||||
code: 'VERIFICATION_FAILED',
|
||||
id: 'C41C067F-24F3-4150-84B2-B5A3AE8C2214',
|
||||
id: 'c41c067f-24f3-4150-84b2-b5a3ae8c2214',
|
||||
httpStatusCode: 400,
|
||||
},
|
||||
unknown: {
|
||||
message: 'unknown',
|
||||
code: 'UNKNOWN',
|
||||
id: 'F868D509-E257-42A9-99C1-42614B031A97',
|
||||
id: 'f868d509-e257-42a9-99c1-42614b031a97',
|
||||
httpStatusCode: 500,
|
||||
},
|
||||
},
|
||||
|
@ -19,6 +19,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
@ -32,6 +33,31 @@ export const meta = {
|
||||
},
|
||||
|
||||
errors: {
|
||||
federationNotAllowed: {
|
||||
message: 'Federation for this host is not allowed.',
|
||||
code: 'FEDERATION_NOT_ALLOWED',
|
||||
id: '974b799e-1a29-4889-b706-18d4dd93e266',
|
||||
},
|
||||
uriInvalid: {
|
||||
message: 'URI is invalid.',
|
||||
code: 'URI_INVALID',
|
||||
id: '1a5eab56-e47b-48c2-8d5e-217b897d70db',
|
||||
},
|
||||
requestFailed: {
|
||||
message: 'Request failed.',
|
||||
code: 'REQUEST_FAILED',
|
||||
id: '81b539cf-4f57-4b29-bc98-032c33c0792e',
|
||||
},
|
||||
responseInvalid: {
|
||||
message: 'Response from remote server is invalid.',
|
||||
code: 'RESPONSE_INVALID',
|
||||
id: '70193c39-54f3-4813-82f0-70a680f7495b',
|
||||
},
|
||||
responseInvalidIdHostNotMatch: {
|
||||
message: 'Requested URI and response URI host does not match.',
|
||||
code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH',
|
||||
id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a',
|
||||
},
|
||||
noSuchObject: {
|
||||
message: 'No such object.',
|
||||
code: 'NO_SUCH_OBJECT',
|
||||
@ -110,7 +136,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
*/
|
||||
@bindThis
|
||||
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
||||
if (!this.utilityService.isFederationAllowedUri(uri)) return null;
|
||||
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
||||
throw new ApiError(meta.errors.federationNotAllowed);
|
||||
}
|
||||
|
||||
let local = await this.mergePack(me, ...await Promise.all([
|
||||
this.apDbResolverService.getUserFromApId(uri),
|
||||
@ -125,7 +153,40 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
// リモートから一旦オブジェクトフェッチ
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
const object = await resolver.resolve(uri) as any;
|
||||
const object = await resolver.resolve(uri).catch((err) => {
|
||||
if (err instanceof IdentifiableError) {
|
||||
switch (err.id) {
|
||||
// resolve
|
||||
case 'b94fd5b1-0e3b-4678-9df2-dad4cd515ab2':
|
||||
throw new ApiError(meta.errors.uriInvalid);
|
||||
case '0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5':
|
||||
case 'd592da9f-822f-4d91-83d7-4ceefabcf3d2':
|
||||
throw new ApiError(meta.errors.requestFailed);
|
||||
case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
|
||||
throw new ApiError(meta.errors.federationNotAllowed);
|
||||
case '72180409-793c-4973-868e-5a118eb5519b':
|
||||
case 'ad2dc287-75c1-44c4-839d-3d2e64576675':
|
||||
throw new ApiError(meta.errors.responseInvalid);
|
||||
case 'fd93c2fa-69a8-440f-880b-bf178e0ec877':
|
||||
throw new ApiError(meta.errors.responseInvalidIdHostNotMatch);
|
||||
|
||||
// resolveLocal
|
||||
case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8':
|
||||
throw new ApiError(meta.errors.uriInvalid);
|
||||
case 'a9d946e5-d276-47f8-95fb-f04230289bb0':
|
||||
case '06ae3170-1796-4d93-a697-2611ea6d83b6':
|
||||
throw new ApiError(meta.errors.noSuchObject);
|
||||
case '7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0':
|
||||
throw new ApiError(meta.errors.responseInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApiError(meta.errors.requestFailed);
|
||||
});
|
||||
|
||||
if (object.id == null) {
|
||||
throw new ApiError(meta.errors.responseInvalid);
|
||||
}
|
||||
|
||||
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
||||
// これはDBに存在する可能性があるため再度DB検索
|
||||
|
@ -131,11 +131,7 @@ describe('Note', () => {
|
||||
rejects(
|
||||
async () => await bob.client.request('ap/show', { uri: `https://a.test/notes/${note.id}` }),
|
||||
(err: any) => {
|
||||
/**
|
||||
* FIXME: this error is not handled
|
||||
* @see https://github.com/misskey-dev/misskey/issues/12736
|
||||
*/
|
||||
strictEqual(err.code, 'INTERNAL_ERROR');
|
||||
strictEqual(err.code, 'REQUEST_FAILED');
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
@ -30,6 +30,9 @@ import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmount
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
// 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 = {
|
||||
render(container: string | Node, options: {
|
||||
readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown;
|
||||
@ -53,6 +56,7 @@ declare global {
|
||||
const props = defineProps<{
|
||||
provider: CaptchaProvider;
|
||||
sitekey: string | null; // null will show error on request
|
||||
secretKey?: string | null;
|
||||
instanceUrl?: string | null;
|
||||
modelValue?: string | null;
|
||||
}>();
|
||||
@ -64,7 +68,7 @@ const emit = defineEmits<{
|
||||
const available = ref(false);
|
||||
|
||||
const captchaEl = shallowRef<HTMLDivElement | undefined>();
|
||||
|
||||
const captchaWidgetId = ref<string | undefined>(undefined);
|
||||
const testcaptchaInput = ref('');
|
||||
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);
|
||||
|
||||
watch(() => [props.instanceUrl, props.sitekey], async () => {
|
||||
watch(() => [props.instanceUrl, props.sitekey, props.secretKey], async () => {
|
||||
// 変更があったときはリフレッシュと再レンダリングをしておかないと、変更後の値で再検証が出来ない
|
||||
if (available.value) {
|
||||
callback(undefined);
|
||||
clearWidget();
|
||||
await requestRender();
|
||||
}
|
||||
});
|
||||
@ -114,9 +119,9 @@ if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha')
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (captcha.value.reset) {
|
||||
if (captcha.value.reset && captchaWidgetId.value !== undefined) {
|
||||
try {
|
||||
captcha.value.reset();
|
||||
captcha.value.reset(captchaWidgetId.value);
|
||||
} catch (error: unknown) {
|
||||
// ignore
|
||||
if (_DEV_) console.warn(error);
|
||||
@ -126,28 +131,33 @@ function reset() {
|
||||
testcaptchaInput.value = '';
|
||||
}
|
||||
|
||||
async function requestRender() {
|
||||
if (captcha.value.render && captchaEl.value instanceof Element) {
|
||||
// 設定値の変更時などのタイミングで再レンダリングを行う際はリセットしておく必要がある
|
||||
reset();
|
||||
captchaEl.value.innerHTML = '';
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (props.sitekey && props.sitekey.length > 0) {
|
||||
captcha.value.render(captchaEl.value, {
|
||||
async function requestRender() {
|
||||
if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) {
|
||||
// reCAPTCHAのレンダリング重複判定を回避するため、captchaEl配下に仮のdivを用意する.
|
||||
// (同じdivに対して複数回renderを呼び出すとreCAPTCHAはエラーを返すので)
|
||||
const elem = document.createElement('div');
|
||||
captchaEl.value.appendChild(elem);
|
||||
|
||||
captchaWidgetId.value = captcha.value.render(elem, {
|
||||
sitekey: props.sitekey,
|
||||
theme: defaultStore.state.darkMode ? 'dark' : 'light',
|
||||
callback: callback,
|
||||
'expired-callback': () => callback(undefined),
|
||||
'error-callback': () => callback(undefined),
|
||||
});
|
||||
}
|
||||
} 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');
|
||||
new Widget({
|
||||
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) {
|
||||
emit('update:modelValue', typeof response === 'string' ? response : null);
|
||||
}
|
||||
@ -192,7 +219,7 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
reset();
|
||||
clearWidget();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
@ -11,6 +11,7 @@ import * as Misskey from 'misskey-js';
|
||||
import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
||||
import type { Form, GetFormResultType } from '@/scripts/form.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.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 { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||
import { focusParent } from '@/scripts/focus.js';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
|
||||
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']>(
|
||||
endpoint: E,
|
||||
data: P,
|
||||
token?: string | null | undefined,
|
||||
customErrors?: Record<string, { title?: string; text: string; }>,
|
||||
customErrors?: ApiWithDialogCustomErrors,
|
||||
) => {
|
||||
const promise = misskeyApi(endpoint, data, token);
|
||||
promiseDialog(promise, null, async (err) => {
|
||||
|
@ -28,21 +28,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkRadios>
|
||||
|
||||
<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 #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey">
|
||||
<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
|
||||
</MkInput>
|
||||
<FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha v-model="captchaResult" provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey"/>
|
||||
<template #label>{{ i18n.ts._captcha.verify }}</template>
|
||||
<MkCaptcha
|
||||
v-model="captchaResult"
|
||||
provider="hcaptcha"
|
||||
:sitekey="botProtectionForm.state.hcaptchaSiteKey"
|
||||
:secretKey="botProtectionForm.state.hcaptchaSecretKey"
|
||||
/>
|
||||
</FormSlot>
|
||||
<MkInfo>
|
||||
<div :class="$style.captchaInfoMsg">
|
||||
<div>{{ i18n.tsx.testSiteKeyMessage({ testSiteKey: '10000000-ffff-ffff-ffff-000000000001' }) }}</div>
|
||||
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
|
||||
<div>
|
||||
<span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a>
|
||||
</div>
|
||||
@ -51,46 +56,51 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<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 #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey">
|
||||
<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl">
|
||||
<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce>
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
|
||||
</MkInput>
|
||||
<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<template #label>{{ i18n.ts._captcha.verify }}</template>
|
||||
<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"
|
||||
/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
|
||||
<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 #label>{{ i18n.ts.recaptchaSiteKey }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="botProtectionForm.state.recaptchaSecretKey">
|
||||
<MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
|
||||
</MkInput>
|
||||
<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<template #label>{{ i18n.ts._captcha.verify }}</template>
|
||||
<MkCaptcha
|
||||
v-model="captchaResult" provider="recaptcha"
|
||||
v-model="captchaResult"
|
||||
provider="recaptcha"
|
||||
:sitekey="botProtectionForm.state.recaptchaSiteKey"
|
||||
:secretKey="botProtectionForm.state.recaptchaSecretKey"
|
||||
/>
|
||||
</FormSlot>
|
||||
<MkInfo>
|
||||
<div :class="$style.captchaInfoMsg">
|
||||
<div>{{ i18n.tsx.testSiteKeyMessage({ testSiteKey: '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI' }) }}</div>
|
||||
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
|
||||
<div>
|
||||
<span>ref: </span>
|
||||
<a
|
||||
@ -103,27 +113,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<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 #label>{{ i18n.ts.turnstileSiteKey }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="botProtectionForm.state.turnstileSecretKey">
|
||||
<MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
|
||||
</MkInput>
|
||||
<FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<template #label>{{ i18n.ts._captcha.verify }}</template>
|
||||
<MkCaptcha
|
||||
v-model="captchaResult" provider="turnstile"
|
||||
v-model="captchaResult"
|
||||
provider="turnstile"
|
||||
:sitekey="botProtectionForm.state.turnstileSiteKey"
|
||||
:secretKey="botProtectionForm.state.turnstileSecretKey"
|
||||
/>
|
||||
</FormSlot>
|
||||
<MkInfo>
|
||||
<div :class="$style.captchaInfoMsg">
|
||||
<div :class="$style.noSpace">
|
||||
{{ i18n.tsx.testSiteKeyMessage({ testSiteKey: '1x00000000000000000000AA' }) }}
|
||||
<div>
|
||||
{{ i18n.ts._captcha.testSiteKeyMessage }}
|
||||
</div>
|
||||
<div :class="$style.noSpace">
|
||||
<div>
|
||||
<span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -133,7 +145,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
|
||||
<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<template #label>{{ i18n.ts._captcha.verify }}</template>
|
||||
<MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
@ -155,9 +167,28 @@ import { useForm } from '@/scripts/use-form.js';
|
||||
import MkFormFooter from '@/components/MkFormFooter.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { ApiWithDialogCustomErrors } from '@/os.js';
|
||||
|
||||
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 meta = await misskeyApi('admin/captcha/current');
|
||||
@ -174,7 +205,14 @@ const botProtectionForm = useForm({
|
||||
turnstileSecretKey: meta.turnstile.secretKey,
|
||||
}, async (state) => {
|
||||
const provider = state.provider;
|
||||
|
||||
if (provider === 'none') {
|
||||
await os.apiWithDialog(
|
||||
'admin/captcha/save',
|
||||
{ provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'] },
|
||||
undefined,
|
||||
errorHandler,
|
||||
);
|
||||
} else {
|
||||
const sitekey = provider === 'hcaptcha'
|
||||
? state.hcaptchaSiteKey
|
||||
: provider === 'mcaptcha'
|
||||
@ -194,12 +232,6 @@ const botProtectionForm = useForm({
|
||||
? state.turnstileSecretKey
|
||||
: null;
|
||||
|
||||
if (provider === 'none') {
|
||||
await os.apiWithDialog(
|
||||
'admin/captcha/save',
|
||||
{ provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'] },
|
||||
);
|
||||
} else {
|
||||
await os.apiWithDialog(
|
||||
'admin/captcha/save',
|
||||
{
|
||||
@ -209,6 +241,8 @@ const botProtectionForm = useForm({
|
||||
instanceUrl: state.mcaptchaInstanceUrl,
|
||||
captchaResult: captchaResult.value,
|
||||
},
|
||||
undefined,
|
||||
errorHandler,
|
||||
);
|
||||
}
|
||||
|
||||
@ -236,8 +270,4 @@ const canSaving = computed((): boolean => {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.noSpace {
|
||||
white-space-collapse: discard;
|
||||
}
|
||||
</style>
|
||||
|
@ -33,7 +33,43 @@ export async function lookup(router?: Router) {
|
||||
uri: query,
|
||||
});
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
os.promiseDialog(promise, null, (err) => {
|
||||
let title = i18n.ts.somethingHappened;
|
||||
let text = err.message + '\n' + err.id;
|
||||
|
||||
switch (err.id) {
|
||||
case '974b799e-1a29-4889-b706-18d4dd93e266':
|
||||
title = i18n.ts._remoteLookupErrors._federationNotAllowed.title;
|
||||
text = i18n.ts._remoteLookupErrors._federationNotAllowed.description;
|
||||
break;
|
||||
case '1a5eab56-e47b-48c2-8d5e-217b897d70db':
|
||||
title = i18n.ts._remoteLookupErrors._uriInvalid.title;
|
||||
text = i18n.ts._remoteLookupErrors._uriInvalid.description;
|
||||
break;
|
||||
case '81b539cf-4f57-4b29-bc98-032c33c0792e':
|
||||
title = i18n.ts._remoteLookupErrors._requestFailed.title;
|
||||
text = i18n.ts._remoteLookupErrors._requestFailed.description;
|
||||
break;
|
||||
case '70193c39-54f3-4813-82f0-70a680f7495b':
|
||||
title = i18n.ts._remoteLookupErrors._responseInvalid.title;
|
||||
text = i18n.ts._remoteLookupErrors._responseInvalid.description;
|
||||
break;
|
||||
case 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a':
|
||||
title = i18n.ts._remoteLookupErrors._responseInvalid.title;
|
||||
text = i18n.ts._remoteLookupErrors._responseInvalidIdHostNotMatch.description;
|
||||
break;
|
||||
case 'dc94d745-1262-4e63-a17d-fecaa57efc82':
|
||||
title = i18n.ts._remoteLookupErrors._noSuchObject.title;
|
||||
text = i18n.ts._remoteLookupErrors._noSuchObject.description;
|
||||
break;
|
||||
}
|
||||
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title,
|
||||
text,
|
||||
});
|
||||
}, i18n.ts.fetchingAsApObject);
|
||||
|
||||
const res = await promise;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user