Skip to content

Commit

Permalink
feat(status): support sandbox!
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Mar 17, 2021
1 parent bbfac8a commit 2e03e5d
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 166 deletions.
1 change: 1 addition & 0 deletions packages/plugin-status/client/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare module '*.vue' {
}

declare module '~/server' {
import {} from 'koishi-plugin-status/server'
export * from 'koishi-plugin-status/server/webui'
}

Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-status/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ export namespace storage {
}

interface Config {
authType?: 0 | 1
authType: 0 | 1
username?: string
password?: string
platform?: string
userId?: string
showPass?: boolean
}

export const user = storage.create<User>('user')
Expand Down
6 changes: 5 additions & 1 deletion packages/plugin-status/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Card from './components/card.vue'
import Button from './components/button.vue'
import Input from './components/input.vue'
import App from './views/layout/index.vue'
import { start, user } from '.'
import { start, user, receive } from '.'

// for el-collapse-transition
import 'element-plus/lib/theme-chalk/base.css'
Expand Down Expand Up @@ -77,6 +77,10 @@ app.use(ElCollapseTransition)

app.use(router)

receive('expire', () => {
router.push('/login')
})

router.beforeEach((route) => {
if (route.meta.authorize && !user.value) {
return '/login'
Expand Down
55 changes: 34 additions & 21 deletions packages/plugin-status/client/views/login.vue
Original file line number Diff line number Diff line change
@@ -1,32 +1,42 @@
<template>
<k-card class="login">
<h1>
<span :class="{ inactive: config.authType === 1 }" @click="config.authType = 0">平台账户登录</span>
/
<span :class="{ inactive: config.authType === 0 }" @click="config.authType = 1">用户名密码登录</span>
</h1>
<template v-if="data.token">
<h1><span>平台账户登录</span></h1>
<p class="hint">欢迎你,{{ data.name || 'Koishi 用户' }}!</p>
<p class="hint">请用上述账号将下面的验证码私聊发送给任意机器人</p>
<p class="hint">请用上述账号将下面的验证码私聊发送给任意机器人</p>
<p class="token">{{ data.token }}</p>
</template>
<template v-else-if="config.authType === 0">
<k-input prefix="at" placeholder="平台名" v-model="config.platform"/>
<k-input prefix="user" placeholder="账号" v-model="config.userId" @enter="enter"/>
<p class="error" v-if="data.message">{{ data.message }}</p>
<div class="control">
<k-button @click="$router.back()">返回</k-button>
<k-button @click="enter">获取验证码</k-button>
<k-button @click="data.token = null">返回上一步</k-button>
</div>
</template>
<template v-else>
<k-input prefix="user" placeholder="用户名" v-model="config.username"/>
<k-input prefix="lock" placeholder="密码" v-model="config.password" type="password" @enter="enter"/>
<p class="error" v-if="data.message">{{ data.message }}</p>
<div class="control">
<k-button @click="$router.back()">返回</k-button>
<k-button @click="login">登录</k-button>
</div>
<h1>
<span :class="{ inactive: config.authType === 1 }" @click="config.authType = 0">平台账户登录</span>
/
<span :class="{ inactive: config.authType === 0 }" @click="config.authType = 1">用户名密码登录</span>
</h1>
<template v-if="config.authType === 0">
<k-input prefix="at" placeholder="平台名" v-model="config.platform"/>
<k-input prefix="user" placeholder="账号" v-model="config.userId" @enter="enter"/>
<p class="error" v-if="data.message">{{ data.message }}</p>
<div class="control">
<k-button @click="$router.back()">返回</k-button>
<k-button @click="enter">获取验证码</k-button>
</div>
</template>
<template v-else>
<k-input prefix="user" placeholder="用户名" v-model="config.username"/>
<k-input prefix="lock" placeholder="密码" v-model="config.password" @enter="login"
:type="config.showPass ? 'text' : 'password'"
:suffix="config.showPass ? 'eye' : 'eye-slash'"
@click-suffix="config.showPass = !config.showPass"
/>
<p class="error" v-if="data.message">{{ data.message }}</p>
<div class="control">
<k-button @click="$router.back()">返回</k-button>
<k-button @click="login">登录</k-button>
</div>
</template>
</template>
</k-card>
</template>
Expand Down Expand Up @@ -74,7 +84,6 @@ async function login() {
<style lang="scss">
section.login {
font-size: 16px;
max-width: 600px;
text-align: center;
display: flex;
Expand Down Expand Up @@ -104,6 +113,10 @@ section.login {
}
}
.token {
font-weight: bold;
}
.k-input {
display: block;
max-width: 400px;
Expand Down
11 changes: 5 additions & 6 deletions packages/plugin-status/client/views/profile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
</k-card>
<k-card title="设置密码">
<k-input v-model="password" @enter="enter"
:type="hidden ? 'password' : 'text'"
:suffix="hidden ? 'eye-slash' : 'eye'"
@click-suffix="hidden = !hidden"
:type="config.showPass ? 'text' : 'password'"
:suffix="config.showPass ? 'eye' : 'eye-slash'"
@click-suffix="config.showPass = !config.showPass"
/>
<p>
<k-button type="danger" solid :disabled="!password" @click="enter">应用更改</k-button>
Expand All @@ -20,13 +20,12 @@
import { user, config, send, sha256 } from '~/client'
import { ref } from 'vue'
const hidden = ref(true)
const password = ref(config.value.password)
async function enter() {
if (!password.value) return
const { id } = user.value
send('password', { id, password: await sha256(password.value) })
const { id, token } = user.value
send('password', { id, token, password: await sha256(password.value) })
config.value.password = password.value
}
Expand Down
49 changes: 44 additions & 5 deletions packages/plugin-status/client/views/sandbox.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<template>
<k-card class="sandbox">
<div class="history">
<p v-for="({ from, content }, index) in messages" :key="index">
<span class="hint">{{ from === 'user' ? '>' : '<' }}</span>
<p v-for="({ from, content }, index) in messages" :key="index" :class="from">
{{ content }}
</p>
</div>
Expand All @@ -13,7 +12,7 @@
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { send, receive } from '~/client'
import { send, receive, user } from '~/client'
interface Message {
from: 'user' | 'bot'
Expand All @@ -26,7 +25,8 @@ const messages = reactive<Message[]>([])
function onEnter() {
if (!text.value) return
messages.push({ from: 'user', content: text.value })
send('sandbox', text.value)
const { token, id } = user.value
send('sandbox', { token, id, content: text.value })
text.value = ''
}
Expand All @@ -40,8 +40,47 @@ receive('sandbox', (data) => {
.sandbox {
height: 100%;
position: relative;
.history {
position: absolute;
top: 2rem;
left: 2rem;
right: 2rem;
bottom: 6rem;
overflow-x: visible;
overflow-y: auto;
}
p {
padding-left: 1rem;
white-space: break-spaces;
color: rgba(244, 244, 245, .8);
}
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
p.user::before {
content: '>';
position: absolute;
left: -.1rem;
}
p.bot::before {
content: '<';
position: absolute;
left: -.1rem;
}
.k-input {
width: 100%;
position: absolute;
bottom: 2rem;
left: 2rem;
right: 2rem;
}
}
Expand Down
122 changes: 79 additions & 43 deletions packages/plugin-status/server/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,88 @@
import { Adapter, Bot, BotOptions, Context, Logger, omit, Random } from 'koishi-core'
import { createHash } from 'crypto'
import { Adapter, Bot, BotOptions, Context, Logger, omit, pick, Random, Session, Time, User } from 'koishi-core'
import Profile from './profile'
import WebSocket from 'ws'

declare module 'koishi-core' {
namespace Bot {
interface Platforms {
'sandbox': WebBot
}
}
}
const logger = new Logger('status')
const states: Record<string, [string, number, WebBot]> = {}

const TOKEN_TIMEOUT = Time.minute * 10

export class WebBot extends Bot<'sandbox'> {
adapter: WebAdapter

class WebBot extends Bot<'sandbox'> {
constructor(adapter: WebAdapter, options: BotOptions) {
super(adapter, options)
Profile.initBot(this)
}

async sendMessage(channelId: string, content: string) {
this._sendSocket('message', content)
this._send('sandbox', content)
return Random.uuid()
}

_sendSocket(type: string, body: any) {
// websocket api

_send(type: string, body?: any) {
this.socket.send(JSON.stringify({ type, body }))
}
}

const logger = new Logger('status')
const states: Record<string, [string, number, WebSocket]> = {}
async $token({ platform, userId }) {
const user = await this.app.database.getUser(platform, userId, ['name'])
if (!user) return this._send('login', { message: '找不到此账户。' })
const id = `${platform}:${userId}`
const token = Random.uuid()
const expire = Date.now() + TOKEN_TIMEOUT
states[id] = [token, expire, this]
setTimeout(() => {
if (states[id]?.[1] > Date.now()) delete states[id]
}, TOKEN_TIMEOUT)
this._send('login', { token, name: user.name })
}

async _validate<T extends User.Field>(id: string, token: string, fields: T[] = []) {
const user = await this.app.database.getUser('id', id, ['token', 'expire', ...fields])
if (token !== user.token || user.expire <= Date.now()) {
this._send('expire')
return
}
return user
}

async $password({ id, token, password }) {
const user = await this._validate(id, token, ['password'])
if (password === user.password) return
await this.app.database.setUser('id', id, { password })
}

async $login({ username, password }) {
const user = await this.app.database.getUser('name', username, ['password', 'authority', 'id', 'expire', 'token'])
if (!user || user.password !== password) {
return this._send('login', { message: '用户名或密码错误。' })
}
user.token = Random.uuid()
user.expire = Date.now() + this.adapter.config.expiration
await this.app.database.setUser('name', username, pick(user, ['token', 'expire']))
this._send('user', omit(user, ['password']))
}

async $sandbox({ id, token, content }) {
const user = await this._validate(id, token, ['name'])
const session = new Session(this.app, {
platform: 'sandbox',
userId: id,
content,
selfId: this.selfId,
type: 'message',
subtype: 'private',
author: {
userId: 'id',
username: user.name,
},
})
session.platform = 'id' as never
this.adapter.dispatch(session)
}
}

export namespace WebAdapter {
export interface Config {
Expand All @@ -51,10 +105,10 @@ export class WebAdapter extends Adapter<'sandbox'> {
if (session.subtype !== 'private') return next()
const state = states[session.uid]
if (state && state[0] === session.content) {
return state[2].send(JSON.stringify({
type: 'user',
body: await session.observeUser(['id', 'name', 'authority']),
}))
const user = await session.observeUser(['id', 'name', 'authority', 'token', 'expire'])
user.token = Random.uuid()
user.expire = Date.now() + config.expiration
return state[2]._send('user', user)
}
return next()
}, true)
Expand All @@ -66,37 +120,19 @@ export class WebAdapter extends Adapter<'sandbox'> {
bot.socket = socket
bot.username = '沙箱机器人'
bot.status = Bot.Status.GOOD

socket.on('close', () => {
bot.dispose()
for (const id in states) {
if (states[id][2] === socket) delete states[id]
if (states[id][2] === bot) delete states[id]
}
})

socket.on('message', async (data) => {
const { type, body } = JSON.parse(data.toString())
if (type === 'token') {
const { platform, userId } = body
const user = await this.app.database.getUser(platform, userId, ['name'])
if (!user) return bot._sendSocket('login', { message: '没有此账户。' })
const id = `${platform}:${userId}`
const token = Random.uuid()
const expire = Date.now() + this.config.expiration
states[id] = [token, expire, socket]
setTimeout(() => {
if (states[id]?.[1] > Date.now()) delete states[id]
}, this.config.expiration)
bot._sendSocket('token', { token, name: user.name })
} else if (type === 'password') {
const { id, password } = body
await this.app.database.setUser('id', id, { password })
} else if (type === 'login') {
const { username, password } = body
const user = await this.app.database.getUser('name', username, ['password', 'authority', 'id'])
if (!user) return bot._sendSocket('login', { message: '没有此账户。' })
if (user.password !== createHash('sha256').update(password).digest('hex')) {
if (!user) return bot._sendSocket('login', { message: '用户名或密码错误。' })
}
bot._sendSocket('user', omit(user, ['password']))
const method = bot['$' + type]
if (method) {
method.call(bot, body)
} else {
logger.info(type, body)
}
Expand Down

0 comments on commit 2e03e5d

Please sign in to comment.