Merge branch 'develop' into enh-14794

This commit is contained in:
かっこかり 2025-01-08 21:16:45 +09:00 committed by GitHub
commit a1a0dac8d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 1979 additions and 283 deletions

View File

@ -54,7 +54,7 @@ body:
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
* Browser: Chrome 113.0.5672.126
* Server URL: misskey.example.com
* Misskey: 2024.x.x
* Misskey: 2025.x.x
value: |
* Model and OS of the device(s):
* Browser:
@ -74,7 +74,7 @@ body:
Examples:
* Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
* Misskey: 2024.x.x
* Misskey: 2025.x.x
* Node: 20.x.x
* PostgreSQL: 15.x.x
* Redis: 7.x.x

View File

@ -1,18 +1,40 @@
## Unreleased
## 2024.11.1
### General
-
### Client
- Enhance: PC画面でチャンネルが複数列で表示されるように
(Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
- Enhance: 照会に失敗した場合、その理由を表示するように
- Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加
- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に
- Enhance: 投稿フォームの設定メニューを改良
- 投稿フォームをリセットできるように
- 文字数カウントを復活
- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
- Fix: サーバー情報メニューに区切り線が不足していたのを修正
- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正
- Fix: 公開範囲がホームのノートの埋め込みウィジェットが読み込まれない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/803)
- Fix: 絵文字管理画面で一部の絵文字が表示されない問題を修正
- Fix: ノート検索が使用できない場合でもチャンネルのノート検索欄がでていた問題を修正
- Fix: `Ui:C:select`で値の変更が画面に反映されない問題を修正
- Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正
(Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4)
- Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正
- Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正
- Fix: 言語データのキャッシュ状況によっては、埋め込みウィジェットが正しく起動しない問題を修正
### Server
- Enhance: pg_bigmが利用できるよう、ートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように
- Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 )
- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737)
- Fix: ロックダウンされた期間指定のートがStreaming経由でLTLに出現するのを修正 ( #15200 )
- Fix: disableClustering設定時の初期化ロジックを調整( #15223 )
- Fix: ActivityPubリクエストかどうかの判定が正しくない問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/869)
## 2024.11.0

View File

@ -1,5 +1,5 @@
Unless otherwise stated this repository is
Copyright © 2014-2024 syuilo and contributors
Copyright © 2014-2025 syuilo and contributors
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4
ARG NODE_VERSION=22.11.0-bullseye
ARG NODE_VERSION=22.11.0-bookworm
# build assets & compile TypeScript

67
locales/index.d.ts vendored
View File

@ -2754,10 +2754,18 @@ export interface Locale extends ILocale {
*
*/
"wordMute": string;
/**
*
*/
"wordMuteDescription": string;
/**
*
*/
"hardWordMute": string;
/**
*
*/
"hardWordMuteDescription": string;
/**
*
*/
@ -10609,6 +10617,65 @@ 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;
};
};
}
declare const locales: {
[lang: string]: Locale;

View File

@ -684,7 +684,9 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使用時はオフにします。"
testEmail: "配信テスト"
wordMute: "ワードミュート"
wordMuteDescription: "指定した語句を含むノートを最小化します。最小化されたノートをクリックすることで表示することができます。"
hardWordMute: "ハードワードミュート"
hardWordMuteDescription: "指定した語句を含むノートを隠します。ワードミュートとは異なり、ノートは完全に表示されなくなります。"
regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
instanceMute: "サーバーミュート"
@ -2828,3 +2830,22 @@ _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をもう一度お確かめください。"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.11.0",
"version": "2024.11.1-alpha.0",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -53,4 +53,4 @@ const promises = Array
connectToPostgres()
]);
await Promise.allSettled(promises);
await Promise.all(promises);

View File

@ -68,16 +68,22 @@ process.on('exit', code => {
//#endregion
if (cluster.isPrimary || envOption.disableClustering) {
await masterMain();
if (!envOption.disableClustering) {
if (cluster.isPrimary) {
logger.info(`Start main process... pid: ${process.pid}`);
await masterMain();
ev.mount();
}
}
if (cluster.isWorker || envOption.disableClustering) {
} else if (cluster.isWorker) {
logger.info(`Start worker process... pid: ${process.pid}`);
await workerMain();
} else {
throw new Error('Unknown process type');
}
} else {
// 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない)
logger.info(`Start main process... pid: ${process.pid}`);
await masterMain();
ev.mount();
}
readyRef.value = true;

View File

@ -91,25 +91,37 @@ export async function masterMain() {
});
}
if (envOption.disableClustering) {
bootLogger.info(
`mode: [disableClustering: ${envOption.disableClustering}, onlyServer: ${envOption.onlyServer}, onlyQueue: ${envOption.onlyQueue}]`
);
if (!envOption.disableClustering) {
// clusterモジュール有効時
if (envOption.onlyServer) {
await server();
// onlyServer かつ enableCluster な場合、メインプロセスはforkのみに制限する(listenしない)。
// ワーカープロセス側でlistenすると、メインプロセスでポートへの着信を受け入れてワーカープロセスへの分配を行う動作をする。
// そのため、メインプロセスでも直接listenするとポートの競合が発生して起動に失敗してしまう。
// see: https://nodejs.org/api/cluster.html#cluster
} else if (envOption.onlyQueue) {
await jobQueue();
} else {
await server();
await jobQueue();
}
} else {
if (envOption.onlyServer) {
// nop
} else if (envOption.onlyQueue) {
// nop
} else {
await server();
}
await spawnWorkers(config.clusterLimit);
} else {
// clusterモジュール無効時
if (envOption.onlyServer) {
await server();
} else if (envOption.onlyQueue) {
await jobQueue();
} else {
await server();
await jobQueue();
}
}
if (envOption.onlyQueue) {

View File

@ -28,7 +28,7 @@ export class S3Service {
? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}`
: `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent
const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy);
const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy, true);
const handlerOption: NodeHttpHandlerOptions = {};
if (meta.objectStorageUseSSL) {
handlerOption.httpsAgent = agent as https.Agent;

View File

@ -215,7 +215,7 @@ export class SearchService {
}
query
.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` })
.andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')

View File

@ -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`);
}
}
}

View File

@ -102,8 +102,7 @@ export class NoteEntityService implements OnModuleInit {
}
@bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
// FIXME: このvisibility変更処理が当関数にあるのは若干不自然かもしれない(関数名を treatVisibility とかに変える手もある)
private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] {
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
if ((followersOnlyBefore != null)
@ -115,7 +114,11 @@ export class NoteEntityService implements OnModuleInit {
packedNote.visibility = 'followers';
}
}
return packedNote.visibility;
}
@bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
if (meId === packedNote.userId) return;
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
@ -458,6 +461,8 @@ export class NoteEntityService implements OnModuleInit {
} : {}),
});
this.treatVisibility(packed);
if (!opts.skipHide) {
await this.hideNote(packed, meId);
}

View File

@ -519,8 +519,8 @@ export class ActivityPubServerService {
},
deriveConstraint(request: IncomingMessage) {
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
const isAp = typeof accepted === 'string' && !accepted.match(/html/);
return isAp ? 'ap' : 'html';
if (accepted === false) return null;
return accepted !== 'html' ? 'ap' : 'html';
},
});

View File

@ -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検索

View File

@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
name: token.name ?? token.app?.name,
createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(),
permission: token.permission,
permission: token.app ? token.app.permission : token.permission,
})));
});
}

View File

@ -871,7 +871,7 @@ export class ClientServerService {
});
if (note == null) return;
if (note.visibility !== 'public') return;
if (['specified', 'followers'].includes(note.visibility)) return;
if (note.userHost != null) return;
const _note = await this.noteEntityService.pack(note, null, { detail: true });

View File

@ -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;
},
);

View File

@ -17,11 +17,11 @@ import { applyTheme, assertIsTheme } from '@/theme.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { DI } from '@/di.js';
import { serverMetadata } from '@/server-metadata.js';
import { url } from '@@/js/config.js';
import { url, version, locale, lang, updateLocale } from '@@/js/config.js';
import { parseEmbedParams } from '@@/js/embed-page.js';
import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
import { serverContext } from '@/server-context.js';
import { i18n } from '@/i18n.js';
import { i18n, updateI18n } from '@/i18n.js';
import type { Theme } from '@/theme.js';
@ -71,6 +71,22 @@ if (embedParams.colorMode === 'dark') {
}
//#endregion
//#region Detect language & fetch translations
const localeVersion = localStorage.getItem('localeVersion');
const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null);
if (localeOutdated) {
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
if (res.status === 200) {
const newLocale = await res.text();
const parsedNewLocale = JSON.parse(newLocale);
localStorage.setItem('locale', newLocale);
localStorage.setItem('localeVersion', version);
updateLocale(parsedNewLocale);
updateI18n(parsedNewLocale);
}
}
//#endregion
// サイズの制限
document.documentElement.style.maxWidth = '500px';

View File

@ -415,8 +415,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
return [h(EmEmoji, {
key: Math.random(),
emoji: token.props.emoji,
menu: props.enableEmojiMenu,
menuReaction: props.enableEmojiMenuReaction,
})];
}

