add sync plugin and storage sync
This commit is contained in:
parent
f395324e8b
commit
8eb5f4957c
|
@ -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
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="false"
|
||||
:canClose="false"
|
||||
@close="dialog.close()"
|
||||
@closed="$emit('closed')"
|
||||
@ok="ok()"
|
||||
>
|
||||
<template #header>{{ 'プラグインの共有設定' }}</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m">
|
||||
<div>
|
||||
<MkInfo>{{ 'このプラグインをローカルのみにインストールするか他端末と同期するかを選択できます。' }}</MkInfo>
|
||||
</div>
|
||||
<div v-if="isExistsFromAccount && localOrAccount === 'account'">
|
||||
<MkInfo warn>{{ 'このプラグインは同期されているプラグインと重複しています。' }}</MkInfo>
|
||||
<MkSwitch v-model="pluginOnlyOverride" :class="$style.switch">{{ 'コードのみを上書きする' }}</MkSwitch>
|
||||
</div>
|
||||
<div>
|
||||
<MkSelect v-model="localOrAccount">
|
||||
<template #label>{{ '共有設定' }}</template>
|
||||
<option value="local">{{ 'ローカルのみ' }}</option>
|
||||
<option value="account">{{ '他端末で同期' }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
import MkSelect from './MkSelect.vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
import MkModalWindow from './MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
isExistsFromAccount: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
(ev: 'done', result: { name: string | null, permissions: string[] }): void;
|
||||
}>();
|
||||
|
||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
let localOrAccount = $ref(props.isExistsFromAccount ? 'account' : 'local');
|
||||
let pluginOnlyOverride = $ref(false);
|
||||
|
||||
function ok(): void {
|
||||
emit('done', {
|
||||
isLocal: localOrAccount === 'local',
|
||||
pluginOnlyOverride,
|
||||
});
|
||||
dialog.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.switch {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_s">
|
||||
<div v-for="plugin in plugins" :key="plugin.id" class="_panel _gaps_m" style="padding: 20px;">
|
||||
<div class="_gaps_s">
|
||||
<span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
|
||||
<span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span v-if="plugin.fromAccount" :class="$style.fromAccount">{{ '同期' }}</span><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
|
||||
<MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch>
|
||||
</div>
|
||||
|
||||
|
@ -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',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.fromAccount {
|
||||
margin-left: 0.7em;
|
||||
font-size: 65%;
|
||||
padding: 2px 3px;
|
||||
color: var(--accent);
|
||||
border: solid 1px var(--accent);
|
||||
border-radius: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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<string, Interpreter>();
|
||||
|
||||
export async function getPluginList(): Promise<Record<string, Plugin>> {
|
||||
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<void> {
|
||||
// 後方互換性のため
|
||||
if (plugin.src == null) return;
|
||||
|
||||
const aiscript = new Interpreter(createPluginEnv({
|
||||
plugin: plugin,
|
||||
storageKey: 'plugins:' + plugin.id,
|
||||
}), {
|
||||
in: (q): Promise<string> => {
|
||||
return new Promise(ok => {
|
||||
|
@ -51,14 +62,14 @@ export async function install(plugin: Plugin): Promise<void> {
|
|||
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
|
||||
}
|
||||
|
||||
function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> {
|
||||
function createPluginEnv(opts: { plugin: Plugin; }): Record<string, values.Value> {
|
||||
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 }),
|
||||
...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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -426,6 +426,7 @@ export type Plugin = {
|
|||
author?: string;
|
||||
description?: string;
|
||||
permissions?: string[];
|
||||
fromAccount: boolean;
|
||||
};
|
||||
|
||||
interface Watcher {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -57,8 +57,10 @@ const components: Ref<AsUiComponent>[] = $ref([]);
|
|||
async function run() {
|
||||
const aiscript = new Interpreter({
|
||||
...createAiScriptEnv({
|
||||
storageKey: 'widget',
|
||||
token: $i?.token,
|
||||
scriptData: {
|
||||
type: 'widget',
|
||||
},
|
||||
}),
|
||||
...registerAsUiLib(components, (_root) => {
|
||||
root.value = _root.value;
|
||||
|
|
|
@ -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 => {
|
||||
|
|
Loading…
Reference in New Issue