diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-03-09 12:34:08 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-09 12:34:08 +0900 |
| commit | d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e (patch) | |
| tree | c0c87a30037d3ffc11784627e67a1965b262c336 /packages/frontend/src/plugin.ts | |
| parent | [skip ci] Update CHANGELOG.md (prepend template) (diff) | |
| download | sharkey-d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e.tar.gz sharkey-d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e.tar.bz2 sharkey-d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e.zip | |
Refine preferences (#15597)
* 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 <dev@taichan.site>
Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src/plugin.ts')
| -rw-r--r-- | packages/frontend/src/plugin.ts | 223 |
1 files changed, 188 insertions, 35 deletions
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<string, { default: any }>; + configData: Record<string, any>; + 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<string, any>; +}; 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<AiScriptPluginMeta> { + 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<string>((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<string, Interpreter>(); export const pluginLogs = ref(new Map<string, string[]>()); -export async function install(plugin: Plugin): Promise<void> { +export async function launchPlugin(plugin: Plugin): Promise<void> { // 後方互換性のため 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<void> { } function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> { + const id = opts.plugin.installId; + const config = new Map<string, values.Value>(); 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<s }; } -function initPlugin({ plugin, aiscript }): void { - pluginContexts.set(plugin.id, aiscript); - pluginLogs.value.set(plugin.id, []); -} - function registerPostFormAction({ pluginId, title, handler }): void { postFormActions.push({ title, handler: (form, update) => { |