From 8eb5f4957c12eada279b24ee31b70ecff8b31866 Mon Sep 17 00:00:00 2001 From: Fairy-Phy Date: Sat, 11 Nov 2023 02:42:33 +0900 Subject: [PATCH] add sync plugin and storage sync --- packages/frontend/src/boot/main-boot.ts | 15 +++- .../components/MkPluginSelectSaveWindow.vue | 76 +++++++++++++++++++ packages/frontend/src/pages/flash/flash.vue | 5 +- packages/frontend/src/pages/scratchpad.vue | 4 +- .../frontend/src/pages/settings/plugin.vue | 40 ++++++++-- packages/frontend/src/plugin.ts | 19 ++++- packages/frontend/src/scripts/aiscript/api.ts | 23 ++++-- .../frontend/src/scripts/aiscript/storage.ts | 35 +++++++++ .../frontend/src/scripts/install-plugin.ts | 73 ++++++++++++++++-- packages/frontend/src/store.ts | 1 + .../frontend/src/widgets/WidgetAiscript.vue | 4 +- .../src/widgets/WidgetAiscriptApp.vue | 4 +- .../frontend/src/widgets/WidgetButton.vue | 4 +- 13 files changed, 273 insertions(+), 30 deletions(-) create mode 100644 packages/frontend/src/components/MkPluginSelectSaveWindow.vue create mode 100644 packages/frontend/src/scripts/aiscript/storage.ts diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 887740f4a5..7bccd00a24 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -19,6 +19,8 @@ import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js import { mainRouter } from '@/router.js'; import { initializeSw } from '@/scripts/initialize-sw.js'; import { deckStore } from '@/ui/deck/deck-store.js'; +import { getPluginList } from '@/plugin.js'; +import { unisonReload } from '@/scripts/unison-reload.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -57,7 +59,11 @@ export async function mainBoot() { } }); - for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { + let plugins = ColdDeviceStorage.get('plugins').filter(p => p.active); + const accountPlugins = Object.values(await getPluginList()).filter(p => p.active); + plugins.push(...accountPlugins); + + for (const plugin of plugins) { import('@/plugin.js').then(async ({ install }) => { // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 await new Promise(r => setTimeout(r, 0)); @@ -274,6 +280,13 @@ export async function mainBoot() { main.on('myTokenRegenerated', () => { signout(); }); + + // プラグインに変更が入ったら自動でリロードする + stream.useChannel('main').on('registryUpdated', ({ scope, key }: { scope: string[], key: string, value: any }) => { + if (scope[0] === 'client' && key === 'plugins') { + unisonReload(); + } + }); } // shortcut diff --git a/packages/frontend/src/components/MkPluginSelectSaveWindow.vue b/packages/frontend/src/components/MkPluginSelectSaveWindow.vue new file mode 100644 index 0000000000..5910798182 --- /dev/null +++ b/packages/frontend/src/components/MkPluginSelectSaveWindow.vue @@ -0,0 +1,76 @@ + + + + + + + diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index ebf117ffbf..fa4050915b 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -145,7 +145,10 @@ async function run() { aiscript = new Interpreter({ ...createAiScriptEnv({ - storageKey: 'flash:' + flash.id, + scriptData: { + type: 'flash', + id: flash.id, + }, }), ...registerAsUiLib(components, (_root) => { root.value = _root.value; diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index f8d3187bd4..21f63ef1ca 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -79,8 +79,10 @@ async function run() { logs.value = []; aiscript = new Interpreter(({ ...createAiScriptEnv({ - storageKey: 'widget', token: $i?.token, + scriptData: { + type: 'widget', + }, }), ...registerAsUiLib(components, (_root) => { root.value = _root.value; diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue index 5ebd74ef7a..f64c554561 100644 --- a/packages/frontend/src/pages/settings/plugin.vue +++ b/packages/frontend/src/pages/settings/plugin.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ plugin.name }}v{{ plugin.version }} + {{ plugin.name }}{{ '同期' }}v{{ plugin.version }} {{ i18n.ts.makeActive }}
@@ -74,11 +74,21 @@ import { ColdDeviceStorage } from '@/store.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { getPluginList } from '@/plugin.js'; -const plugins = ref(ColdDeviceStorage.get('plugins')); +let plugins = Object.values(await getPluginList()); +plugins.push(...ColdDeviceStorage.get('plugins')); +plugins = ref(plugins); async function uninstall(plugin) { - ColdDeviceStorage.set('plugins', plugins.value.filter(x => x.id !== plugin.id)); + if (plugin.fromAccount) { + let plugins = await getPluginList(); + delete plugins[plugin.id]; + await os.api('i/registry/remove-all-keys-in-scope', { scope: ['client', 'aiscript', 'plugins', plugin.id] }); + await os.api('i/registry/set', { scope: ['client'], key: 'plugins', value: plugins }); + } else { + ColdDeviceStorage.set('plugins', plugins.value.filter(x => x.id !== plugin.id)); + } await os.apiWithDialog('i/revoke-token', { token: plugin.token, }); @@ -102,9 +112,15 @@ async function config(plugin) { const { canceled, result } = await os.form(plugin.name, config); if (canceled) return; - const coldPlugins = ColdDeviceStorage.get('plugins'); - coldPlugins.find(p => p.id === plugin.id)!.configData = result; - ColdDeviceStorage.set('plugins', coldPlugins); + if (plugin.fromAccount) { + let plugins = await getPluginList(); + plugins[plugin.id].configData = result; + await os.api('i/registry/set', { scope: ['client'], key: 'plugins', value: plugins }); + } else { + const coldPlugins = ColdDeviceStorage.get('plugins'); + coldPlugins.find(p => p.id === plugin.id)!.configData = result; + ColdDeviceStorage.set('plugins', coldPlugins); + } nextTick(() => { location.reload(); @@ -130,3 +146,15 @@ definePageMetadata({ icon: 'ti ti-plug', }); + + diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index e24f646a35..d780061b37 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -5,19 +5,30 @@ import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; import { createAiScriptEnv } from '@/scripts/aiscript/api.js'; -import { inputText } from '@/os.js'; +import { inputText, api } from '@/os.js'; import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js'; +import { $i } from './account.js'; const parser = new Parser(); const pluginContexts = new Map(); +export async function getPluginList(): Promise> { + if ($i == null) return {}; + + try { + return await api('i/registry/get', { scope: ['client'], key: 'plugins' }); + } catch (err) { + if (err.code === 'NO_SUCH_KEY') return {}; + throw err; + } +} + export async function install(plugin: Plugin): Promise { // 後方互換性のため if (plugin.src == null) return; const aiscript = new Interpreter(createPluginEnv({ plugin: plugin, - storageKey: 'plugins:' + plugin.id, }), { in: (q): Promise => { return new Promise(ok => { @@ -51,14 +62,14 @@ export async function install(plugin: Plugin): Promise { console.info('Plugin installed:', plugin.name, 'v' + plugin.version); } -function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record { +function createPluginEnv(opts: { plugin: Plugin; }): Record { 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 }), + ...createAiScriptEnv({ token: opts.plugin.token, scriptData: { type: 'plugins', id: opts.plugin.id, fromAccount: opts.plugin.fromAccount } }), //#region Deprecated 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { utils.assertString(title); diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index fb7ab924b7..4bd9c197cb 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -6,12 +6,12 @@ import { utils, values } from '@syuilo/aiscript'; import * as os from '@/os.js'; import { $i } from '@/account.js'; -import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; import { url, lang } from '@/config.js'; import { nyaize } from '@/scripts/nyaize.js'; +import { ScriptData, loadScriptStorage, saveScriptStorage } from './storage.js'; -export function createAiScriptEnv(opts) { +export function createAiScriptEnv(opts: { token: string; scriptData: ScriptData; }) { return { USER_ID: $i ? values.STR($i.id) : values.NULL, USER_NAME: $i ? values.STR($i.name) : values.NULL, @@ -60,14 +60,23 @@ export function createAiScriptEnv(opts) { return values.ERROR('request_failed', utils.jsToVal(err)); }); }), - 'Mk:save': values.FN_NATIVE(([key, value]) => { + 'Mk:save': values.FN_NATIVE(async ([key, value, toAccount]) => { utils.assertString(key); - miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value))); - return values.NULL; + const saveToAccount = toAccount ? toAccount.value : false; + return saveScriptStorage(saveToAccount, opts.scriptData, key.value, utils.valToJs(value)).then(() => { + return values.NULL; + }, err => { + return values.ERROR('request_failed', utils.jsToVal(err)); + }); }), - 'Mk:load': values.FN_NATIVE(([key]) => { + 'Mk:load': values.FN_NATIVE(async ([key, toAccount]) => { utils.assertString(key); - return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`))); + const loadToAccount = toAccount ? toAccount.value : false; + return loadScriptStorage(loadToAccount, opts.scriptData, key.value).then(res => { + return utils.jsToVal(res); + }, err => { + return values.ERROR('request_failed', utils.jsToVal(err)); + }); }), 'Mk:url': values.FN_NATIVE(() => { return values.STR(window.location.href); diff --git a/packages/frontend/src/scripts/aiscript/storage.ts b/packages/frontend/src/scripts/aiscript/storage.ts new file mode 100644 index 0000000000..cc98bf9b32 --- /dev/null +++ b/packages/frontend/src/scripts/aiscript/storage.ts @@ -0,0 +1,35 @@ +import { api } from '@/os.js'; +import { miLocalStorage } from '@/local-storage.js'; +import { $i } from '@/account.js'; + +type ScriptType = 'widget' | 'plugins' | 'flash'; +export type ScriptData = { type: ScriptType; id?: string; fromAccount?: boolean }; + +export async function loadScriptStorage(toAccount: boolean, scriptData: ScriptData, key: string) { + let value: string | null; + if ($i && toAccount && (scriptData.type !== 'plugins' || (scriptData.type === 'plugins' && scriptData.fromAccount))) { + if (scriptData.type === 'widget') { + value = await api('i/registry/get', { scope: ['client', 'aiscript', scriptData.type], key: key }); + } else { + value = await api('i/registry/get', { scope: ['client', 'aiscript', scriptData.type, scriptData.id!], key: key }); + } + } else { + value = miLocalStorage.getItem(`aiscript:${scriptData.type}:${key}`); + } + + if (value === null) return null; + return JSON.parse(value); +} + +export async function saveScriptStorage(toAccount: boolean, scriptData: ScriptData, key: string, value: any) { + const jsonValue = JSON.stringify(value); + if ($i && toAccount && (scriptData.type !== 'plugins' || (scriptData.type === 'plugins' && scriptData.fromAccount))) { + if (scriptData.type === 'widget') { + await api('i/registry/set', { scope: ['client', 'aiscript', scriptData.type], key: key, value: jsonValue }); + } else { + await api('i/registry/set', { scope: ['client', 'aiscript', scriptData.type, scriptData.id!], key: key, value: jsonValue }); + } + } else { + miLocalStorage.setItem(`aiscript:${scriptData.type}:${key}`, jsonValue); + } +} diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts index 1310a0dc73..f9eb66ac71 100644 --- a/packages/frontend/src/scripts/install-plugin.ts +++ b/packages/frontend/src/scripts/install-plugin.ts @@ -6,11 +6,13 @@ import { defineAsyncComponent } from 'vue'; import { compareVersions } from 'compare-versions'; import { v4 as uuid } from 'uuid'; +import xxhash from 'xxhash-wasm'; import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import type { Plugin } from '@/store.js'; import { ColdDeviceStorage } from '@/store.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; +import { getPluginList } from '@/plugin.js'; export type AiScriptPluginMeta = { name: string; @@ -23,6 +25,11 @@ export type AiScriptPluginMeta = { const parser = new Parser(); +async function toHash(name: string, author: string) { + const { h32ToString } = await xxhash(); + return h32ToString(author + name); +} + export function savePlugin({ id, meta, src, token }: { id: string; meta: AiScriptPluginMeta; @@ -36,9 +43,37 @@ export function savePlugin({ id, meta, src, token }: { configData: {}, token: token, src: src, + fromAccount: false, } as Plugin)); } +async function savePluginToAccount(pluginOnlyOverride: boolean, { hash, meta, src, token }: { + hash: string; + meta: AiScriptPluginMeta; + src: string; + token: string; +}) { + let plugins = await getPluginList(); + // pluginOnlyOverrideがtrueになっているということはすでに重複していることが確定している + const configData = pluginOnlyOverride ? plugins[hash].configData : {}; + const pluginToken = pluginOnlyOverride ? plugins[hash].token : token; + plugins[hash] = { + ...meta, + id: hash, + active: true, + configData, + token: pluginToken, + src: src, + fromAccount: true, + } as Plugin; + + if (!pluginOnlyOverride) { + await os.api('i/registry/remove-all-keys-in-scope', { scope: ['client', 'aiscript', 'plugins', hash] }); + } + + await os.api('i/registry/set', { scope: ['client'], key: 'plugins', value: plugins }); +} + export function isSupportedAiScriptVersion(version: string): boolean { try { return (compareVersions(version, '0.12.0') >= 0); @@ -101,7 +136,20 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { realMeta = meta; } - const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => { + const plugins = Object.keys(await getPluginList()); + const pluginHash = await toHash(realMeta.name, realMeta.author); + + const { isLocal, pluginOnlyOverride } = (await new Promise((res, rej) => { + os.popup(defineAsyncComponent(() => import('@/components/MkPluginSelectSaveWindow.vue')), { + isExistsFromAccount: plugins.some(v => v === pluginHash) + }, { + done: result => { + res(result); + }, + }, 'closed'); + })); + + const token = realMeta.permissions == null || realMeta.permissions.length === 0 || pluginOnlyOverride ? null : await new Promise((res, rej) => { os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { title: i18n.ts.tokenRequested, information: i18n.ts.pluginTokenRequestedDescription, @@ -120,10 +168,21 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { }, 'closed'); }); - savePlugin({ - id: uuid(), - meta: realMeta, - token, - src: code, - }); + if (isLocal) { + + savePlugin({ + id: uuid(), + meta: realMeta, + token, + src: code, + }); + } + else { + await savePluginToAccount(pluginOnlyOverride, { + hash: pluginHash, + meta: realMeta, + token, + src: code, + }); + } } diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 6d95ddba35..eb6f82cb95 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -426,6 +426,7 @@ export type Plugin = { author?: string; description?: string; permissions?: string[]; + fromAccount: boolean; }; interface Watcher { diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index 1b8c8ad9bc..df3f163c10 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -66,8 +66,10 @@ const logs = ref<{ const run = async () => { logs.value = []; const aiscript = new Interpreter(createAiScriptEnv({ - storageKey: 'widget', token: $i?.token, + scriptData: { + type: 'widget', + }, }), { in: (q) => { return new Promise(ok => { diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue index 53b6020ffc..b99ab5a15c 100644 --- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue +++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue @@ -57,8 +57,10 @@ const components: Ref[] = $ref([]); async function run() { const aiscript = new Interpreter({ ...createAiScriptEnv({ - storageKey: 'widget', token: $i?.token, + scriptData: { + type: 'widget', + }, }), ...registerAsUiLib(components, (_root) => { root.value = _root.value; diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue index a7bdd4c49c..387af44e69 100644 --- a/packages/frontend/src/widgets/WidgetButton.vue +++ b/packages/frontend/src/widgets/WidgetButton.vue @@ -53,8 +53,10 @@ const parser = new Parser(); const run = async () => { const aiscript = new Interpreter(createAiScriptEnv({ - storageKey: 'widget', token: $i?.token, + scriptData: { + type: 'widget', + }, }), { in: (q) => { return new Promise(ok => {