View File

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notes }">
<div :class="[$style.root]">
<EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
<EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note as Misskey.entities.Note"/>
</div>
</template>
</EmPagination>
@ -24,6 +24,7 @@ import { useTemplateRef } from 'vue';
import EmNote from '@/components/EmNote.vue';
import EmPagination, { Paging } from '@/components/EmPagination.vue';
import { i18n } from '@/i18n.js';
import * as Misskey from 'misskey-js';
withDefaults(defineProps<{
pagination: Paging;

View File

@ -75,8 +75,12 @@ function compile(theme: Theme): Record<string, string> {
return getColor(theme.props[val]);
} else if (val[0] === ':') { // func
const parts = val.split('<');
const func = parts.shift().substring(1);
const arg = parseFloat(parts.shift());
const funcTxt = parts.shift();
const argTxt = parts.shift();
if (funcTxt && argTxt) {
const func = funcTxt.substring(1);
const arg = parseFloat(argTxt);
const color = getColor(parts.join('<'));
switch (func) {
@ -87,6 +91,7 @@ function compile(theme: Theme): Record<string, string> {
case 'saturate': return color.saturate(arg);
}
}
}
// other case
return tinycolor(val);

View File

@ -10,8 +10,8 @@
"declaration": false,
"sourceMap": false,
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"module": "ES2022",
"moduleResolution": "Bundler",
"removeComments": false,
"noLib": false,
"strict": true,

View File

@ -26,6 +26,7 @@
"@typescript-eslint/parser": "7.17.0",
"esbuild": "0.24.0",
"eslint-plugin-vue": "9.31.0",
"nodemon": "3.1.7",
"typescript": "5.6.3",
"vue-eslint-parser": "9.4.3"
},

View File

@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkInput>
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default ?? null" @update:modelValue="c.onChange">
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" @update:modelValue="onSelectUpdate">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
@ -77,8 +77,8 @@ import MkPostForm from '@/components/MkPostForm.vue';
const props = withDefaults(defineProps<{
component: AsUiComponent;
components: Ref<AsUiComponent>[];
size: 'small' | 'medium' | 'large';
align: 'left' | 'center' | 'right';
size?: 'small' | 'medium' | 'large';
align?: 'left' | 'center' | 'right';
}>(), {
size: 'medium',
align: 'left',
@ -86,7 +86,7 @@ const props = withDefaults(defineProps<{
const c = props.component;
function g(id) {
function g(id: string) {
const v = props.components.find(x => x.value.id === id)?.value;
if (v) return v;
@ -122,13 +122,22 @@ const containerStyle = computed(() => {
const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false);
function onSwitchUpdate(v) {
function onSwitchUpdate(v: boolean) {
valueForSwitch.value = v;
if ('onChange' in c && c.onChange) {
c.onChange(v as never);
}
}
const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null);
function onSelectUpdate(v) {
valueForSelect.value = v;
if ('onChange' in c && c.onChange) {
c.onChange(v as never);
}
}
function openPostForm() {
const form = (c as AsUiPostFormButton).form;
if (!form) return;

View File

@ -125,7 +125,9 @@ const bannerStyle = computed(() => {
position: absolute;
top: 16px;
left: 16px;
max-width: calc(100% - 32px);
padding: 12px 16px;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 1.2em;

View File

@ -248,6 +248,7 @@ const canPost = computed((): boolean => {
quoteId.value != null
) &&
(textLength.value <= maxTextLength.value) &&
(files.value.length <= 16) &&
(!poll.value || poll.value.choices.length >= 2);
});

View File

@ -22,7 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
</Sortable>
<p :class="$style.remain">{{ 16 - props.modelValue.length }}/16</p>
<p :class="[$style.remain, {
[$style.exceeded]: props.modelValue.length > 16,
}]">{{ 16 - props.modelValue.length }}/16</p>
</div>
</template>
@ -239,5 +241,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
margin: 0;
padding: 0;
font-size: 90%;
&.exceeded {
color: var(--MI_THEME-error);
}
}
</style>

View File

@ -384,6 +384,7 @@ const patrons = [
'こまつぶり',
'まゆつな空高',
'asata',
'ruru',
];
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));

View File

@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkNotes :pagination="featuredPagination"/>
</div>
<div v-else-if="tab === 'search'" key="search">
<div class="_gaps">
<div v-if="notesSearchAvailable" class="_gaps">
<div>
<MkInput v-model="searchQuery" @enter="search()">
<template #prefix><i class="ti ti-search"></i></template>
@ -54,6 +54,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
</div>
<div v-else>
<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
</div>
</div>
</MkHorizontalSwipe>
</MkSpacer>
@ -94,6 +97,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { PageHeaderItem } from '@/types/page-header.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { notesSearchAvailable } from '@/scripts/check-permissions.js';
import { miLocalStorage } from '@/local-storage.js';
import { useRouter } from '@/router/supplier.js';

View File

@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700">
<MkSpacer :contentMax="1200">
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div v-if="tab === 'search'" key="search">
<div v-if="tab === 'search'" key="search" :class="$style.searchRoot">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
<template #prefix><i class="ti ti-search"></i></template>
@ -27,23 +27,31 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-if="tab === 'featured'" key="featured">
<MkPagination v-slot="{items}" :pagination="featuredPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'favorites'" key="favorites">
<MkPagination v-slot="{items}" :pagination="favoritesPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'following'" key="following">
<MkPagination v-slot="{items}" :pagination="followingPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'owned'" key="owned">
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="ownedPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
</MkPagination>
</div>
</MkHorizontalSwipe>
@ -85,6 +93,7 @@ onMounted(() => {
const featuredPagination = {
endpoint: 'channels/featured' as const,
limit: 10,
noPaging: true,
};
const favoritesPagination = {
@ -157,3 +166,17 @@ definePageMetadata(() => ({
icon: 'ti ti-device-tv',
}));
</script>
<style lang="scss" module>
.searchRoot {
width: 100%;
max-width: 700px;
margin: 0 auto;
}
.root {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: var(--MI-margin);
}
</style>

View File

@ -46,9 +46,10 @@ import { clipsCache } from '@/cache.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
import { getServerContext } from '@/server-context.js';
import { assertServerContext, serverContext } from '@/server-context.js';
const CTX_CLIP = getServerContext('clip');
// context
const CTX_CLIP = !$i && assertServerContext(serverContext, 'clip') ? serverContext.clip : null;
const props = defineProps<{
clipId: string,

View File

@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{items}">
<div class="ldhfsamy">
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
<img :src="`/emoji/${emoji.name}.webp`" class="img" :alt="emoji.name"/>
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.category }}</div>
@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{items}">
<div class="ldhfsamy">
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
<img :src="`/emoji/${emoji.name}@${emoji.host}.webp`" class="img" :alt="emoji.name"/>
<img :src="getProxiedImageUrl(emoji.url, 'emoji')" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.host }}</div>
@ -83,6 +83,7 @@ import FormSplit from '@/components/form/split.vue';
import { selectFile } from '@/scripts/select-file.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';

View File

@ -118,7 +118,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
}, { immediate: true });
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? props.emoji.url : null);
async function changeImage(ev: Event) {
file.value = await selectFile(ev.currentTarget ?? ev.target, null);

View File

@ -59,10 +59,7 @@ async function onAccept(token: string) {
name: props.name,
iconUrl: props.icon,
permission: _permissions.value,
}, token).catch(() => {
authRoot.value?.showUI('failed');
});
}, token).then(() => {
if (props.callback && props.callback !== '') {
const cbUrl = new URL(props.callback);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
@ -71,6 +68,9 @@ async function onAccept(token: string) {
} else {
authRoot.value?.showUI('success');
}
}).catch(() => {
authRoot.value?.showUI('failed');
});
}
function onDeny() {
@ -117,5 +117,6 @@ definePageMetadata(() => ({
border-radius: var(--MI-radius);
background-color: var(--MI_THEME-panel);
overflow-x: scroll;
white-space: nowrap;
}
</style>

View File

@ -50,6 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import type { Paging } from '@/components/MkPagination.vue';
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import MkNotes from '@/components/MkNotes.vue';
@ -62,9 +63,11 @@ import { dateString } from '@/filters/date.js';
import MkClipPreview from '@/components/MkClipPreview.vue';
import { defaultStore } from '@/store.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { getServerContext } from '@/server-context.js';
import { serverContext, assertServerContext } from '@/server-context.js';
import { $i } from '@/account.js';
const CTX_NOTE = getServerContext('note');
// context
const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null;
const props = defineProps<{
noteId: string;
@ -140,7 +143,12 @@ function fetchNote() {
}).catch(err => {
if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') {
pleaseLogin({
path: '/',
message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
openOnRemote: {
type: 'lookup',
url: `https://${host}/notes/${props.noteId}`,
},
});
}
error.value = err;

View File

@ -12,7 +12,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton @click="init"><i class="ti ti-refresh"></i> {{ i18n.ts.reloadAccountsList }}</MkButton>
</div>
<MkUserCardMini v-for="user in accounts" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/>
<template v-for="[id, user] in accounts">
<MkUserCardMini v-if="user != null" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/>
<button v-else v-panel class="_button" :class="$style.unknownUser" @click="menu(id, $event)">
<div :class="$style.unknownUserAvatarMock"><i class="ti ti-user-question"></i></div>
<div>
<div :class="$style.unknownUserTitle">{{ i18n.ts.unknown }}</div>
<div :class="$style.unknownUserSub">ID: <span class="_monospace">{{ id }}</span></div>
</div>
</button>
</template>
</div>
</FormSuspense>
</div>
@ -29,9 +38,10 @@ import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWith
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { MenuItem } from '@/types/menu';
const storedAccounts = ref<{ id: string, token: string }[] | null>(null);
const accounts = ref<Misskey.entities.UserDetailed[]>([]);
const accounts = ref(new Map<string, Misskey.entities.UserDetailed | null>());
const init = async () => {
getAccounts().then(accounts => {
@ -41,21 +51,35 @@ const init = async () => {
userIds: storedAccounts.value.map(x => x.id),
});
}).then(response => {
accounts.value = response;
if (storedAccounts.value == null) return;
accounts.value = new Map(storedAccounts.value.map(x => [x.id, response.find((y: Misskey.entities.UserDetailed) => y.id === x.id) ?? null]));
});
};
function menu(account: Misskey.entities.UserDetailed, ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts.switch,
icon: 'ti ti-switch-horizontal',
action: () => switchAccount(account),
}, {
function menu(account: Misskey.entities.UserDetailed | string, ev: MouseEvent) {
let menu: MenuItem[];
if (typeof account === 'string') {
menu = [{
text: i18n.ts.logout,
icon: 'ti ti-trash',
danger: true,
action: () => removeAccount(account),
}], ev.currentTarget ?? ev.target);
}];
} else {
menu = [{
text: i18n.ts.switch,
icon: 'ti ti-switch-horizontal',
action: () => switchAccount(account.id),
}, {
text: i18n.ts.logout,
icon: 'ti ti-trash',
danger: true,
action: () => removeAccount(account.id),
}];
}
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
function addAccount(ev: MouseEvent) {
@ -68,9 +92,9 @@ function addAccount(ev: MouseEvent) {
}], ev.currentTarget ?? ev.target);
}
async function removeAccount(account: Misskey.entities.UserDetailed) {
await _removeAccount(account.id);
accounts.value = accounts.value.filter(x => x.id !== account.id);
async function removeAccount(id: string) {
await _removeAccount(id);
accounts.value.delete(id);
}
function addExistingAccount() {
@ -90,9 +114,9 @@ function createAccount() {
});
}
async function switchAccount(account: Misskey.entities.UserDetailed) {
async function switchAccount(id: string) {
const fetchedAccounts = await getAccounts();
const token = fetchedAccounts.find(x => x.id === account.id)!.token;
const token = fetchedAccounts.find(x => x.id === id)!.token;
switchAccountWithToken(token);
}
@ -114,4 +138,47 @@ definePageMetadata(() => ({
.user {
cursor: pointer;
}
.unknownUser {
display: flex;
align-items: center;
text-align: start;
padding: 16px;
background: var(--MI_THEME-panel);
border-radius: 8px;
font-size: 0.9em;
}
.unknownUserAvatarMock {
display: block;
width: 34px;
height: 34px;
line-height: 34px;
text-align: center;
font-size: 16px;
margin-right: 12px;
background-color: color-mix(in srgb, var(--MI_THEME-fg), transparent 85%);
color: color-mix(in srgb, var(--MI_THEME-fg), transparent 25%);
border-radius: 50%;
}
.unknownUserTitle {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 18px;
}
.unknownUserSub {
display: block;
width: 100%;
font-size: 95%;
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 16px;
}
</style>

View File

@ -9,14 +9,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.wordMute }}</template>
<div class="_gaps_m">
<MkInfo>{{ i18n.ts.wordMuteDescription }}</MkInfo>
<XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.hardWordMute }}</template>
<div class="_gaps_m">
<MkInfo>{{ i18n.ts.hardWordMuteDescription }}</MkInfo>
<XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/>
</div>
</MkFolder>
<MkFolder>

View File

@ -39,7 +39,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { getServerContext } from '@/server-context.js';
import { serverContext, assertServerContext } from '@/server-context.js';
const XHome = defineAsyncComponent(() => import('./home.vue'));
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
@ -53,7 +53,8 @@ const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
const CTX_USER = getServerContext('user');
// context
const CTX_USER = !$i && assertServerContext(serverContext, 'user') ? serverContext.user : null;
const props = withDefaults(defineProps<{
acct: string;

View File

@ -59,6 +59,7 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string
misskeyApiGet('federation/instances', {
sort: '+pubSub',
limit: 20,
blocked: 'false',
}).then(_instances => {
instances.value = _instances;
});

View File

@ -3,14 +3,24 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { utils, values } from '@syuilo/aiscript';
import { errors, utils, values } from '@syuilo/aiscript';
import * as Misskey from 'misskey-js';
import { url, lang } from '@@/js/config.js';
import { assertStringAndIsIn } from './common.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i } from '@/account.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
import { url, lang } from '@@/js/config.js';
const DIALOG_TYPES = [
'error',
'info',
'success',
'warning',
'waiting',
'question',
] as const;
export function aiScriptReadline(q: string): Promise<string> {
return new Promise(ok => {
@ -22,15 +32,20 @@ export function aiScriptReadline(q: string): Promise<string> {
});
}
export function createAiScriptEnv(opts) {
export function createAiScriptEnv(opts: { storageKey: string, token?: string }) {
return {
USER_ID: $i ? values.STR($i.id) : values.NULL,
USER_NAME: $i ? values.STR($i.name) : values.NULL,
USER_NAME: $i?.name ? values.STR($i.name) : values.NULL,
USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
LOCALE: values.STR(lang),
SERVER_URL: values.STR(url),
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
utils.assertString(title);
utils.assertString(text);
if (type != null) {
assertStringAndIsIn(type, DIALOG_TYPES);
}
await os.alert({
type: type ? type.value : 'info',
title: title.value,
@ -39,6 +54,11 @@ export function createAiScriptEnv(opts) {
return values.NULL;
}),
'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
utils.assertString(title);
utils.assertString(text);
if (type != null) {
assertStringAndIsIn(type, DIALOG_TYPES);
}
const confirm = await os.confirm({
type: type ? type.value : 'question',
title: title.value,
@ -48,14 +68,20 @@ export function createAiScriptEnv(opts) {
}),
'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
utils.assertString(ep);
if (ep.value.includes('://')) throw new Error('invalid endpoint');
if (ep.value.includes('://')) {
throw new errors.AiScriptRuntimeError('invalid endpoint');
}
if (token) {
utils.assertString(token);
// バグがあればundefinedもあり得るため念のため
if (typeof token.value !== 'string') throw new Error('invalid token');
}
const actualToken: string|null = token?.value ?? opts.token ?? null;
return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => {
if (param == null) {
throw new errors.AiScriptRuntimeError('expected param');
}
utils.assertObject(param);
return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => {
return utils.jsToVal(res);
}, err => {
return values.ERROR('request_failed', utils.jsToVal(err));
@ -75,12 +101,18 @@ export function createAiScriptEnv(opts) {
*/
'Mk:save': values.FN_NATIVE(([key, value]) => {
utils.assertString(key);
utils.expectAny(value);
miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value)));
return values.NULL;
}),
'Mk:load': values.FN_NATIVE(([key]) => {
utils.assertString(key);
return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`)));
return utils.jsToVal(miLocalStorage.getItemAsJson(`aiscript:${opts.storageKey}:${key.value}`) ?? null);
}),
'Mk:remove': values.FN_NATIVE(([key]) => {
utils.assertString(key);
miLocalStorage.removeItem(`aiscript:${opts.storageKey}:${key.value}`);
return values.NULL;
}),
'Mk:url': values.FN_NATIVE(() => {
return values.STR(window.location.href);

View File

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { errors, utils, type values } from '@syuilo/aiscript';
export function assertStringAndIsIn<A extends readonly string[]>(value: values.Value | undefined, expects: A): asserts value is values.VStr & { value: A[number] } {
utils.assertString(value);
const str = value.value;
if (!expects.includes(str)) {
const expected = expects.map((expect) => `"${expect}"`).join(', ');
throw new errors.AiScriptRuntimeError(`"${value.value}" is not in ${expected}`);
}
}

View File

@ -7,6 +7,15 @@ import { utils, values } from '@syuilo/aiscript';
import { v4 as uuid } from 'uuid';
import { ref, Ref } from 'vue';
import * as Misskey from 'misskey-js';
import { assertStringAndIsIn } from './common.js';
const ALIGNS = ['left', 'center', 'right'] as const;
const FONTS = ['serif', 'sans-serif', 'monospace'] as const;
const BORDER_STYLES = ['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] as const;
type Align = (typeof ALIGNS)[number];
type Font = (typeof FONTS)[number];
type BorderStyle = (typeof BORDER_STYLES)[number];
export type AsUiComponentBase = {
id: string;
@ -21,13 +30,13 @@ export type AsUiRoot = AsUiComponentBase & {
export type AsUiContainer = AsUiComponentBase & {
type: 'container';
children?: AsUiComponent['id'][];
align?: 'left' | 'center' | 'right';
align?: Align;
bgColor?: string;
fgColor?: string;
font?: 'serif' | 'sans-serif' | 'monospace';
font?: Font;
borderWidth?: number;
borderColor?: string;
borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset';
borderStyle?: BorderStyle;
borderRadius?: number;
padding?: number;
rounded?: boolean;
@ -40,7 +49,7 @@ export type AsUiText = AsUiComponentBase & {
size?: number;
bold?: boolean;
color?: string;
font?: 'serif' | 'sans-serif' | 'monospace';
font?: Font;
};
export type AsUiMfm = AsUiComponentBase & {
@ -49,14 +58,14 @@ export type AsUiMfm = AsUiComponentBase & {
size?: number;
bold?: boolean;
color?: string;
font?: 'serif' | 'sans-serif' | 'monospace';
onClickEv?: (evId: string) => void
font?: Font;
onClickEv?: (evId: string) => Promise<void>;
};
export type AsUiButton = AsUiComponentBase & {
type: 'button';
text?: string;
onClick?: () => void;
onClick?: () => Promise<void>;
primary?: boolean;
rounded?: boolean;
disabled?: boolean;
@ -69,7 +78,7 @@ export type AsUiButtons = AsUiComponentBase & {
export type AsUiSwitch = AsUiComponentBase & {
type: 'switch';
onChange?: (v: boolean) => void;
onChange?: (v: boolean) => Promise<void>;
default?: boolean;
label?: string;
caption?: string;
@ -77,7 +86,7 @@ export type AsUiSwitch = AsUiComponentBase & {
export type AsUiTextarea = AsUiComponentBase & {
type: 'textarea';
onInput?: (v: string) => void;
onInput?: (v: string) => Promise<void>;
default?: string;
label?: string;
caption?: string;
@ -85,7 +94,7 @@ export type AsUiTextarea = AsUiComponentBase & {
export type AsUiTextInput = AsUiComponentBase & {
type: 'textInput';
onInput?: (v: string) => void;
onInput?: (v: string) => Promise<void>;
default?: string;
label?: string;
caption?: string;
@ -93,7 +102,7 @@ export type AsUiTextInput = AsUiComponentBase & {
export type AsUiNumberInput = AsUiComponentBase & {
type: 'numberInput';
onInput?: (v: number) => void;
onInput?: (v: number) => Promise<void>;
default?: number;
label?: string;
caption?: string;
@ -105,7 +114,7 @@ export type AsUiSelect = AsUiComponentBase & {
text: string;
value: string;
}[];
onChange?: (v: string) => void;
onChange?: (v: string) => Promise<void>;
default?: string;
label?: string;
caption?: string;
@ -140,11 +149,15 @@ export type AsUiPostForm = AsUiComponentBase & {
export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm;
type Options<T extends AsUiComponent> = T extends AsUiButtons
? Omit<T, 'id' | 'type' | 'buttons'> & { 'buttons'?: Options<AsUiButton>[] }
: Omit<T, 'id' | 'type'>;
export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
// TODO
}
function getRootOptions(def: values.Value | undefined): Omit<AsUiRoot, 'id' | 'type'> {
function getRootOptions(def: values.Value | undefined): Options<AsUiRoot> {
utils.assertObject(def);
const children = def.value.get('children');
@ -153,30 +166,32 @@ function getRootOptions(def: values.Value | undefined): Omit<AsUiRoot, 'id' | 't
return {
children: children.value.map(v => {
utils.assertObject(v);
return v.value.get('id').value;
const id = v.value.get('id');
utils.assertString(id);
return id.value;
}),
};
}
function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, 'id' | 'type'> {
function getContainerOptions(def: values.Value | undefined): Options<AsUiContainer> {
utils.assertObject(def);
const children = def.value.get('children');
if (children) utils.assertArray(children);
const align = def.value.get('align');
if (align) utils.assertString(align);
if (align) assertStringAndIsIn(align, ALIGNS);
const bgColor = def.value.get('bgColor');
if (bgColor) utils.assertString(bgColor);
const fgColor = def.value.get('fgColor');
if (fgColor) utils.assertString(fgColor);
const font = def.value.get('font');
if (font) utils.assertString(font);
if (font) assertStringAndIsIn(font, FONTS);
const borderWidth = def.value.get('borderWidth');
if (borderWidth) utils.assertNumber(borderWidth);
const borderColor = def.value.get('borderColor');
if (borderColor) utils.assertString(borderColor);
const borderStyle = def.value.get('borderStyle');
if (borderStyle) utils.assertString(borderStyle);
if (borderStyle) assertStringAndIsIn(borderStyle, BORDER_STYLES);
const borderRadius = def.value.get('borderRadius');
if (borderRadius) utils.assertNumber(borderRadius);
const padding = def.value.get('padding');
@ -189,7 +204,9 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer,
return {
children: children ? children.value.map(v => {
utils.assertObject(v);
return v.value.get('id').value;
const id = v.value.get('id');
utils.assertString(id);
return id.value;
}) : [],
align: align?.value,
fgColor: fgColor?.value,
@ -205,7 +222,7 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer,
};
}
function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 'type'> {
function getTextOptions(def: values.Value | undefined): Options<AsUiText> {
utils.assertObject(def);
const text = def.value.get('text');
@ -217,7 +234,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't
const color = def.value.get('color');
if (color) utils.assertString(color);
const font = def.value.get('font');
if (font) utils.assertString(font);
if (font) assertStringAndIsIn(font, FONTS);
return {
text: text?.value,
@ -228,7 +245,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't
};
}
function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiMfm, 'id' | 'type'> {
function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiMfm> {
utils.assertObject(def);
const text = def.value.get('text');
@ -240,7 +257,7 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg
const color = def.value.get('color');
if (color) utils.assertString(color);
const font = def.value.get('font');
if (font) utils.assertString(font);
if (font) assertStringAndIsIn(font, FONTS);
const onClickEv = def.value.get('onClickEv');
if (onClickEv) utils.assertFunction(onClickEv);
@ -250,13 +267,13 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg
bold: bold?.value,
color: color?.value,
font: font?.value,
onClickEv: (evId: string) => {
if (onClickEv) call(onClickEv, [values.STR(evId)]);
onClickEv: async (evId: string) => {
if (onClickEv) await call(onClickEv, [values.STR(evId)]);
},
};
}
function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextInput, 'id' | 'type'> {
function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextInput> {
utils.assertObject(def);
const onInput = def.value.get('onInput');
@ -269,8 +286,8 @@ function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VF
if (caption) utils.assertString(caption);
return {
onInput: (v) => {
if (onInput) call(onInput, [utils.jsToVal(v)]);
onInput: async (v) => {
if (onInput) await call(onInput, [utils.jsToVal(v)]);
},
default: defaultValue?.value,
label: label?.value,
@ -278,7 +295,7 @@ function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VF
};
}
function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextarea, 'id' | 'type'> {
function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextarea> {
utils.assertObject(def);
const onInput = def.value.get('onInput');
@ -291,8 +308,8 @@ function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn
if (caption) utils.assertString(caption);
return {
onInput: (v) => {
if (onInput) call(onInput, [utils.jsToVal(v)]);
onInput: async (v) => {
if (onInput) await call(onInput, [utils.jsToVal(v)]);
},
default: defaultValue?.value,
label: label?.value,
@ -300,7 +317,7 @@ function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn
};
}
function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiNumberInput, 'id' | 'type'> {
function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiNumberInput> {
utils.assertObject(def);
const onInput = def.value.get('onInput');
@ -313,8 +330,8 @@ function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.
if (caption) utils.assertString(caption);
return {
onInput: (v) => {
if (onInput) call(onInput, [utils.jsToVal(v)]);
onInput: async (v) => {
if (onInput) await call(onInput, [utils.jsToVal(v)]);
},
default: defaultValue?.value,
label: label?.value,
@ -322,7 +339,7 @@ function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.
};
}
function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButton, 'id' | 'type'> {
function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButton> {
utils.assertObject(def);
const text = def.value.get('text');
@ -338,8 +355,8 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn,
return {
text: text?.value,
onClick: () => {
if (onClick) call(onClick, []);
onClick: async () => {
if (onClick) await call(onClick, []);
},
primary: primary?.value,
rounded: rounded?.value,
@ -347,7 +364,7 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn,
};
}
function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButtons, 'id' | 'type'> {
function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButtons> {
utils.assertObject(def);
const buttons = def.value.get('buttons');
@ -369,8 +386,8 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn,
return {
text: text.value,
onClick: () => {
call(onClick, []);
onClick: async () => {
await call(onClick, []);
},
primary: primary?.value,
rounded: rounded?.value,
@ -380,7 +397,7 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn,
};
}
function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSwitch, 'id' | 'type'> {
function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSwitch> {
utils.assertObject(def);
const onChange = def.value.get('onChange');
@ -393,8 +410,8 @@ function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn,
if (caption) utils.assertString(caption);
return {
onChange: (v) => {
if (onChange) call(onChange, [utils.jsToVal(v)]);
onChange: async (v) => {
if (onChange) await call(onChange, [utils.jsToVal(v)]);
},
default: defaultValue?.value,
label: label?.value,
@ -402,7 +419,7 @@ function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn,
};
}
function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSelect, 'id' | 'type'> {
function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSelect> {
utils.assertObject(def);
const items = def.value.get('items');
@ -428,8 +445,8 @@ function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn,
value: value ? value.value : text.value,
};
}) : [],
onChange: (v) => {
if (onChange) call(onChange, [utils.jsToVal(v)]);
onChange: async (v) => {
if (onChange) await call(onChange, [utils.jsToVal(v)]);
},
default: defaultValue?.value,
label: label?.value,
@ -437,7 +454,7 @@ function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn,
};
}
function getFolderOptions(def: values.Value | undefined): Omit<AsUiFolder, 'id' | 'type'> {
function getFolderOptions(def: values.Value | undefined): Options<AsUiFolder> {
utils.assertObject(def);
const children = def.value.get('children');
@ -450,7 +467,9 @@ function getFolderOptions(def: values.Value | undefined): Omit<AsUiFolder, 'id'
return {
children: children ? children.value.map(v => {
utils.assertObject(v);
return v.value.get('id').value;
const id = v.value.get('id');
utils.assertString(id);
return id.value;
}) : [],
title: title?.value ?? '',
opened: opened?.value ?? true,
@ -475,7 +494,7 @@ function getPostFormProps(form: values.VObj): PostFormPropsForAsUi {
};
}
function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostFormButton, 'id' | 'type'> {
function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostFormButton> {
utils.assertObject(def);
const text = def.value.get('text');
@ -497,7 +516,7 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu
};
}
function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostForm, 'id' | 'type'> {
function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostForm> {
utils.assertObject(def);
const form = def.value.get('form');
@ -511,18 +530,26 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn
}
export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) {
type OptionsConverter<T extends AsUiComponent, C> = (def: values.Value | undefined, call: C) => Options<T>;
const instances = {};
function createComponentInstance(type: AsUiComponent['type'], def: values.Value | undefined, id: values.Value | undefined, getOptions: (def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) => any, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
function createComponentInstance<T extends AsUiComponent, C>(
type: T['type'],
def: values.Value | undefined,
id: values.Value | undefined,
getOptions: OptionsConverter<T, C>,
call: C,
) {
if (id) utils.assertString(id);
const _id = id?.value ?? uuid();
const component = ref({
...getOptions(def, call),
type,
id: _id,
});
} as T);
components.push(component);
const instance = values.OBJ(new Map([
const instance = values.OBJ(new Map<string, values.Value>([
['id', values.STR(_id)],
['update', values.FN_NATIVE(([def], opts) => {
utils.assertObject(def);
@ -547,7 +574,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
'Ui:patch': values.FN_NATIVE(([id, val], opts) => {
utils.assertString(id);
utils.assertArray(val);
patch(id.value, val.value, opts.call);
// patch(id.value, val.value, opts.call); // TODO
}),
'Ui:get': values.FN_NATIVE(([id], opts) => {
@ -566,7 +593,9 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
rootComponent.value.children = children.value.map(v => {
utils.assertObject(v);
return v.value.get('id').value;
const id = v.value.get('id');
utils.assertString(id);
return id.value;
});
}),

View File

@ -5,19 +5,19 @@
import { defineAsyncComponent, Ref, ShallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import { claimAchievement } from './achievements.js';
import type { MenuItem } from '@/types/menu.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { url } from '@@/js/config.js';
import { defaultStore, noteActions } from '@/store.js';
import { miLocalStorage } from '@/local-storage.js';
import { getUserMenu } from '@/scripts/get-user-menu.js';
import { clipsCache, favoritedChannelsCache } from '@/cache.js';
import type { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { isSupportShare } from '@/scripts/navigator.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
@ -194,7 +194,7 @@ export function getNoteMenu(props: {
noteId: appearNote.id,
});
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) {
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
claimAchievement('noteDeletedWithin1min');
}
});
@ -213,7 +213,7 @@ export function getNoteMenu(props: {
os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) {
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
claimAchievement('noteDeletedWithin1min');
}
});

View File

@ -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;

View File

@ -9,12 +9,24 @@ import { apiUrl } from '@@/js/config.js';
import { $i } from '@/account.js';
export const pendingApiRequestsCount = ref(0);
export type Endpoint = keyof Misskey.Endpoints;
export type Request<E extends Endpoint> = Misskey.Endpoints[E]['req'];
export type AnyRequest<E extends Endpoint | (string & unknown)> =
(E extends Endpoint ? Request<E> : never) | object;
export type Response<E extends Endpoint | (string & unknown), P extends AnyRequest<E>> =
E extends Endpoint
? P extends Request<E> ? Misskey.api.SwitchCaseResponseType<E, P> : never
: object;
// Implements Misskey.api.ApiClient.request
export function misskeyApi<
ResT = void,
E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
E extends Endpoint | NonNullable<string> = Endpoint,
P extends AnyRequest<E> = E extends Endpoint ? Request<E> : never,
_ResT = ResT extends void ? Response<E, P> : ResT,
>(
endpoint: E,
data: P & { i?: string | null; } = {} as any,

View File

@ -2,22 +2,20 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'misskey-js';
import { $i } from '@/account.js';
const providedContextEl = document.getElementById('misskey_clientCtx');
export type ServerContext = {
clip?: Misskey.entities.Clip;
note?: Misskey.entities.Note;
user?: Misskey.entities.UserLite;
user?: Misskey.entities.UserDetailed;
} | null;
export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null;
export function getServerContext<K extends keyof NonNullable<ServerContext>>(entity: K): Required<Pick<NonNullable<ServerContext>, K>> | null {
// contextは非ログイン状態の情報しかないためログイン時は利用できない
if ($i) return null;
return serverContext ? (serverContext[entity] ?? null) : null;
export function assertServerContext<K extends keyof NonNullable<ServerContext>>(ctx: ServerContext, entity: K): ctx is Required<Pick<NonNullable<ServerContext>, K>> {
if (ctx == null) return false;
return entity in ctx && ctx[entity] != null;
}

View File

@ -0,0 +1,401 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { miLocalStorage } from '@/local-storage.js';
import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
test,
vi
} from 'vitest';
async function exe(script: string): Promise<values.Value[]> {
const outputs: values.Value[] = [];
const interpreter = new Interpreter(
createAiScriptEnv({ storageKey: 'widget' }),
{
in: aiScriptReadline,
out: (value) => {
outputs.push(value);
}
}
);
const ast = Parser.parse(script);
await interpreter.exec(ast);
return outputs;
}
let $iMock = vi.hoisted<Partial<typeof import('@/account.js').$i> | null >(
() => null
);
vi.mock('@/account.js', () => {
return {
get $i() {
return $iMock;
},
};
});
const osMock = vi.hoisted(() => {
return {
inputText: vi.fn(),
alert: vi.fn(),
confirm: vi.fn(),
};
});
vi.mock('@/os.js', () => {
return osMock;
});
const misskeyApiMock = vi.hoisted(() => vi.fn());
vi.mock('@/scripts/misskey-api.js', () => {
return { misskeyApi: misskeyApiMock };
});
describe('AiScript common API', () => {
afterAll(() => {
vi.unstubAllGlobals();
});
describe('readline', () => {
afterEach(() => {
vi.restoreAllMocks();
});
test.sequential('ok', async () => {
osMock.inputText.mockImplementationOnce(async ({ title }) => {
expect(title).toBe('question');
return {
canceled: false,
result: 'Hello',
};
});
const [res] = await exe(`
<: readline('question')
`);
expect(res).toStrictEqual(values.STR('Hello'));
expect(osMock.inputText).toHaveBeenCalledOnce();
});
test.sequential('cancelled', async () => {
osMock.inputText.mockImplementationOnce(async ({ title }) => {
expect(title).toBe('question');
return {
canceled: true,
result: undefined,
};
});
const [res] = await exe(`
<: readline('question')
`);
expect(res).toStrictEqual(values.STR(''));
expect(osMock.inputText).toHaveBeenCalledOnce();
});
});
describe('user constants', () => {
describe.sequential('logged in', () => {
beforeAll(() => {
$iMock = {
id: 'xxxxxxxx',
name: '藍',
username: 'ai',
};
});
test.concurrent('USER_ID', async () => {
const [res] = await exe(`
<: USER_ID
`);
expect(res).toStrictEqual(values.STR('xxxxxxxx'));
});
test.concurrent('USER_NAME', async () => {
const [res] = await exe(`
<: USER_NAME
`);
expect(res).toStrictEqual(values.STR('藍'));
});
test.concurrent('USER_USERNAME', async () => {
const [res] = await exe(`
<: USER_USERNAME
`);
expect(res).toStrictEqual(values.STR('ai'));
});
});
describe.sequential('not logged in', () => {
beforeAll(() => {
$iMock = null;
});
test.concurrent('USER_ID', async () => {
const [res] = await exe(`
<: USER_ID
`);
expect(res).toStrictEqual(values.NULL);
});
test.concurrent('USER_NAME', async () => {
const [res] = await exe(`
<: USER_NAME
`);
expect(res).toStrictEqual(values.NULL);
});
test.concurrent('USER_USERNAME', async () => {
const [res] = await exe(`
<: USER_USERNAME
`);
expect(res).toStrictEqual(values.NULL);
});
});
});
describe('dialog', () => {
afterEach(() => {
vi.restoreAllMocks();
});
test.sequential('ok', async () => {
osMock.alert.mockImplementationOnce(async ({ type, title, text }) => {
expect(type).toBe('success');
expect(title).toBe('Hello');
expect(text).toBe('world');
});
const [res] = await exe(`
<: Mk:dialog('Hello', 'world', 'success')
`);
expect(res).toStrictEqual(values.NULL);
expect(osMock.alert).toHaveBeenCalledOnce();
});
test.sequential('omit type', async () => {
osMock.alert.mockImplementationOnce(async ({ type, title, text }) => {
expect(type).toBe('info');
expect(title).toBe('Hello');
expect(text).toBe('world');
});
const [res] = await exe(`
<: Mk:dialog('Hello', 'world')
`);
expect(res).toStrictEqual(values.NULL);
expect(osMock.alert).toHaveBeenCalledOnce();
});
test.sequential('invalid type', async () => {
await expect(() => exe(`
<: Mk:dialog('Hello', 'world', 'invalid')
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
expect(osMock.alert).not.toHaveBeenCalled();
});
});
describe('confirm', () => {
afterEach(() => {
vi.restoreAllMocks();
});
test.sequential('ok', async () => {
osMock.confirm.mockImplementationOnce(async ({ type, title, text }) => {
expect(type).toBe('success');
expect(title).toBe('Hello');
expect(text).toBe('world');
return { canceled: false };
});
const [res] = await exe(`
<: Mk:confirm('Hello', 'world', 'success')
`);
expect(res).toStrictEqual(values.TRUE);
expect(osMock.confirm).toHaveBeenCalledOnce();
});
test.sequential('omit type', async () => {
osMock.confirm
.mockImplementationOnce(async ({ type, title, text }) => {
expect(type).toBe('question');
expect(title).toBe('Hello');
expect(text).toBe('world');
return { canceled: false };
});
const [res] = await exe(`
<: Mk:confirm('Hello', 'world')
`);
expect(res).toStrictEqual(values.TRUE);
expect(osMock.confirm).toHaveBeenCalledOnce();
});
test.sequential('canceled', async () => {
osMock.confirm.mockImplementationOnce(async ({ type, title, text }) => {
expect(type).toBe('question');
expect(title).toBe('Hello');
expect(text).toBe('world');
return { canceled: true };
});
const [res] = await exe(`
<: Mk:confirm('Hello', 'world')
`);
expect(res).toStrictEqual(values.FALSE);
expect(osMock.confirm).toHaveBeenCalledOnce();
});
test.sequential('invalid type', async () => {
const confirm = osMock.confirm;
await expect(() => exe(`
<: Mk:confirm('Hello', 'world', 'invalid')
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
expect(confirm).not.toHaveBeenCalled();
});
});
describe('api', () => {
afterEach(() => {
vi.restoreAllMocks();
});
test.sequential('successful', async () => {
misskeyApiMock.mockImplementationOnce(
async (endpoint, data, token) => {
expect(endpoint).toBe('ping');
expect(data).toStrictEqual({});
expect(token).toBeNull();
return { pong: 1735657200000 };
}
);
const [res] = await exe(`
<: Mk:api('ping', {})
`);
expect(res).toStrictEqual(values.OBJ(new Map([
['pong', values.NUM(1735657200000)],
])));
expect(misskeyApiMock).toHaveBeenCalledOnce();
});
test.sequential('with token', async () => {
misskeyApiMock.mockImplementationOnce(
async (endpoint, data, token) => {
expect(endpoint).toBe('ping');
expect(data).toStrictEqual({});
expect(token).toStrictEqual('xxxxxxxx');
return { pong: 1735657200000 };
}
);
const [res] = await exe(`
<: Mk:api('ping', {}, 'xxxxxxxx')
`);
expect(res).toStrictEqual(values.OBJ(new Map([
['pong', values.NUM(1735657200000 )],
])));
expect(misskeyApiMock).toHaveBeenCalledOnce();
});
test.sequential('request failed', async () => {
misskeyApiMock.mockRejectedValueOnce('Not Found');
const [res] = await exe(`
<: Mk:api('this/endpoint/should/not/be/found', {})
`);
expect(res).toStrictEqual(
values.ERROR('request_failed', values.STR('Not Found'))
);
expect(misskeyApiMock).toHaveBeenCalledOnce();
});
test.sequential('invalid endpoint', async () => {
await expect(() => exe(`
Mk:api('https://example.com/api/ping', {})
`)).rejects.toStrictEqual(
new errors.AiScriptRuntimeError('invalid endpoint'),
);
expect(misskeyApiMock).not.toHaveBeenCalled();
});
test.sequential('missing param', async () => {
await expect(() => exe(`
Mk:api('ping')
`)).rejects.toStrictEqual(
new errors.AiScriptRuntimeError('expected param'),
);
expect(misskeyApiMock).not.toHaveBeenCalled();
});
});
describe('save and load', () => {
beforeEach(() => {
miLocalStorage.removeItem('aiscript:widget:key');
});
afterEach(() => {
miLocalStorage.removeItem('aiscript:widget:key');
});
test.sequential('successful', async () => {
const [res] = await exe(`
Mk:save('key', 'value')
<: Mk:load('key')
`);
expect(miLocalStorage.getItem('aiscript:widget:key')).toBe('"value"');
expect(res).toStrictEqual(values.STR('value'));
});
test.sequential('missing value to save', async () => {
await expect(() => exe(`
Mk:save('key')
`)).rejects.toStrictEqual(
new errors.AiScriptRuntimeError('Expect anything, but got nothing.'),
);
});
test.sequential('not value found to load', async () => {
const [res] = await exe(`
<: Mk:load('key')
`);
expect(res).toStrictEqual(values.NULL);
});
test.sequential('remove existing', async () => {
const res = await exe(`
Mk:save('key', 'value')
<: Mk:load('key')
<: Mk:remove('key')
<: Mk:load('key')
`);
expect(res).toStrictEqual([values.STR('value'), values.NULL, values.NULL]);
});
test.sequential('remove nothing', async () => {
const res = await exe(`
<: Mk:load('key')
<: Mk:remove('key')
<: Mk:load('key')
`);
expect(res).toStrictEqual([values.NULL, values.NULL, values.NULL]);
});
});
test.concurrent('url', async () => {
vi.stubGlobal('location', { href: 'https://example.com/' });
const [res] = await exe(`
<: Mk:url()
`);
expect(res).toStrictEqual(values.STR('https://example.com/'));
});
test.concurrent('nyaize', async () => {
const [res] = await exe(`
<: Mk:nyaize('な')
`);
expect(res).toStrictEqual(values.STR('にゃ'));
});
});

View File

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { assertStringAndIsIn } from "@/scripts/aiscript/common.js";
import { values } from "@syuilo/aiscript";
import { describe, expect, test } from "vitest";
describe('AiScript common script', () => {
test('assertStringAndIsIn', () => {
expect(
() => assertStringAndIsIn(values.STR('a'), ['a', 'b'])
).not.toThrow();
expect(
() => assertStringAndIsIn(values.STR('c'), ['a', 'b'])
).toThrow('"c" is not in "a", "b"');
expect(() => assertStringAndIsIn(
values.STR('invalid'),
['left', 'center', 'right']
)).toThrow('"invalid" is not in "left", "center", "right"');
});
});

View File

@ -0,0 +1,825 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
import { describe, expect, test } from 'vitest';
import { type Ref, ref } from 'vue';
import type {
AsUiButton,
AsUiButtons,
AsUiComponent,
AsUiMfm,
AsUiNumberInput,
AsUiRoot,
AsUiSelect,
AsUiSwitch,
AsUiText,
AsUiTextarea,
AsUiTextInput,
} from '@/scripts/aiscript/ui.js';
type ExeResult = {
root: AsUiRoot;
get: (id: string) => AsUiComponent;
outputs: values.Value[];
}
async function exe(script: string): Promise<ExeResult> {
const rootRef = ref<AsUiRoot>();
const componentRefs = ref<Ref<AsUiComponent>[]>([]);
const outputs: values.Value[] = [];
const interpreter = new Interpreter(
registerAsUiLib(componentRefs.value, (root) => {
rootRef.value = root.value;
}),
{
out: (value) => {
outputs.push(value);
}
}
);
const ast = Parser.parse(script);
await interpreter.exec(ast);
const root = rootRef.value;
if (root === undefined) {
expect.unreachable('root must not be undefined');
}
const components = componentRefs.value.map(
(componentRef) => componentRef.value,
);
expect(root).toBe(components[0]);
expect(root.type).toBe('root');
const get = (id: string) => {
const component = componentRefs.value.find(
(componentRef) => componentRef.value.id === id,
);
if (component === undefined) {
expect.unreachable(`component "${id}" is not defined`);
}
return component.value;
};
return { root, get, outputs };
}
describe('AiScript UI API', () => {
test.concurrent('root', async () => {
const { root } = await exe('');
expect(root.children).toStrictEqual([]);
});
describe('get', () => {
test.concurrent('some', async () => {
const { outputs } = await exe(`
Ui:C:text({}, 'id')
<: Ui:get('id')
`);
const output = outputs[0] as values.VObj;
expect(output.type).toBe('obj');
expect(output.value.size).toBe(2);
expect(output.value.get('id')).toStrictEqual(values.STR('id'));
expect(output.value.get('update')!.type).toBe('fn');
});
test.concurrent('none', async () => {
const { outputs } = await exe(`
<: Ui:get('id')
`);
expect(outputs).toStrictEqual([values.NULL]);
});
});
describe('update', () => {
test.concurrent('normal', async () => {
const { get } = await exe(`
let text = Ui:C:text({ text: 'a' }, 'id')
text.update({ text: 'b' })
`);
const text = get('id') as AsUiText;
expect(text.text).toBe('b');
});
test.concurrent('skip unknown key', async () => {
const { get } = await exe(`
let text = Ui:C:text({ text: 'a' }, 'id')
text.update({
text: 'b'
unknown: null
})
`);
const text = get('id') as AsUiText;
expect(text.text).toBe('b');
expect('unknown' in text).toBeFalsy();
});
});
describe('container', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let text = Ui:C:text({
text: 'text'
}, 'id1')
let container = Ui:C:container({
children: [text]
align: 'left'
bgColor: '#fff'
fgColor: '#000'
font: 'sans-serif'
borderWidth: 1
borderColor: '#f00'
borderStyle: 'hidden'
borderRadius: 2
padding: 3
rounded: true
hidden: false
}, 'id2')
Ui:render([container])
`);
expect(root.children).toStrictEqual(['id2']);
expect(get('id2')).toStrictEqual({
type: 'container',
id: 'id2',
children: ['id1'],
align: 'left',
bgColor: '#fff',
fgColor: '#000',
font: 'sans-serif',
borderColor: '#f00',
borderWidth: 1,
borderStyle: 'hidden',
borderRadius: 2,
padding: 3,
rounded: true,
hidden: false,
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:container({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'container',
id: 'id',
children: [],
align: undefined,
fgColor: undefined,
bgColor: undefined,
font: undefined,
borderWidth: undefined,
borderColor: undefined,
borderStyle: undefined,
borderRadius: undefined,
padding: undefined,
rounded: undefined,
hidden: undefined,
});
});
test.concurrent('invalid children', async () => {
await expect(() => exe(`
Ui:C:container({
children: 0
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
test.concurrent('invalid align', async () => {
await expect(() => exe(`
Ui:C:container({
align: 'invalid'
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
test.concurrent('invalid font', async () => {
await expect(() => exe(`
Ui:C:container({
font: 'invalid'
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
test.concurrent('invalid borderStyle', async () => {
await expect(() => exe(`
Ui:C:container({
borderStyle: 'invalid'
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
});
describe('text', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let text = Ui:C:text({
text: 'a'
size: 1
bold: true
color: '#000'
font: 'sans-serif'
}, 'id')
Ui:render([text])
`);
expect(root.children).toStrictEqual(['id']);
expect(get('id')).toStrictEqual({
type: 'text',
id: 'id',
text: 'a',
size: 1,
bold: true,
color: '#000',
font: 'sans-serif',
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:text({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'text',
id: 'id',
text: undefined,
size: undefined,
bold: undefined,
color: undefined,
font: undefined,
});
});
test.concurrent('invalid font', async () => {
await expect(() => exe(`
Ui:C:text({
font: 'invalid'
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
});
describe('mfm', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let mfm = Ui:C:mfm({
text: 'text'
size: 1
bold: true
color: '#000'
font: 'sans-serif'
onClickEv: print
}, 'id')
Ui:render([mfm])
`);
expect(root.children).toStrictEqual(['id']);
const { onClickEv, ...mfm } = get('id') as AsUiMfm;
expect(mfm).toStrictEqual({
type: 'mfm',
id: 'id',
text: 'text',
size: 1,
bold: true,
color: '#000',
font: 'sans-serif',
});
await onClickEv!('a');
expect(outputs).toStrictEqual([values.STR('a')]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:mfm({}, 'id')
`);
const { onClickEv, ...mfm } = get('id') as AsUiMfm;
expect(onClickEv).toBeTypeOf('function');
expect(mfm).toStrictEqual({
type: 'mfm',
id: 'id',
text: undefined,
size: undefined,
bold: undefined,
color: undefined,
font: undefined,
});
});
test.concurrent('invalid font', async () => {
await expect(() => exe(`
Ui:C:mfm({
font: 'invalid'
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
});
describe('textInput', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let text_input = Ui:C:textInput({
onInput: print
default: 'a'
label: 'b'
caption: 'c'
}, 'id')
Ui:render([text_input])
`);
expect(root.children).toStrictEqual(['id']);
const { onInput, ...textInput } = get('id') as AsUiTextInput;
expect(textInput).toStrictEqual({
type: 'textInput',
id: 'id',
default: 'a',
label: 'b',
caption: 'c',
});
await onInput!('d');
expect(outputs).toStrictEqual([values.STR('d')]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:textInput({}, 'id')
`);
const { onInput, ...textInput } = get('id') as AsUiTextInput;
expect(onInput).toBeTypeOf('function');
expect(textInput).toStrictEqual({
type: 'textInput',
id: 'id',
default: undefined,
label: undefined,
caption: undefined,
});
});
});
describe('textarea', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let textarea = Ui:C:textarea({
onInput: print
default: 'a'
label: 'b'
caption: 'c'
}, 'id')
Ui:render([textarea])
`);
expect(root.children).toStrictEqual(['id']);
const { onInput, ...textarea } = get('id') as AsUiTextarea;
expect(textarea).toStrictEqual({
type: 'textarea',
id: 'id',
default: 'a',
label: 'b',
caption: 'c',
});
await onInput!('d');
expect(outputs).toStrictEqual([values.STR('d')]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:textarea({}, 'id')
`);
const { onInput, ...textarea } = get('id') as AsUiTextarea;
expect(onInput).toBeTypeOf('function');
expect(textarea).toStrictEqual({
type: 'textarea',
id: 'id',
default: undefined,
label: undefined,
caption: undefined,
});
});
});
describe('numberInput', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let number_input = Ui:C:numberInput({
onInput: print
default: 1
label: 'a'
caption: 'b'
}, 'id')
Ui:render([number_input])
`);
expect(root.children).toStrictEqual(['id']);
const { onInput, ...numberInput } = get('id') as AsUiNumberInput;
expect(numberInput).toStrictEqual({
type: 'numberInput',
id: 'id',
default: 1,
label: 'a',
caption: 'b',
});
await onInput!(2);
expect(outputs).toStrictEqual([values.NUM(2)]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:numberInput({}, 'id')
`);
const { onInput, ...numberInput } = get('id') as AsUiNumberInput;
expect(onInput).toBeTypeOf('function');
expect(numberInput).toStrictEqual({
type: 'numberInput',
id: 'id',
default: undefined,
label: undefined,
caption: undefined,
});
});
});
describe('button', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let button = Ui:C:button({
text: 'a'
onClick: @() { <: 'clicked' }
primary: true
rounded: false
disabled: false
}, 'id')
Ui:render([button])
`);
expect(root.children).toStrictEqual(['id']);
const { onClick, ...button } = get('id') as AsUiButton;
expect(button).toStrictEqual({
type: 'button',
id: 'id',
text: 'a',
primary: true,
rounded: false,
disabled: false,
});
await onClick!();
expect(outputs).toStrictEqual([values.STR('clicked')]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:button({}, 'id')
`);
const { onClick, ...button } = get('id') as AsUiButton;
expect(onClick).toBeTypeOf('function');
expect(button).toStrictEqual({
type: 'button',
id: 'id',
text: undefined,
primary: undefined,
rounded: undefined,
disabled: undefined,
});
});
});
describe('buttons', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let buttons = Ui:C:buttons({
buttons: []
}, 'id')
Ui:render([buttons])
`);
expect(root.children).toStrictEqual(['id']);
expect(get('id')).toStrictEqual({
type: 'buttons',
id: 'id',
buttons: [],
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:buttons({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'buttons',
id: 'id',
buttons: [],
});
});
test.concurrent('some buttons', async () => {
const { root, get, outputs } = await exe(`
let buttons = Ui:C:buttons({
buttons: [
{
text: 'a'
onClick: @() { <: 'clicked a' }
primary: true
rounded: false
disabled: false
}
{
text: 'b'
onClick: @() { <: 'clicked b' }
primary: true
rounded: false
disabled: false
}
]
}, 'id')
Ui:render([buttons])
`);
expect(root.children).toStrictEqual(['id']);
const { buttons, ...buttonsOptions } = get('id') as AsUiButtons;
expect(buttonsOptions).toStrictEqual({
type: 'buttons',
id: 'id',
});
expect(buttons!.length).toBe(2);
const { onClick: onClickA, ...buttonA } = buttons![0];
expect(buttonA).toStrictEqual({
text: 'a',
primary: true,
rounded: false,
disabled: false,
});
const { onClick: onClickB, ...buttonB } = buttons![1];
expect(buttonB).toStrictEqual({
text: 'b',
primary: true,
rounded: false,
disabled: false,
});
await onClickA!();
await onClickB!();
expect(outputs).toStrictEqual(
[values.STR('clicked a'), values.STR('clicked b')]
);
});
});
describe('switch', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let switch = Ui:C:switch({
onChange: print
default: false
label: 'a'
caption: 'b'
}, 'id')
Ui:render([switch])
`);
expect(root.children).toStrictEqual(['id']);
const { onChange, ...switchOptions } = get('id') as AsUiSwitch;
expect(switchOptions).toStrictEqual({
type: 'switch',
id: 'id',
default: false,
label: 'a',
caption: 'b',
});
await onChange!(true);
expect(outputs).toStrictEqual([values.TRUE]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:switch({}, 'id')
`);
const { onChange, ...switchOptions } = get('id') as AsUiSwitch;
expect(onChange).toBeTypeOf('function');
expect(switchOptions).toStrictEqual({
type: 'switch',
id: 'id',
default: undefined,
label: undefined,
caption: undefined,
});
});
});
describe('select', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let select = Ui:C:select({
items: [
{ text: 'A', value: 'a' }
{ text: 'B', value: 'b' }
]
onChange: print
default: 'a'
label: 'c'
caption: 'd'
}, 'id')
Ui:render([select])
`);
expect(root.children).toStrictEqual(['id']);
const { onChange, ...select } = get('id') as AsUiSelect;
expect(select).toStrictEqual({
type: 'select',
id: 'id',
items: [
{ text: 'A', value: 'a' },
{ text: 'B', value: 'b' },
],
default: 'a',
label: 'c',
caption: 'd',
});
await onChange!('b');
expect(outputs).toStrictEqual([values.STR('b')]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:select({}, 'id')
`);
const { onChange, ...select } = get('id') as AsUiSelect;
expect(onChange).toBeTypeOf('function');
expect(select).toStrictEqual({
type: 'select',
id: 'id',
items: [],
default: undefined,
label: undefined,
caption: undefined,
});
});
test.concurrent('omit item values', async () => {
const { get } = await exe(`
let select = Ui:C:select({
items: [
{ text: 'A' }
{ text: 'B' }
]
}, 'id')
`);
const { onChange, ...select } = get('id') as AsUiSelect;
expect(onChange).toBeTypeOf('function');
expect(select).toStrictEqual({
type: 'select',
id: 'id',
items: [
{ text: 'A', value: 'A' },
{ text: 'B', value: 'B' },
],
default: undefined,
label: undefined,
caption: undefined,
});
});
});
describe('folder', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let folder = Ui:C:folder({
children: []
title: 'a'
opened: true
}, 'id')
Ui:render([folder])
`);
expect(root.children).toStrictEqual(['id']);
expect(get('id')).toStrictEqual({
type: 'folder',
id: 'id',
children: [],
title: 'a',
opened: true,
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:folder({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'folder',
id: 'id',
children: [],
title: '',
opened: true,
});
});
test.concurrent('some children', async () => {
const { get } = await exe(`
let text = Ui:C:text({
text: 'text'
}, 'id1')
Ui:C:folder({
children: [text]
}, 'id2')
`);
expect(get('id2')).toStrictEqual({
type: 'folder',
id: 'id2',
children: ['id1'],
title: '',
opened: true,
});
});
});
describe('postFormButton', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let post_form_button = Ui:C:postFormButton({
text: 'a'
primary: true
rounded: false
form: {
text: 'b'
cw: 'c'
visibility: 'public'
localOnly: true
}
}, 'id')
Ui:render([post_form_button])
`);
expect(root.children).toStrictEqual(['id']);
expect(get('id')).toStrictEqual({
type: 'postFormButton',
id: 'id',
text: 'a',
primary: true,
rounded: false,
form: {
text: 'b',
cw: 'c',
visibility: 'public',
localOnly: true,
},
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:postFormButton({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'postFormButton',
id: 'id',
text: undefined,
primary: undefined,
rounded: undefined,
form: { text: '' },
});
});
});
describe('postForm', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let post_form = Ui:C:postForm({
form: {
text: 'a'
cw: 'b'
visibility: 'public'
localOnly: true
}
}, 'id')
Ui:render([post_form])
`);
expect(root.children).toStrictEqual(['id']);
expect(get('id')).toStrictEqual({
type: 'postForm',
id: 'id',
form: {
text: 'a',
cw: 'b',
visibility: 'public',
localOnly: true,
},
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:postForm({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'postForm',
id: 'id',
form: { text: '' },
});
});
test.concurrent('minimum options for form', async () => {
const { get } = await exe(`
Ui:C:postForm({
form: { text: '' }
}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'postForm',
id: 'id',
form: {
text: '',
cw: undefined,
visibility: undefined,
localOnly: undefined,
},
});
});
});
});

View File

@ -10,8 +10,8 @@
"declaration": false,
"sourceMap": false,
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"module": "ES2022",
"moduleResolution": "Bundler",
"removeComments": false,
"noLib": false,
"strict": true,

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021-2024 syuilo and other contributors
Copyright (c) 2021-2025 syuilo and other 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

View File

@ -6,8 +6,8 @@
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import { EventEmitter } from 'eventemitter3';
import { Options } from 'reconnecting-websocket';
import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
import _ReconnectingWebsocket from 'reconnecting-websocket';
// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
//
@ -3150,7 +3150,7 @@ export class Stream extends EventEmitter<StreamEvents> implements IStream {
constructor(origin: string, user: {
token: string;
} | null, options?: {
WebSocket?: _ReconnectingWebsocket.Options['WebSocket'];
WebSocket?: Options['WebSocket'];
});
// (undocumented)
close(): void;

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.11.0",
"version": "2024.11.1-alpha.0",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@ -44,7 +44,7 @@ export class APIClient {
credential?: APIClient['credential'];
fetch?: APIClient['fetch'] | null | undefined;
}) {
this.origin = opts.origin;
this.origin = opts.origin.replace(/\/$/, '');
this.credential = opts.credential;
// ネイティブ関数をそのまま変数に代入して使おうとするとChromiumではIllegal invocationエラーが発生するため、
// 環境で実装されているfetchを使う場合は無名関数でラップして使用する

View File

@ -1,8 +1,10 @@
import { EventEmitter } from 'eventemitter3';
import _ReconnectingWebsocket from 'reconnecting-websocket';
import _ReconnectingWebSocket, { Options } from 'reconnecting-websocket';
import type { BroadcastEvents, Channels } from './streaming.types.js';
const ReconnectingWebsocket = _ReconnectingWebsocket as unknown as typeof _ReconnectingWebsocket['default'];
// コンストラクタとクラスそのものの定義が上手く解決出来ないため再定義
const ReconnectingWebSocketConstructor = _ReconnectingWebSocket as unknown as typeof _ReconnectingWebSocket.default;
type ReconnectingWebSocket = _ReconnectingWebSocket.default;
export function urlQuery(obj: Record<string, string | number | boolean | undefined>): string {
const params = Object.entries(obj)
@ -43,7 +45,7 @@ export interface IStream extends EventEmitter<StreamEvents> {
*/
// eslint-disable-next-line import/no-default-export
export default class Stream extends EventEmitter<StreamEvents> implements IStream {
private stream: _ReconnectingWebsocket.default;
private stream: ReconnectingWebSocket;
public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
private sharedConnectionPools: Pool[] = [];
private sharedConnections: SharedConnection[] = [];
@ -51,7 +53,7 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
private idCounter = 0;
constructor(origin: string, user: { token: string; } | null, options?: {
WebSocket?: _ReconnectingWebsocket.Options['WebSocket'];
WebSocket?: Options['WebSocket'];
}) {
super();
@ -80,7 +82,7 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
const wsOrigin = origin.replace('http://', 'ws://').replace('https://', 'wss://');
this.stream = new ReconnectingWebsocket(`${wsOrigin}/streaming?${query}`, '', {
this.stream = new ReconnectingWebSocketConstructor(`${wsOrigin}/streaming?${query}`, '', {
minReconnectionDelay: 1, // https://github.com/pladaria/reconnecting-websocket/issues/91
WebSocket: options.WebSocket,
});

View File

@ -142,7 +142,7 @@ importers:
version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/testing':
specifier: 10.4.7
version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7))
version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)
'@peertube/http-signature':
specifier: 1.7.0
version: 1.7.0
@ -1163,7 +1163,7 @@ importers:
version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
'@vitest/coverage-v8':
specifier: 1.6.0
version: 1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0))
version: 1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0))
'@vue/runtime-core':
specifier: 3.5.12
version: 3.5.12
@ -1240,6 +1240,9 @@ importers:
eslint-plugin-vue:
specifier: 9.31.0
version: 9.31.0(eslint@9.14.0)
nodemon:
specifier: 3.1.7
version: 3.1.7
typescript:
specifier: 5.6.3
version: 5.6.3
@ -10942,6 +10945,9 @@ packages:
vue-component-type-helpers@2.1.10:
resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==}
vue-component-type-helpers@2.2.0:
resolution: {integrity: sha512-cYrAnv2me7bPDcg9kIcGwjJiSB6Qyi08+jLDo9yuvoFQjzHiPTzML7RnkJB1+3P6KMsX/KbCD4QE3Tv/knEllw==}
vue-demi@0.14.7:
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'}
@ -11780,7 +11786,7 @@ snapshots:
'@babel/traverse': 7.23.5
'@babel/types': 7.24.7
convert-source-map: 2.0.0
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@ -11800,7 +11806,7 @@ snapshots:
'@babel/traverse': 7.24.7
'@babel/types': 7.24.7
convert-source-map: 2.0.0
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@ -12059,7 +12065,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.25.6
'@babel/types': 7.24.7
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -12074,7 +12080,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.24.7
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -12465,7 +12471,7 @@ snapshots:
'@eslint/config-array@0.18.0':
dependencies:
'@eslint/object-schema': 2.1.4
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@ -12475,7 +12481,7 @@ snapshots:
'@eslint/eslintrc@3.1.0':
dependencies:
ajv: 6.12.6
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
espree: 10.3.0
globals: 14.0.0
ignore: 5.3.1
@ -12985,7 +12991,7 @@ snapshots:
nopt: 5.0.0
npmlog: 5.0.1
rimraf: 3.0.2
semver: 7.6.0
semver: 7.6.3
tar: 6.2.1
transitivePeerDependencies:
- encoding
@ -13180,7 +13186,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@nestjs/testing@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7))':
'@nestjs/testing@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)':
dependencies:
'@nestjs/common': 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
@ -14554,7 +14560,7 @@ snapshots:
ts-dedent: 2.2.0
type-fest: 2.19.0
vue: 3.5.12(typescript@5.6.3)
vue-component-type-helpers: 2.1.10
vue-component-type-helpers: 2.2.0
'@swc/cli@0.3.12(@swc/core@1.9.2)(chokidar@3.5.3)':
dependencies:
@ -15264,7 +15270,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 7.1.0(typescript@5.6.3)
'@typescript-eslint/utils': 7.1.0(eslint@9.14.0)(typescript@5.6.3)
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
eslint: 9.14.0
ts-api-utils: 1.0.1(typescript@5.6.3)
optionalDependencies:
@ -15276,7 +15282,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.3)
'@typescript-eslint/utils': 7.17.0(eslint@9.14.0)(typescript@5.6.3)
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
eslint: 9.14.0
ts-api-utils: 1.3.0(typescript@5.6.3)
optionalDependencies:
@ -15292,11 +15298,11 @@ snapshots:
dependencies:
'@typescript-eslint/types': 7.1.0
'@typescript-eslint/visitor-keys': 7.1.0
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
semver: 7.6.0
semver: 7.6.3
ts-api-utils: 1.0.1(typescript@5.6.3)
optionalDependencies:
typescript: 5.6.3
@ -15307,11 +15313,11 @@ snapshots:
dependencies:
'@typescript-eslint/types': 7.17.0
'@typescript-eslint/visitor-keys': 7.17.0
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.4
semver: 7.6.0
semver: 7.6.3
ts-api-utils: 1.3.0(typescript@5.6.3)
optionalDependencies:
typescript: 5.6.3
@ -15327,7 +15333,7 @@ snapshots:
'@typescript-eslint/types': 7.1.0
'@typescript-eslint/typescript-estree': 7.1.0(typescript@5.6.3)
eslint: 9.14.0
semver: 7.6.0
semver: 7.6.3
transitivePeerDependencies:
- supports-color
- typescript
@ -15384,7 +15390,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0))':
'@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0))':
dependencies:
'@ampproject/remapping': 2.2.1
'@bcoe/v8-coverage': 0.2.3
@ -15399,7 +15405,7 @@ snapshots:
std-env: 3.7.0
strip-literal: 2.1.0
test-exclude: 6.0.0
vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0)
vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0)
transitivePeerDependencies:
- supports-color
@ -15637,14 +15643,14 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
optional: true
agent-base@7.1.0:
dependencies:
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -17248,7 +17254,7 @@ snapshots:
esbuild-register@3.5.0(esbuild@0.24.0):
dependencies:
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
esbuild: 0.24.0
transitivePeerDependencies:
- supports-color
@ -17490,7 +17496,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
escape-string-regexp: 4.0.0
eslint-scope: 8.2.0
eslint-visitor-keys: 4.2.0
@ -17935,7 +17941,7 @@ snapshots:
follow-redirects@1.15.9(debug@4.3.7):
optionalDependencies:
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
for-each@0.3.3:
dependencies:
@ -18138,7 +18144,7 @@ snapshots:
dependencies:
foreground-child: 3.1.1
jackspeak: 2.3.6
minimatch: 9.0.3
minimatch: 9.0.4
minipass: 7.0.4
path-scurry: 1.10.1
@ -18405,7 +18411,7 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.0
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -18444,7 +18450,7 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
optional: true
@ -18452,14 +18458,14 @@ snapshots:
https-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.0
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.5:
dependencies:
agent-base: 7.1.0
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -18805,7 +18811,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@ -18814,7 +18820,7 @@ snapshots:
istanbul-lib-source-maps@5.0.4:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@ -19108,7 +19114,7 @@ snapshots:
jest-util: 29.7.0
natural-compare: 1.4.0
pretty-format: 29.7.0
semver: 7.6.0
semver: 7.6.3
transitivePeerDependencies:
- supports-color
@ -19215,35 +19221,6 @@ snapshots:
jsdoc-type-pratt-parser@4.1.0: {}
jsdom@24.1.1:
dependencies:
cssstyle: 4.0.1
data-urls: 5.0.0
decimal.js: 10.4.3
form-data: 4.0.1
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.5
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.12
parse5: 7.2.1
rrweb-cssom: 0.7.1
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 4.1.4
w3c-xmlserializer: 5.0.0
webidl-conversions: 7.0.0
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.0.0
ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
optional: true
jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3):
dependencies:
cssstyle: 4.0.1
@ -19936,7 +19913,7 @@ snapshots:
micromark@4.0.0:
dependencies:
'@types/debug': 4.1.12
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
decode-named-character-reference: 1.0.2
devlop: 1.1.0
micromark-core-commonmark: 2.0.0
@ -20276,7 +20253,7 @@ snapshots:
make-fetch-happen: 13.0.0
nopt: 7.2.0
proc-log: 4.2.0
semver: 7.6.0
semver: 7.6.3
tar: 6.2.1
which: 4.0.0
transitivePeerDependencies:
@ -21396,7 +21373,7 @@ snapshots:
require-in-the-middle@7.3.0:
dependencies:
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
module-details-from-path: 1.0.3
resolve: 1.22.8
transitivePeerDependencies:
@ -21732,7 +21709,7 @@ snapshots:
simple-update-notifier@2.0.0:
dependencies:
semver: 7.6.0
semver: 7.6.3
sinon@16.1.3:
dependencies:
@ -21821,7 +21798,7 @@ snapshots:
socks-proxy-agent@8.0.2:
dependencies:
agent-base: 7.1.0
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
socks: 2.7.1
transitivePeerDependencies:
- supports-color
@ -21930,7 +21907,7 @@ snapshots:
arg: 5.0.2
bluebird: 3.7.2
check-more-types: 2.24.0
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
execa: 5.1.1
lazy-ass: 1.6.0
ps-tree: 1.2.0
@ -22677,7 +22654,7 @@ snapshots:
vite-node@1.6.0(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0):
dependencies:
cac: 6.7.14
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
pathe: 1.1.2
picocolors: 1.0.1
vite: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0)
@ -22695,7 +22672,7 @@ snapshots:
vite-node@1.6.0(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0):
dependencies:
cac: 6.7.14
debug: 4.3.5
debug: 4.3.7(supports-color@8.1.1)
pathe: 1.1.2
picocolors: 1.0.1
vite: 5.4.11(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0)
@ -22777,7 +22754,7 @@ snapshots:
- supports-color
- terser
vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0):
vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0):
dependencies:
'@vitest/expect': 1.6.0
'@vitest/runner': 1.6.0
@ -22802,7 +22779,7 @@ snapshots:
optionalDependencies:
'@types/node': 22.9.0
happy-dom: 10.0.3
jsdom: 24.1.1
jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- less
- lightningcss
@ -22853,6 +22830,8 @@ snapshots:
vue-component-type-helpers@2.1.10: {}
vue-component-type-helpers@2.2.0: {}
vue-demi@0.14.7(vue@3.5.12(typescript@5.6.3)):
dependencies:
vue: 3.5.12(typescript@5.6.3)