Merge 65afaacdb1
into 83ea0395f6
This commit is contained in:
commit
e8401a6328
|
@ -15,6 +15,10 @@
|
|||
## 2023.11.1
|
||||
|
||||
### General
|
||||
- Feat: プラグインのスクリプトとデータを端末間で同期をできるようになりました
|
||||
- 既存のプラグインはローカルに保存されています。
|
||||
- ローカルのプラグインはプラグインの管理にあるボタンからアカウントに移動できます。
|
||||
- `Mk:load`と`Mk:save`の引数にオプションオブジェクト`option?: { toAccount?: bool }`が追加され、`true`にするとアカウントでデータを共有できます。(デフォルトは`false`)
|
||||
- Feat: 管理者がコントロールパネルからメールアドレスの照会を行えるようになりました
|
||||
- Enhance: ローカリゼーションの更新
|
||||
- Enhance: 依存関係の更新
|
||||
|
|
|
@ -1161,6 +1161,15 @@ export interface Locale {
|
|||
"signupPendingError": string;
|
||||
"cwNotationRequired": string;
|
||||
"doReaction": string;
|
||||
"pluginSyncSettings": string;
|
||||
"pluginSyncSettingsInfo": string;
|
||||
"duplicateSyncedPlugin": string;
|
||||
"overrideSourceCodeOnly": string;
|
||||
"syncSetting": string;
|
||||
"syncing": string;
|
||||
"movePluginToAccount": string;
|
||||
"movePluginToAccountConfirm": string;
|
||||
"overridePluginConfirm": string;
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
|
|
|
@ -1158,6 +1158,15 @@ useGroupedNotifications: "通知をグルーピングして表示する"
|
|||
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
|
||||
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
|
||||
doReaction: "リアクションする"
|
||||
pluginSyncSettings: "プラグインの同期設定"
|
||||
pluginSyncSettingsInfo: "このプラグインをローカルのみにインストールするか他端末と同期するかを選択できます。"
|
||||
duplicateSyncedPlugin: "このプラグインは同期されているプラグインと重複しています。"
|
||||
overrideSourceCodeOnly: "コードのみを上書きする"
|
||||
syncSetting: "同期設定"
|
||||
syncing: "他端末と同期"
|
||||
movePluginToAccount: "アカウントに移行"
|
||||
movePluginToAccountConfirm: "プラグインをアカウントに移行して他端末と同期しますか?プラグインとプラグイン設定以外は引き継がれません。"
|
||||
overridePluginConfirm: "すでにプラグインがアカウントに存在します。上書きしますか?アカウントにあるプラグインデータは削除されます。"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
|
|
|
@ -131,7 +131,7 @@ export class RegistryApiService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async remove(userId: MiUser['id'], domain: string | null, scope: string[], key: string) {
|
||||
public async remove(userId: MiUser['id'], domain: string | null, scope: string[], key?: string) {
|
||||
const query = this.registryItemsRepository.createQueryBuilder().delete();
|
||||
if (domain) {
|
||||
query.where('domain = :domain', { domain: domain });
|
||||
|
@ -139,7 +139,9 @@ export class RegistryApiService {
|
|||
query.where('domain IS NULL');
|
||||
}
|
||||
query.andWhere('userId = :userId', { userId: userId });
|
||||
query.andWhere('key = :key', { key: key });
|
||||
if (key) {
|
||||
query.andWhere('key = :key', { key: key });
|
||||
}
|
||||
query.andWhere('scope = :scope', { scope: scope });
|
||||
|
||||
await query.execute();
|
||||
|
|
|
@ -231,6 +231,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js';
|
|||
import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js';
|
||||
import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
|
||||
import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
|
||||
import * as ep___i_registry_removeAllKeysInScope from './endpoints/i/registry/remove-all-keys-in-scope.js';
|
||||
import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
|
||||
import * as ep___i_registry_set from './endpoints/i/registry/set.js';
|
||||
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
|
||||
|
@ -590,6 +591,7 @@ const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep__
|
|||
const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default };
|
||||
const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default };
|
||||
const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default };
|
||||
const $i_registry_removeAllKeysInScope: Provider = { provide: 'ep:i/registry/remove-all-keys-in-scope', useClass: ep___i_registry_removeAllKeysInScope.default };
|
||||
const $i_registry_scopesWithDomain: Provider = { provide: 'ep:i/registry/scopes-with-domain', useClass: ep___i_registry_scopesWithDomain.default };
|
||||
const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default };
|
||||
const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default };
|
||||
|
@ -953,6 +955,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$i_registry_keysWithType,
|
||||
$i_registry_keys,
|
||||
$i_registry_remove,
|
||||
$i_registry_removeAllKeysInScope,
|
||||
$i_registry_scopesWithDomain,
|
||||
$i_registry_set,
|
||||
$i_revokeToken,
|
||||
|
@ -1310,6 +1313,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$i_registry_keysWithType,
|
||||
$i_registry_keys,
|
||||
$i_registry_remove,
|
||||
$i_registry_removeAllKeysInScope,
|
||||
$i_registry_scopesWithDomain,
|
||||
$i_registry_set,
|
||||
$i_revokeToken,
|
||||
|
|
|
@ -231,6 +231,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js';
|
|||
import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js';
|
||||
import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
|
||||
import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
|
||||
import * as ep___i_registry_removeAllKeysInScope from './endpoints/i/registry/remove-all-keys-in-scope.js';
|
||||
import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
|
||||
import * as ep___i_registry_set from './endpoints/i/registry/set.js';
|
||||
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
|
||||
|
@ -588,6 +589,7 @@ const eps = [
|
|||
['i/registry/keys-with-type', ep___i_registry_keysWithType],
|
||||
['i/registry/keys', ep___i_registry_keys],
|
||||
['i/registry/remove', ep___i_registry_remove],
|
||||
['i/registry/remove-all-keys-in-scope', ep___i_registry_removeAllKeysInScope],
|
||||
['i/registry/scopes-with-domain', ep___i_registry_scopesWithDomain],
|
||||
['i/registry/set', ep___i_registry_set],
|
||||
['i/revoke-token', ep___i_revokeToken],
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistryItemsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RegistryApiService } from '@/core/RegistryApiService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
errors: {},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
scope: { type: 'array', default: [], items: {
|
||||
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
|
||||
} },
|
||||
domain: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['scope'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private registryApiService: RegistryApiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me, accessToken) => {
|
||||
await this.registryApiService.remove(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -59,8 +59,8 @@
|
|||
"querystring": "0.2.1",
|
||||
"rollup": "4.4.1",
|
||||
"sanitize-html": "2.11.0",
|
||||
"shiki": "^0.14.5",
|
||||
"sass": "1.69.5",
|
||||
"shiki": "^0.14.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.158.0",
|
||||
|
@ -75,7 +75,8 @@
|
|||
"vanilla-tilt": "1.8.1",
|
||||
"vite": "4.5.0",
|
||||
"vue": "3.3.8",
|
||||
"vuedraggable": "next"
|
||||
"vuedraggable": "next",
|
||||
"xxhash-wasm": "1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.5.3",
|
||||
|
|
|
@ -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(
|
||||
|
@ -56,7 +58,11 @@ export async function mainBoot() {
|
|||
}
|
||||
});
|
||||
|
||||
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
|
||||
const 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));
|
||||
|
@ -273,6 +279,13 @@ export async function mainBoot() {
|
|||
main.on('myTokenRegenerated', () => {
|
||||
signout();
|
||||
});
|
||||
|
||||
// プラグインに変更が入ったら自動でリロードする
|
||||
stream.useChannel('main').on('registryUpdated', ({ scope, key }: { scope: string[], key: string, value: any }) => {
|
||||
if (scope.length === 1 && scope[0] === 'client' && key === 'plugins') {
|
||||
unisonReload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// shortcut
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
<!--
|
||||
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>{{ i18n.ts.pluginSyncSettings }}</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m">
|
||||
<div>
|
||||
<MkInfo>{{ i18n.ts.pluginSyncSettingsInfo }}</MkInfo>
|
||||
</div>
|
||||
<div v-if="isExistsFromAccount && localOrAccount === 'account'">
|
||||
<MkInfo warn>{{ i18n.ts.duplicateSyncedPlugin }}</MkInfo>
|
||||
<MkSwitch v-model="pluginOnlyOverride" :class="$style.switch">{{ i18n.ts.overrideSourceCodeOnly }}</MkSwitch>
|
||||
</div>
|
||||
<div>
|
||||
<MkSelect v-model="localOrAccount">
|
||||
<template #label>{{ i18n.ts.syncSetting }}</template>
|
||||
<option value="local">{{ i18n.ts.localOnly }}</option>
|
||||
<option value="account">{{ i18n.ts.syncing }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
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,
|
||||
storageMetadata: {
|
||||
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,
|
||||
storageMetadata: {
|
||||
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>
|
||||
|
||||
|
@ -38,6 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div class="_buttons">
|
||||
<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton>
|
||||
<MkButton v-if="!plugin.fromAccount" inline @click="moveToAccount(plugin)"><i class="ti ti-link"></i> {{ i18n.ts.movePluginToAccount }}</MkButton>
|
||||
<MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
|
||||
</div>
|
||||
|
||||
|
@ -74,11 +75,24 @@ 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';
|
||||
import { toHash } from '@/scripts/xxhash.js';
|
||||
import { savePluginToAccount } from '@/scripts/install-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 {
|
||||
const coldPlugins = ColdDeviceStorage.get('plugins');
|
||||
ColdDeviceStorage.set('plugins', coldPlugins.filter(x => x.id !== plugin.id));
|
||||
}
|
||||
await os.apiWithDialog('i/revoke-token', {
|
||||
token: plugin.token,
|
||||
});
|
||||
|
@ -87,6 +101,37 @@ async function uninstall(plugin) {
|
|||
});
|
||||
}
|
||||
|
||||
async function moveToAccount(plugin) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.movePluginToAccountConfirm,
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
const hash = await toHash(plugin.name, plugin.author);
|
||||
const plugins = await getPluginList();
|
||||
if (Object.keys(plugins).some(v => v === hash)) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.overridePluginConfirm,
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
await os.api('i/registry/remove-all-keys-in-scope', { scope: ['client', 'aiscript', 'plugins', hash] });
|
||||
}
|
||||
|
||||
const coldPlugins = ColdDeviceStorage.get('plugins');
|
||||
ColdDeviceStorage.set('plugins', coldPlugins.filter(x => x.id !== plugin.id));
|
||||
|
||||
plugin.id = hash;
|
||||
plugin.fromAccount = true;
|
||||
plugins[hash] = plugin;
|
||||
|
||||
await os.api('i/registry/set', { scope: ['client'], key: 'plugins', value: plugins });
|
||||
}
|
||||
|
||||
function copy(plugin) {
|
||||
copyToClipboard(plugin.src ?? '');
|
||||
os.success();
|
||||
|
@ -102,9 +147,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 +181,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, storageMetadata: { 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 { StorageMetadata, loadScriptStorage, saveScriptStorage } from './storage.js';
|
||||
|
||||
export function createAiScriptEnv(opts) {
|
||||
export function createAiScriptEnv(opts: { token: string; storageMetadata: StorageMetadata; }) {
|
||||
return {
|
||||
USER_ID: $i ? values.STR($i.id) : values.NULL,
|
||||
USER_NAME: $i ? values.STR($i.name) : values.NULL,
|
||||
|
@ -60,14 +60,35 @@ 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, option]) => {
|
||||
utils.assertString(key);
|
||||
miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value)));
|
||||
return values.NULL;
|
||||
if (option) {
|
||||
utils.assertObject(option);
|
||||
if (option.value.has('toAccount')) {
|
||||
utils.assertBoolean(option.value.get('toAccount'));
|
||||
}
|
||||
}
|
||||
const saveToAccount = option && option.value.has('toAccount') ? option.value.get('toAccount').value : false;
|
||||
return saveScriptStorage(saveToAccount, opts.storageMetadata, 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, option]) => {
|
||||
utils.assertString(key);
|
||||
return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`)));
|
||||
if (option) {
|
||||
utils.assertObject(option);
|
||||
if (option.value.has('toAccount')) {
|
||||
utils.assertBoolean(option.value.get('toAccount'));
|
||||
}
|
||||
}
|
||||
const loadToAccount = option && option.value.has('toAccount') ? option.value.get('toAccount').value : false;
|
||||
return loadScriptStorage(loadToAccount, opts.storageMetadata, 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,43 @@
|
|||
import { api } from '@/os.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
type ScriptType = 'widget' | 'plugins' | 'flash';
|
||||
export type StorageMetadata = { type: ScriptType; id?: string; fromAccount?: boolean };
|
||||
|
||||
export async function loadScriptStorage(toAccount: boolean, storageMetadata: StorageMetadata, key: string) {
|
||||
let value: string | null;
|
||||
if ($i && toAccount && (storageMetadata.type !== 'plugins' || (storageMetadata.type === 'plugins' && storageMetadata.fromAccount))) {
|
||||
if (storageMetadata.type === 'widget') {
|
||||
value = await api('i/registry/get', { scope: ['client', 'aiscript', storageMetadata.type], key: key });
|
||||
} else {
|
||||
value = await api('i/registry/get', { scope: ['client', 'aiscript', storageMetadata.type, storageMetadata.id!], key: key });
|
||||
}
|
||||
} else {
|
||||
if (storageMetadata.type === 'widget') {
|
||||
value = miLocalStorage.getItem(`aiscript:${storageMetadata.type}:${key}`);
|
||||
} else {
|
||||
value = miLocalStorage.getItem(`aiscript:${storageMetadata.type}:${storageMetadata.id!}:${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (value === null) return null;
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
||||
export async function saveScriptStorage(toAccount: boolean, storageMetadata: StorageMetadata, key: string, value: any) {
|
||||
const jsonValue = JSON.stringify(value);
|
||||
if ($i && toAccount && (storageMetadata.type !== 'plugins' || (storageMetadata.type === 'plugins' && storageMetadata.fromAccount))) {
|
||||
if (storageMetadata.type === 'widget') {
|
||||
await api('i/registry/set', { scope: ['client', 'aiscript', storageMetadata.type], key: key, value: jsonValue });
|
||||
} else {
|
||||
await api('i/registry/set', { scope: ['client', 'aiscript', storageMetadata.type, storageMetadata.id!], key: key, value: jsonValue });
|
||||
}
|
||||
} else {
|
||||
if (storageMetadata.type === 'widget') {
|
||||
miLocalStorage.setItem(`aiscript:${storageMetadata.type}:${key}`, jsonValue);
|
||||
} else {
|
||||
miLocalStorage.setItem(`aiscript:${storageMetadata.type}:${storageMetadata.id!}:${key}`, jsonValue);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ 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';
|
||||
import { toHash } from './xxhash.js';
|
||||
|
||||
export type AiScriptPluginMeta = {
|
||||
name: string;
|
||||
|
@ -19,6 +21,7 @@ export type AiScriptPluginMeta = {
|
|||
description?: string;
|
||||
permissions?: string[];
|
||||
config?: Record<string, any>;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const parser = new Parser();
|
||||
|
@ -36,9 +39,37 @@ export function savePlugin({ id, meta, src, token }: {
|
|||
configData: {},
|
||||
token: token,
|
||||
src: src,
|
||||
fromAccount: false,
|
||||
} as Plugin));
|
||||
}
|
||||
|
||||
async function savePluginToAccount(pluginOnlyOverride: boolean, { id, meta, src, token }: {
|
||||
id: string;
|
||||
meta: AiScriptPluginMeta;
|
||||
src: string;
|
||||
token: string;
|
||||
}) {
|
||||
const plugins = await getPluginList();
|
||||
// pluginOnlyOverrideがtrueになっているということはすでに重複していることが確定している
|
||||
const configData = pluginOnlyOverride ? plugins[id].configData : {};
|
||||
const pluginToken = pluginOnlyOverride ? plugins[id].token : token;
|
||||
plugins[id] = {
|
||||
...meta,
|
||||
id,
|
||||
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', id] });
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -76,11 +107,15 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta>
|
|||
throw new Error('Metadata not found');
|
||||
}
|
||||
|
||||
const { name, version, author, description, permissions, config } = metadata;
|
||||
const { name, version, author, description, permissions, config, id } = metadata;
|
||||
if (name == null || version == null || author == null) {
|
||||
throw new Error('Required property not found');
|
||||
}
|
||||
|
||||
if (id != null && !/^[a-zA-Z0-9_]+$/.test(id)) {
|
||||
throw new Error('Invalid id format.');
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
|
@ -88,6 +123,7 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta>
|
|||
description,
|
||||
permissions,
|
||||
config,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -101,7 +137,21 @@ 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) => {
|
||||
const pluginCheckId = realMeta.id ?? pluginHash;
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkPluginSelectSaveWindow.vue')), {
|
||||
isExistsFromAccount: plugins.some(v => v === pluginCheckId)
|
||||
}, {
|
||||
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 +170,19 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
|
|||
}, 'closed');
|
||||
});
|
||||
|
||||
savePlugin({
|
||||
id: uuid(),
|
||||
meta: realMeta,
|
||||
token,
|
||||
src: code,
|
||||
});
|
||||
if (isLocal) {
|
||||
savePlugin({
|
||||
id: realMeta.id ?? uuid(),
|
||||
meta: realMeta,
|
||||
token,
|
||||
src: code,
|
||||
});
|
||||
} else {
|
||||
await savePluginToAccount(pluginOnlyOverride, {
|
||||
id: realMeta.id ?? pluginHash,
|
||||
meta: realMeta,
|
||||
token,
|
||||
src: code,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import xxhash from 'xxhash-wasm';
|
||||
|
||||
export async function toHash(name: string, author: string) {
|
||||
const { h32ToString } = await xxhash();
|
||||
return h32ToString(author + name);
|
||||
}
|
|
@ -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,
|
||||
storageMetadata: {
|
||||
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,
|
||||
storageMetadata: {
|
||||
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,
|
||||
storageMetadata: {
|
||||
type: 'widget',
|
||||
},
|
||||
}), {
|
||||
in: (q) => {
|
||||
return new Promise(ok => {
|
||||
|
|
397
pnpm-lock.yaml
397
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue