- {{ 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 => {