From c72f607befaf12e918b097bc76aee0a320900af1 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Thu, 1 Apr 2021 03:36:01 +0800 Subject: [PATCH 01/17] feat(core): support functional optionConfig.hidden --- packages/koishi-core/src/help.ts | 9 +++++++-- packages/koishi-core/src/parser.ts | 2 +- packages/koishi-core/src/session.ts | 8 ++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/koishi-core/src/help.ts b/packages/koishi-core/src/help.ts index c7eda0531f..d3f3013efb 100644 --- a/packages/koishi-core/src/help.ts +++ b/packages/koishi-core/src/help.ts @@ -3,6 +3,7 @@ import { TableType } from './database' import { Session, FieldCollector } from './session' import { template } from 'koishi-utils' import { Context } from './context' +import { Domain } from './parser' interface HelpConfig { showHidden?: boolean @@ -111,12 +112,16 @@ function formatCommands(path: string, session: Session, childre return output } +function getOptionVisibility(option: Domain.OptionConfig, session: Session) { + if (session.user && option.authority > session.user.authority) return false + return !session.resolveValue(option.hidden) +} + function getOptions(command: Command, session: Session, maxUsage: number, config: HelpConfig) { if (command.config.hideOptions && !config.showHidden) return [] const options = config.showHidden ? Object.values(command._options) - : Object.values(command._options) - .filter(option => !option.hidden && (!session.user || option.authority <= session.user.authority)) + : Object.values(command._options).filter(option => getOptionVisibility(option, session)) if (!options.length) return [] const output = config.authority && options.some(o => o.authority) diff --git a/packages/koishi-core/src/parser.ts b/packages/koishi-core/src/parser.ts index 1e0926abdc..9fd30ac468 100644 --- a/packages/koishi-core/src/parser.ts +++ b/packages/koishi-core/src/parser.ts @@ -166,7 +166,7 @@ export namespace Domain { fallback?: any type?: T /** hide the option by default */ - hidden?: boolean + hidden?: boolean | ((session: Session) => boolean) authority?: number notUsage?: boolean } diff --git a/packages/koishi-core/src/session.ts b/packages/koishi-core/src/session.ts index 42da40ac82..942a871643 100644 --- a/packages/koishi-core/src/session.ts +++ b/packages/koishi-core/src/session.ts @@ -180,7 +180,7 @@ export class Session< })) } - private _getValue(source: T | ((session: Session) => T)): T { + resolveValue(source: T | ((session: Session) => T)): T { return typeof source === 'function' ? Reflect.apply(source, null, [this]) : source } @@ -219,7 +219,7 @@ export class Session< if (hasActiveCache) return this.channel = cache as any // 绑定一个新的可观测频道实例 - const assignee = this._getValue(this.app.options.autoAssign) ? this.selfId : '' + const assignee = this.resolveValue(this.app.options.autoAssign) ? this.selfId : '' const data = await this.getChannel(channelId, assignee, fieldArray) const newChannel = observe(data, diff => this.database.setChannel(platform, channelId, diff), `channel ${this.cid}`) this.app._channelCache.set(this.cid, newChannel) @@ -266,7 +266,7 @@ export class Session< // 确保匿名消息不会写回数据库 if (this.author?.anonymous) { const fallback = User.create(this.platform, userId) - fallback.authority = this._getValue(this.app.options.autoAuthorize) + fallback.authority = this.resolveValue(this.app.options.autoAuthorize) const user = observe(fallback, () => Promise.resolve()) return this.user = user } @@ -278,7 +278,7 @@ export class Session< if (hasActiveCache) return this.user = cache as any // 绑定一个新的可观测用户实例 - const data = await this.getUser(userId, this._getValue(this.app.options.autoAuthorize), fieldArray) + const data = await this.getUser(userId, this.resolveValue(this.app.options.autoAuthorize), fieldArray) const newUser = observe(data, diff => this.database.setUser(this.platform, userId, diff), `user ${this.uid}`) userCache.set(userId, newUser) return this.user = newUser From a4264e9f302116538fa678aeca9a6f1b6ad13fd6 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Thu, 1 Apr 2021 03:39:30 +0800 Subject: [PATCH 02/17] feat(github): add authority/visibility setting for github, fix #182 --- packages/plugin-github/src/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/plugin-github/src/index.ts b/packages/plugin-github/src/index.ts index f990ca6066..9c894257b9 100644 --- a/packages/plugin-github/src/index.ts +++ b/packages/plugin-github/src/index.ts @@ -3,7 +3,7 @@ import { createHmac } from 'crypto' import { encode } from 'querystring' -import { Context, camelize, Time, Random, sanitize, Logger } from 'koishi-core' +import { Context, camelize, Time, Random, sanitize, Logger, Session } from 'koishi-core' import { CommonPayload, addListeners, defaultEvents, EventConfig } from './events' import { Config, GitHub, ReplyHandler, ReplySession, ReplyPayloads } from './server' import axios from 'axios' @@ -144,11 +144,13 @@ export function apply(ctx: Context, config: Config = {}) { } } + const hidden = (sess: Session) => sess.subtype !== 'group' + ctx.command('github [name]') .channelFields(['githubWebhooks']) - .option('list', '-l 查看当前频道订阅的仓库列表') - .option('add', '-a 为当前频道添加仓库订阅') - .option('delete', '-d 从当前频道移除仓库订阅') + .option('list', '-l 查看当前频道订阅的仓库列表', { hidden }) + .option('add', '-a 为当前频道添加仓库订阅', { hidden, authority: 2 }) + .option('delete', '-d 从当前频道移除仓库订阅', { hidden, authority: 2 }) .action(async ({ session, options }, name) => { if (options.list) { if (!session.channel) return '当前不是群聊上下文。' From d23c170af8ccc2ec5d28df18d22b024800d201a6 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Thu, 1 Apr 2021 13:05:09 +0800 Subject: [PATCH 03/17] feat(webui): support clear in sandbox --- .../plugin-webui/client/views/plugins/plugin-view.vue | 1 + packages/plugin-webui/client/views/sandbox.vue | 8 +++++++- packages/plugin-webui/src/adapter.ts | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/plugin-webui/client/views/plugins/plugin-view.vue b/packages/plugin-webui/client/views/plugins/plugin-view.vue index a911aa19bd..a98d4845c8 100644 --- a/packages/plugin-webui/client/views/plugins/plugin-view.vue +++ b/packages/plugin-webui/client/views/plugins/plugin-view.vue @@ -14,6 +14,7 @@ import type { Registry } from '~/server' import { ref, computed, defineProps } from 'vue' +import PluginView from './plugin-view.vue' const show = ref(false) diff --git a/packages/plugin-webui/client/views/sandbox.vue b/packages/plugin-webui/client/views/sandbox.vue index a1012f236f..d9a9f98de7 100644 --- a/packages/plugin-webui/client/views/sandbox.vue +++ b/packages/plugin-webui/client/views/sandbox.vue @@ -11,7 +11,7 @@ diff --git a/packages/plugin-webui/client/index.ts b/packages/plugin-webui/client/index.ts index 0909264d98..657917fd9b 100644 --- a/packages/plugin-webui/client/index.ts +++ b/packages/plugin-webui/client/index.ts @@ -108,3 +108,101 @@ export async function sha256(password: string) { } return output } + +export interface segment { + type: string + data: segment.Data +} + +export function segment(type: string, data: segment.Data = {}) { + if (type === 'text') return String(data.content) + let output = '[CQ:' + type + for (const key in data) { + if (data[key]) output += `,${key}=${segment.escape(data[key], true)}` + } + return output + ']' +} + +type primitive = string | number | boolean + +export namespace segment { + export type Chain = segment.Parsed[] + export type Data = Record + export type Transformer = string | ((data: Record, index: number, chain: Chain) => string) + + export interface Parsed extends segment { + data: Record + capture?: RegExpExecArray + } + + export function escape(source: any, inline = false) { + const result = String(source) + .replace(/&/g, '&') + .replace(/\[/g, '[') + .replace(/\]/g, ']') + return inline + ? result.replace(/,/g, ',').replace(/(\ud83c[\udf00-\udfff])|(\ud83d[\udc00-\ude4f\ude80-\udeff])|[\u2600-\u2B55]/g, ' ') + : result + } + + export function unescape(source: string) { + return String(source) + .replace(/[/g, '[') + .replace(/]/g, ']') + .replace(/,/g, ',') + .replace(/&/g, '&') + } + + export function join(codes: segment[]) { + return codes.map(code => segment(code.type, code.data)).join('') + } + + export function from(source: string, typeRegExp = '\\w+'): segment.Parsed { + const capture = new RegExp(`\\[CQ:(${typeRegExp})((,\\w+=[^,\\]]*)*)\\]`).exec(source) + if (!capture) return null + const [, type, attrs] = capture + const data: Record = {} + attrs && attrs.slice(1).split(',').forEach((str) => { + const index = str.indexOf('=') + data[str.slice(0, index)] = unescape(str.slice(index + 1)) + }) + return { type, data, capture } + } + + export function parse(source: string) { + const chain: Chain = [] + let result: segment.Parsed + while ((result = from(source))) { + const { capture } = result + if (capture.index) { + chain.push({ type: 'text', data: { content: source.slice(0, capture.index) } }) + } + chain.push(result) + source = source.slice(capture.index + capture[0].length) + } + if (source) chain.push({ type: 'text', data: { content: source } }) + return chain + } + + export function transform(source: string, rules: Record, dropOthers = false) { + return parse(source).map(({ type, data, capture }, index, chain) => { + const transformer = rules[type] + return typeof transformer === 'string' ? transformer + : typeof transformer === 'function' ? transformer(data, index, chain) + : dropOthers ? '' : type === 'text' ? data.content : capture[0] + }).join('') + } + + export type Factory = (value: T, data?: segment.Data) => string + + function createFactory(type: string, key: string): Factory { + return (value, data = {}) => segment(type, { ...data, [key]: value }) + } + + export const at = createFactory('at', 'id') + export const sharp = createFactory('sharp', 'id') + export const quote = createFactory('quote', 'id') + export const image = createFactory('image', 'url') + export const video = createFactory('video', 'url') + export const audio = createFactory('audio', 'url') +} diff --git a/packages/plugin-webui/client/main.ts b/packages/plugin-webui/client/main.ts index 1efa912154..72c5501a9d 100644 --- a/packages/plugin-webui/client/main.ts +++ b/packages/plugin-webui/client/main.ts @@ -5,6 +5,7 @@ import Card from './components/card.vue' import Collapse from './components/collapse.vue' import Button from './components/button.vue' import Input from './components/input.vue' +import Message from './components/message.vue' import Numeric from './components/numeric.vue' import App from './views/layout/index.vue' import { start, user, receive, router } from '~/client' @@ -62,9 +63,9 @@ router.addRoute({ app.component('k-card', Card) app.component('k-button', Button) - app.component('k-collapse', Collapse) app.component('k-input', Input) +app.component('k-message', Message) app.component('k-numeric', Numeric) app.component('k-chart', defineAsyncComponent(() => import('./components/echarts'))) diff --git a/packages/plugin-webui/client/views/layout/index.vue b/packages/plugin-webui/client/views/layout/index.vue index 4a415e9553..42ad297bbc 100644 --- a/packages/plugin-webui/client/views/layout/index.vue +++ b/packages/plugin-webui/client/views/layout/index.vue @@ -21,7 +21,7 @@ import { useRoute } from 'vue-router' const route = useRoute() const frameless = computed(() => route.meta.frameless) const loaded = computed(() => (route.meta.require || []).every((key) => client[key].value)) -const invalid = computed(() => route.meta.authority > client.user.value.authority) +const invalid = computed(() => route.meta.authority > client.user.value?.authority) diff --git a/packages/plugin-webui/client/views/sandbox.vue b/packages/plugin-webui/client/views/sandbox.vue index d9a9f98de7..d4c5754834 100644 --- a/packages/plugin-webui/client/views/sandbox.vue +++ b/packages/plugin-webui/client/views/sandbox.vue @@ -2,17 +2,17 @@

- {{ content }} +

- +
diff --git a/packages/plugin-webui/client/components/chat-panel.vue b/packages/plugin-webui/client/components/chat-panel.vue new file mode 100644 index 0000000000..47542ff5ce --- /dev/null +++ b/packages/plugin-webui/client/components/chat-panel.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/packages/plugin-webui/client/components/input.vue b/packages/plugin-webui/client/components/input.vue index 12a9414e33..c75976ef39 100644 --- a/packages/plugin-webui/client/components/input.vue +++ b/packages/plugin-webui/client/components/input.vue @@ -56,19 +56,19 @@ const inputStyle = computed(() => ({ const emit = defineEmit(['update:modelValue', 'paste', 'focus', 'blur', 'enter', 'clickPrefix', 'clickSuffix']) -function onInput (event) { +function onInput(event) { if (props.validate) { invalid.value = !props.validate(event.target.value) } emit('update:modelValue', event.target.value) } -function onFocus (event) { +function onFocus(event) { focused.value = true emit('focus', event) } -function onBlur (event) { +function onBlur(event) { focused.value = false emit('blur', event) } diff --git a/packages/plugin-webui/client/components/message.vue b/packages/plugin-webui/client/components/message.vue index f2a8050e77..ad3e448254 100644 --- a/packages/plugin-webui/client/components/message.vue +++ b/packages/plugin-webui/client/components/message.vue @@ -1,6 +1,6 @@