Skip to content

Commit

Permalink
feat(chat): support send message
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Apr 4, 2021
1 parent 16f2441 commit 27da2e5
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 47 deletions.
28 changes: 21 additions & 7 deletions packages/plugin-chat/client/chat.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<k-chat-panel class="page-chat" :messages="messages" pinned>
<k-chat-panel class="page-chat" :messages="messages" pinned @click="handleClick" @send="handleSend">
<template #default="{ channelName, username, timestamp, content }">
<div class="header">
<span class="channel">{{ channelName || '私聊' }}</span>
Expand All @@ -8,24 +8,38 @@
</div>
<k-message :content="content"/>
</template>
<template #footer v-if="activeMessage">
发送到频道:{{ activeMessage.channelName }}
</template>
</k-chat-panel>
</template>

<script lang="ts" setup>
import { receive, storage } from '~/client'
interface Message {
username: string
content: string
}
import { receive, storage, send, user } from '~/client'
import { ref } from 'vue'
import type { Message } from '../src'
const pinned = ref(true)
const activeMessage = ref<Message>()
const messages = storage.create<Message[]>('chat', [])
receive('chat', (body) => {
messages.value.push(body)
})
function handleClick(message: Message) {
activeMessage.value = message
}
function handleSend(content: string) {
if (!activeMessage.value) return
pinned.value = false
const { platform, selfId, channelId } = activeMessage.value
const { token, id } = user.value
send('chat', { token, id, content, platform, selfId, channelId })
}
function formatDateTime(timestamp: number) {
const date = new Date(timestamp)
const now = new Date()
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-chat/src/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const cqTypes = {
card: '卡片消息',
}

interface Message {
export interface Message {
avatar?: string
content?: string
abstract?: string
Expand Down
14 changes: 12 additions & 2 deletions packages/plugin-chat/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { Context } from 'koishi-core'
import {} from 'koishi-plugin-webui'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { SocketChannel } from 'koishi-plugin-webui'
import { resolve } from 'path'
import debug, { DebugConfig } from './debug'

export * from './debug'

export interface Config extends DebugConfig {}

export const name = 'chat'

export function apply(ctx: Context, options: Config = {}) {
ctx.plugin(debug, options)

ctx.with(['koishi-plugin-webui'], (ctx) => {
ctx.with(['koishi-plugin-webui'] as const, (ctx, { SocketChannel }) => {
const { devMode } = ctx.webui.config
const filename = devMode ? '../client/index.ts' : '../dist/index.js'
ctx.webui.addEntry(resolve(__dirname, filename))

SocketChannel.prototype['$chat'] = async function (this: SocketChannel, { id, token, content, platform, selfId, channelId }) {
const user = await this.validate(id, token, ['name'])
if (!user) return
content = await this.app.transformAssets(content)
this.app.bots[`${platform}:${selfId}`]?.sendMessage(channelId, content)
}
})
}
30 changes: 16 additions & 14 deletions packages/plugin-webui/client/components/chat-panel.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<template>
<k-card class="k-chat-panel">
<div class="k-chat-body" ref="body">
<div class="k-chat-message" v-for="(message, index) in messages" :key="index">
<div class="k-chat-message" v-for="(message, index) in messages" :key="index" @click="$emit('click', message)">
<slot v-bind="message"/>
</div>
</div>
<k-input v-model="text" @enter="onEnter" @paste="onPaste"></k-input>
<div class="k-chat-footer">
<slot name="footer"/>
<k-input v-model="text" @enter="onEnter" @paste="onPaste"/>
</div>
</k-card>
</template>

Expand All @@ -14,7 +17,7 @@
import { ref, watch, defineProps, onMounted, nextTick, defineEmit } from 'vue'
import { segment } from '~/client'
const emit = defineEmit(['enter'])
const emit = defineEmit(['send', 'click'])
const props = defineProps<{ messages: any[], pinned?: boolean }>()
const text = ref('')
Expand All @@ -35,7 +38,7 @@ watch(props.messages, () => {
function onEnter() {
if (!text.value) return
emit('enter', text.value)
emit('send', text.value)
text.value = ''
}
Expand All @@ -46,7 +49,7 @@ async function onPaste(event: ClipboardEvent) {
const file = item.getAsFile()
const reader = new FileReader()
reader.addEventListener('load', function () {
emit('enter', segment.image('base64://' + reader.result.slice(22)))
emit('send', segment.image('base64://' + reader.result.slice(22)))
}, false)
reader.readAsDataURL(file)
}
Expand All @@ -62,12 +65,13 @@ $padding: 1.5rem;
height: 100%;
position: relative;
.k-card-body {
display: flex;
flex-direction: column;
height: -webkit-fill-available;
}
.k-chat-body {
position: absolute;
top: $padding;
left: $padding;
right: $padding;
bottom: 2rem + $padding * 2;
overflow-x: visible;
overflow-y: auto;
}
Expand All @@ -83,10 +87,8 @@ $padding: 1.5rem;
}
.k-input {
position: absolute;
bottom: $padding;
left: $padding;
right: $padding;
padding-top: 1rem;
width: -webkit-fill-available;
}
}
Expand Down
10 changes: 5 additions & 5 deletions packages/plugin-webui/client/components/message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<div class="k-message">
<template v-for="({ type, data }, index) in segment.parse(content)" :key="index">
<span v-if="type === 'text'">{{ data.content }}</span>
<img v-else-if="type === 'image'" :src="normalizeUrl(data)"/>
<span v-else-if="type === 'quote'">[引用回复]</span>
<img v-else-if="type === 'image'" :src="normalizeUrl(data.url)"/>
</template>
</div>
</template>
Expand All @@ -16,10 +17,9 @@ defineProps<{
content: string
}>()
function normalizeUrl(data: any) {
if (!data.url) return console.log(data), ''
if (data.url.startsWith('base64://')) return data.url
return KOISHI_CONFIG.endpoint + '/assets/' + encodeURIComponent(data.url)
function normalizeUrl(url: string) {
if (!KOISHI_CONFIG.whitelist.some(prefix => url.startsWith(prefix))) return url
return KOISHI_CONFIG.endpoint + '/assets/' + encodeURIComponent(url)
}
</script>
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-webui/client/views/sandbox.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<k-chat-panel class="sandbox" :messages="messages" @enter="sendSandbox" :pinned="pinned">
<k-chat-panel class="sandbox" :messages="messages" @send="sendSandbox" :pinned="pinned">
<template #default="{ from, content }">
<k-message :class="from" :content="content"/>
</template>
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-webui/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const states: Record<string, [string, number, SocketChannel]> = {}

const TOKEN_TIMEOUT = Time.minute * 10

class SocketChannel {
export class SocketChannel {
readonly app: App
readonly id = Random.uuid()

Expand Down
7 changes: 7 additions & 0 deletions packages/plugin-webui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,23 @@ const defaultConfig: Config = {
apiPath: '/status',
uiPath: '/console',
selfUrl: '',
whitelist: [],
title: 'Koishi 控制台',
expiration: Time.week,
tickInterval: Time.second * 5,
refreshInterval: Time.hour,
}

const builtinWhitelist = [
'http://gchat.qpic.cn/',
'http://c2cpicdw.qpic.cn',
]

export const name = 'status'

export function apply(ctx: Context, config: Config = {}) {
config = Object.assign(defaultConfig, config)
config.whitelist.push(...builtinWhitelist)

ctx.webui = new WebServer(ctx, config)

Expand Down
29 changes: 13 additions & 16 deletions packages/plugin-webui/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ import type PluginVue from '@vitejs/plugin-vue'

Context.delegate('webui')

export interface Config extends WebAdapter.Config, Profile.Config, Meta.Config, Registry.Config, Statistics.Config {
interface BaseConfig {
title?: string
selfUrl?: string
uiPath?: string
devMode?: boolean
uiPath?: string
whitelist?: string[]
}

export interface Config extends BaseConfig, WebAdapter.Config, Profile.Config, Meta.Config, Registry.Config, Statistics.Config {
title?: string
selfUrl?: string
}

export interface ClientConfig {
title: string
uiPath: string
export interface ClientConfig extends Required<BaseConfig> {
endpoint: string
devMode: boolean
extensions: string[]
}

Expand All @@ -43,11 +45,6 @@ export class WebServer {
private vite: Vite.ViteDevServer
private readonly [Context.current]: Context

static whitelist = [
'http://gchat.qpic.cn/',
'http://c2cpicdw.qpic.cn',
]

constructor(private ctx: Context, public config: Config) {
this.root = resolve(__dirname, '..', config.devMode ? 'client' : 'dist')
this.sources = {
Expand Down Expand Up @@ -75,7 +72,7 @@ export class WebServer {
}

private async start() {
const { uiPath, apiPath } = this.config
const { uiPath, apiPath, whitelist } = this.config
await Promise.all([this.createVite(), this.createAdapter()])

this.ctx.router.get(uiPath + '(/.+)*', async (ctx) => {
Expand Down Expand Up @@ -106,7 +103,7 @@ export class WebServer {
})

this.ctx.router.get(apiPath + '/assets/:url', async (ctx) => {
if (!WebServer.whitelist.some(prefix => ctx.params.url.startsWith(prefix))) {
if (!whitelist.some(prefix => ctx.params.url.startsWith(prefix))) {
console.log(ctx.params.url)
return ctx.status = 403
}
Expand All @@ -117,12 +114,12 @@ export class WebServer {

private async transformHtml(template: string) {
if (this.vite) template = await this.vite.transformIndexHtml(this.config.uiPath, template)
const { apiPath, uiPath, devMode, selfUrl, title } = this.config
const { apiPath, uiPath, devMode, selfUrl, title, whitelist } = this.config
const endpoint = selfUrl + apiPath
const extensions = Object.entries(this.entries).map(([name, filename]) => {
return this.config.devMode ? '/vite/@fs' + filename : `./${name}`
})
const global: ClientConfig = { title, uiPath, endpoint, devMode, extensions }
const global: ClientConfig = { title, uiPath, endpoint, devMode, extensions, whitelist }
const headInjection = `<script>KOISHI_CONFIG = ${JSON.stringify(global)}</script>`
return template.replace('</title>', '</title>' + headInjection)
}
Expand Down

0 comments on commit 27da2e5

Please sign in to comment.