From d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 9 Mar 2025 12:34:08 +0900 Subject: Refine preferences (#15597) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * test * wip rollup pluginでsearchIndexの情報生成 * wip * SPDX * wip: markerIdを自動付与 * rollupでビルド時・devモード時に毎回uuidを生成するように * 開発サーバーでだけ必要な挙動は開発サーバーのみで * 条件が逆 * wip: childrenの生成 * update comment * update comment * rename auto generated file * hashをパスと行数から決定 * Update privacy.vue * Update privacy.vue * wip * Update general.vue * Update general.vue * wip * wip * Update SearchMarker.vue * wip * Update profile.vue * Update mute-block.vue * Update mute-block.vue * Update general.vue * Update general.vue * childrenがduplicate key errorを吐く問題をいったん解決 * マーカーの形を成形 * loggerを置きかえ * とりあえず省略記法に対応 * Refactor and Format codes * wip * Update settings-search-index.ts * wip * wip * とりあえず不確定要因の仮置きidを削除 * hashの生成を正規化(絶対パスになっていたのを緩和) * pathの入力を省略可能に * adminでもパス生成できるように * Update settings-search-index.ts * Update privacy.vue * wip * build searchIndex * wip * build * Update general.vue * build * Update sounds.vue * build * build * Update sounds.vue * 🎨 * 🎨 * Update privacy.vue * Update privacy.vue * Update security.vue * create-search-indexを多少改善 * build * Update 2fa.vue * wip * 必ずtransformCodeCacheを利用するように, キャッシュの明確な受け渡しを定義 * キャッシュはdevServerでなくても更新 * Revert "wip" This reverts commit 41bffd3a13f55618bf939dc1c9acb2a77ead4054. * inlining * wip * Update theme.vue * 🎨 * wip normalize * Update theme.vue * キャッシュのパス変換 * build * wip * wip * Update SearchMarker.vue * i18n.ts['key'] の形式が取り出せない問題のFix * build * 仮でpath入れ * 必ず絶対パスが使われるように * wip * 🎨 * storybookビルド時はcreateSearchIndexをしない * inliningの構造化 * format code * Update index.vue * wip * wip * 🎨 * wip * wip * wip * wip * wip * wip * wip * wip * clean up * wip * wip * wip * Update rollup-plugin-unwind-css-module-class-name.test.ts * Update navbar.vue * clean up * wip * wip * wip * wip * wip * Update preferences-backups.vue * Update common.ts * Update preferences.ts * wip * wip * wip * wip * Update MkPreferenceContainer.vue * Update MkPreferenceContainer.vue * Update MkPreferenceContainer.vue * enhance: 検索で上下矢印を使用することで検索結果を移動できるように * Update main-boot.ts * refactor * wip * Update sounds.vue * fix(frontend): PageWindowでSearchMarkerが動作するように * enhance(frontend): SearchMarkerの点滅を一定時間で止める * wip * lint fix * fix: 子要素監視が抜けていたのを修正 * アニメーションの回数はCSSで制御するように * refactor * enhance(frontend): 検索インデックス作成時のログを削減 * revert * fix * fix * Update preferences.ts * Update preferences.ts * wip * Update preferences.ts * wip * 🎨 * wip * Update MkPreferenceContainer.vue * wip * Update preferences.ts * wip * Update preferences.ts * Update preferences.ts * wip * wip * Update preferences.ts * wip * wip * Update preferences.ts * Update CHANGELOG.md * Update preferences.ts * Update deck-store.ts * deckStoreをdefaultStoreに統合 * wip * defaultStore -> store * Update profile.ts * wip * refactor * wip: plugin * plugin * plugin * plugin * Update plugin.ts * wip * Update plugin.vue * Update preferences.ts * Update main-boot.ts * wip * fix test * Update plugin.vue * Update plugin.vue * Update utility.ts * wip * wip * Update utility.ts * wip * wip * clean up * Update utility.ts --------- Co-authored-by: tai-cha Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com> Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> --- packages/frontend/src/plugin.ts | 223 +++++++++++++++++++++++++++++++++------- 1 file changed, 188 insertions(+), 35 deletions(-) (limited to 'packages/frontend/src/plugin.ts') diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index e319a8c398..609c819053 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -3,39 +3,208 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ref } from 'vue'; +import { ref, defineAsyncComponent } from 'vue'; import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; +import { compareVersions } from 'compare-versions'; +import { v4 as uuid } from 'uuid'; import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; -import { inputText } from '@/os.js'; -import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js'; -import type { Plugin } from '@/store.js'; +import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors, store } from '@/store.js'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; + +export type Plugin = { + installId: string; + name: string; + active: boolean; + config?: Record; + configData: Record; + src: string | null; + version: string; + author?: string; + description?: string; + permissions?: string[]; +}; + +export type AiScriptPluginMeta = { + name: string; + version: string; + author: string; + description?: string; + permissions?: string[]; + config?: Record; +}; const parser = new Parser(); + +export function isSupportedAiScriptVersion(version: string): boolean { + try { + return (compareVersions(version, '0.12.0') >= 0); + } catch (err) { + return false; + } +} + +export async function parsePluginMeta(code: string): Promise { + if (!code) { + throw new Error('code is required'); + } + + const lv = utils.getLangVersion(code); + if (lv == null) { + throw new Error('No language version annotation found'); + } else if (!isSupportedAiScriptVersion(lv)) { + throw new Error(`Aiscript version '${lv}' is not supported`); + } + + let ast; + try { + ast = parser.parse(code); + } catch (err) { + throw new Error('Aiscript syntax error'); + } + + const meta = Interpreter.collectMetadata(ast); + if (meta == null) { + throw new Error('Meta block not found'); + } + + const metadata = meta.get(null); + if (metadata == null) { + throw new Error('Metadata not found'); + } + + const { name, version, author, description, permissions, config } = metadata; + if (name == null || version == null || author == null) { + throw new Error('Required property not found'); + } + + return { + name, + version, + author, + description, + permissions, + config, + }; +} + +export async function authorizePlugin(plugin: Plugin) { + if (plugin.permissions == null || plugin.permissions.length === 0) return; + if (Object.hasOwn(store.state.pluginTokens, plugin.installId)) return; + + const token = await new Promise((res, rej) => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { + title: i18n.ts.tokenRequested, + information: i18n.ts.pluginTokenRequestedDescription, + initialName: plugin.name, + initialPermissions: plugin.permissions, + }, { + done: async result => { + const { name, permissions } = result; + const { token } = await misskeyApi('miauth/gen-token', { + session: null, + name: name, + permission: permissions, + }); + res(token); + }, + closed: () => dispose(), + }); + }); + + store.set('pluginTokens', { + ...store.state.pluginTokens, + [plugin.installId]: token, + }); +} + +export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { + if (!code) return; + + let realMeta: AiScriptPluginMeta; + if (!meta) { + realMeta = await parsePluginMeta(code); + } else { + realMeta = meta; + } + + const installId = uuid(); + + const plugin = { + ...realMeta, + installId, + active: true, + configData: {}, + src: code, + }; + + prefer.set('plugins', prefer.s.plugins.concat(plugin)); + + await authorizePlugin(plugin); +} + +export async function uninstallPlugin(plugin: Plugin) { + prefer.set('plugins', prefer.s.plugins.filter(x => x.installId !== plugin.installId)); + if (Object.hasOwn(store.state.pluginTokens, plugin.installId)) { + await os.apiWithDialog('i/revoke-token', { + token: store.state.pluginTokens[plugin.installId], + }); + const pluginTokens = { ...store.state.pluginTokens }; + delete pluginTokens[plugin.installId]; + store.set('pluginTokens', pluginTokens); + } +} + +export async function configPlugin(plugin: Plugin) { + if (plugin.config == null) { + throw new Error('This plugin does not have a config'); + } + + const config = plugin.config; + for (const key in plugin.configData) { + config[key].default = plugin.configData[key]; + } + + const { canceled, result } = await os.form(plugin.name, config); + if (canceled) return; + + prefer.set('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, configData: result } : x)); +} + +export function changePluginActive(plugin: Plugin, active: boolean) { + prefer.set('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, active } : x)); +} + const pluginContexts = new Map(); export const pluginLogs = ref(new Map()); -export async function install(plugin: Plugin): Promise { +export async function launchPlugin(plugin: Plugin): Promise { // 後方互換性のため if (plugin.src == null) return; + await authorizePlugin(plugin); + const aiscript = new Interpreter(createPluginEnv({ plugin: plugin, - storageKey: 'plugins:' + plugin.id, + storageKey: 'plugins:' + plugin.installId, }), { in: aiScriptReadline, out: (value): void => { console.log(value); - pluginLogs.value.get(plugin.id).push(utils.reprValue(value)); + pluginLogs.value.get(plugin.installId).push(utils.reprValue(value)); }, log: (): void => { }, err: (err): void => { - pluginLogs.value.get(plugin.id).push(`${err}`); + pluginLogs.value.get(plugin.installId).push(`${err}`); throw err; // install時のtry-catchに反応させる }, }); - initPlugin({ plugin, aiscript }); + pluginContexts.set(plugin.installId, aiscript); + pluginLogs.value.set(plugin.installId, []); aiscript.exec(parser.parse(plugin.src)).then( () => { @@ -49,47 +218,36 @@ export async function install(plugin: Plugin): Promise { } function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record { + const id = opts.plugin.installId; + const config = new Map(); for (const [k, v] of Object.entries(opts.plugin.config ?? {})) { config.set(k, utils.jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default)); } return { - ...createAiScriptEnv({ ...opts, token: opts.plugin.token }), - //#region Deprecated - 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { - utils.assertString(title); - registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => { - utils.assertString(title); - registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => { - utils.assertString(title); - registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - //#endregion + ...createAiScriptEnv({ ...opts, token: store.state.pluginTokens[id] }), + 'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => { utils.assertString(title); - registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); + registerPostFormAction({ pluginId: id, title: title.value, handler }); }), 'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => { utils.assertString(title); - registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); + registerUserAction({ pluginId: id, title: title.value, handler }); }), 'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => { utils.assertString(title); - registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); + registerNoteAction({ pluginId: id, title: title.value, handler }); }), 'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => { - registerNoteViewInterruptor({ pluginId: opts.plugin.id, handler }); + registerNoteViewInterruptor({ pluginId: id, handler }); }), 'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => { - registerNotePostInterruptor({ pluginId: opts.plugin.id, handler }); + registerNotePostInterruptor({ pluginId: id, handler }); }), 'Plugin:register_page_view_interruptor': values.FN_NATIVE(([handler]) => { - registerPageViewInterruptor({ pluginId: opts.plugin.id, handler }); + registerPageViewInterruptor({ pluginId: id, handler }); }), 'Plugin:open_url': values.FN_NATIVE(([url]) => { utils.assertString(url); @@ -99,11 +257,6 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record { -- cgit v1.2.3-freya