Compare commits

...

7 Commits

Author SHA1 Message Date
CrescentLeaf
059078ea8f refactor: 忽略刷新访问令牌重试的请求 方法重写
* 将原有的 CallableMethodBeforeAuth 纳入
2025-10-06 17:31:35 +08:00
CrescentLeaf
674fe000f4 fix: 无限进行刷新访问令牌
* 由于 Client.ts 中的 invoke 没有对请求方法做判断, 导致不该被 retry 的请求被自动重试
2025-10-06 17:27:03 +08:00
CrescentLeaf
85477fe46e feat: 添加刷新令牌支持
* 服务端: 添加对应的接口, 对原有令牌系统稍有修改, 添加了令牌类型
* 客户端: 自动刷新访问令牌, 登录时顺带获取刷新令牌
2025-10-06 17:13:23 +08:00
CrescentLeaf
dced175d7a chore: 统一为简体中文 2025-10-06 15:36:12 +08:00
CrescentLeaf
bd857b840b chore: 修改网页标题 2025-10-06 14:52:48 +08:00
CrescentLeaf
5d1c395340 docs: readme 2025-10-06 14:46:13 +08:00
CrescentLeaf
0e17b37156 chore: add icon for this project 2025-10-06 14:42:38 +08:00
30 changed files with 224 additions and 207 deletions

View File

@@ -51,15 +51,33 @@ class Client {
}) })
} }
return new Promise((resolve) => { return new Promise((resolve) => {
this.socket!.timeout(timeout).emit("The_White_Silk", method, args, (err: Error, res: ApiCallbackMessage) => { this.socket!.timeout(timeout).emit("The_White_Silk", method, args, async (err: Error, res: ApiCallbackMessage) => {
if (err) return resolve({ if (err) return resolve({
code: -1, code: -1,
msg: err.message.indexOf("timed out") != -1 ? "請求超時" : err.message, msg: err.message.indexOf("timed out") != -1 ? "請求超時" : err.message,
}) })
resolve(res) if (!["User.refreshAccessToken", ...CallableMethodBeforeAuth].includes(method) && res.code == 401) {
const token = await this.refreshAccessToken()
if (token) {
data.access_token = token
data.apply()
resolve(await this.invoke(method, {
...args,
token
}, timeout))
} else
resolve(res)
} else
resolve(res)
}) })
}) })
} }
static async refreshAccessToken() {
const re = await this.invoke("User.refreshAccessToken", {
refresh_token: data.refresh_token
})
return re.data?.access_token
}
static async auth(token: string, timeout: number = 5000) { static async auth(token: string, timeout: number = 5000) {
const re = await this.invoke("User.auth", { const re = await this.invoke("User.auth", {
access_token: token access_token: token

BIN
client/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -10,7 +10,7 @@
<link rel="icon" href="icon.ico" /> <link rel="icon" href="icon.ico" />
<link rel="stylesheet" href="./static/material_icons.css" /> <link rel="stylesheet" href="./static/material_icons.css" />
<title>TheWhiteSilk</title> <title>LingChair</title>
<link rel="stylesheet" href="./style.css" /> <link rel="stylesheet" href="./style.css" />
</head> </head>

View File

@@ -90,7 +90,7 @@ export default function App() {
if (re.code == 401) if (re.code == 401)
loginDialogRef.current!.open = true loginDialogRef.current!.open = true
else if (re.code != 200) { else if (re.code != 200) {
if (checkApiSuccessOrSncakbar(re, "驗證失敗")) return if (checkApiSuccessOrSncakbar(re, "验证失败")) return
} else if (re.code == 200) { } else if (re.code == 200) {
setMyUserProfileCache(Client.myUserProfile as User) setMyUserProfileCache(Client.myUserProfile as User)
} }
@@ -201,7 +201,7 @@ export default function App() {
textAlign: 'center', textAlign: 'center',
alignSelf: 'center', alignSelf: 'center',
}}> }}>
... ......
</div> </div>
} }
{ {

View File

@@ -92,7 +92,7 @@ export default function AppMobile() {
if (re.code == 401) if (re.code == 401)
loginDialogRef.current!.open = true loginDialogRef.current!.open = true
else if (re.code != 200) { else if (re.code != 200) {
if (checkApiSuccessOrSncakbar(re, "驗證失敗")) return if (checkApiSuccessOrSncakbar(re, "验证失败")) return
} else if (re.code == 200) { } else if (re.code == 200) {
setMyUserProfileCache(Client.myUserProfile as User) setMyUserProfileCache(Client.myUserProfile as User)
} }
@@ -191,8 +191,8 @@ export default function AppMobile() {
}}> }}>
<mdui-top-app-bar-title>{ <mdui-top-app-bar-title>{
({ ({
Recents: "最近對話", Recents: "最近对话",
Contacts: "所有對話" Contacts: "所有对话"
})[navigationItemSelected] })[navigationItemSelected]
}</mdui-top-app-bar-title> }</mdui-top-app-bar-title>
<div style={{ <div style={{
@@ -234,8 +234,8 @@ export default function AppMobile() {
position: 'sticky', position: 'sticky',
bottom: '0', bottom: '0',
}}> }}>
<mdui-navigation-bar-item icon="watch_later--outlined" active-icon="watch_later--filled" value="Recents"></mdui-navigation-bar-item> <mdui-navigation-bar-item icon="watch_later--outlined" active-icon="watch_later--filled" value="Recents"></mdui-navigation-bar-item>
<mdui-navigation-bar-item icon="chat--outlined" active-icon="chat--filled" value="Contacts"></mdui-navigation-bar-item> <mdui-navigation-bar-item icon="chat--outlined" active-icon="chat--filled" value="Contacts"></mdui-navigation-bar-item>
</mdui-navigation-bar> </mdui-navigation-bar>
</div> </div>
) )

View File

@@ -52,7 +52,7 @@ const markedInstance = new marked.Marked({
export default function ChatFragment({ target, showReturnButton, onReturnButtonClicked, openChatInfoDialog, openUserInfoDialog, ...props }: Args) { export default function ChatFragment({ target, showReturnButton, onReturnButtonClicked, openChatInfoDialog, openUserInfoDialog, ...props }: Args) {
const [messagesList, setMessagesList] = React.useState([] as Message[]) const [messagesList, setMessagesList] = React.useState([] as Message[])
const [chatInfo, setChatInfo] = React.useState({ const [chatInfo, setChatInfo] = React.useState({
title: '加中...' title: '加中...'
} as Chat) } as Chat)
const [tabItemSelected, setTabItemSelected] = React.useState('None') const [tabItemSelected, setTabItemSelected] = React.useState('None')
@@ -68,7 +68,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
target: target, target: target,
}) })
if (re.code != 200) if (re.code != 200)
return target != '' && checkApiSuccessOrSncakbar(re, "對話錯誤") return target != '' && checkApiSuccessOrSncakbar(re, "获取对话信息失败")
setChatInfo(re.data as Chat) setChatInfo(re.data as Chat)
await loadMore() await loadMore()
@@ -90,7 +90,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
page: page.current, page: page.current,
}) })
if (checkApiSuccessOrSncakbar(re, "拉取歷史記錄失敗")) return if (checkApiSuccessOrSncakbar(re, "拉取对话记录失败")) return
const returnMsgs = (re.data!.messages as Message[]).reverse() const returnMsgs = (re.data!.messages as Message[]).reverse()
page.current++ page.current++
if (returnMsgs.length == 0) { if (returnMsgs.length == 0) {
@@ -150,7 +150,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
target, target,
data: cachedFiles.current[fileName], data: cachedFiles.current[fileName],
}, 5000) }, 5000)
if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上傳失敗`)) return setIsMessageSending(false) if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上传失败`)) return setIsMessageSending(false)
text = text.replaceAll('(' + fileName + ')', '(' + re.data!.file_path as string + ')') text = text.replaceAll('(' + fileName + ')', '(' + re.data!.file_path as string + ')')
} }
} }
@@ -160,12 +160,12 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
target, target,
text, text,
}, 5000) }, 5000)
if (checkApiSuccessOrSncakbar(re, "送失")) return setIsMessageSending(false) if (checkApiSuccessOrSncakbar(re, "送失")) return setIsMessageSending(false)
inputRef.current!.value = '' inputRef.current!.value = ''
cachedFiles.current = {} cachedFiles.current = {}
} catch (e) { } catch (e) {
snackbar({ snackbar({
message: '送失: ' + (e as Error).message, message: '送失: ' + (e as Error).message,
placement: 'top', placement: 'top',
}) })
} }
@@ -188,7 +188,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
cachedFiles.current[name] = await data.arrayBuffer() cachedFiles.current[name] = await data.arrayBuffer()
cachedFileNamesCount.current[name] = 1 cachedFileNamesCount.current[name] = 1
if (type.startsWith('image/')) if (type.startsWith('image/'))
insertText(`![片](${name})`) insertText(`![片](${name})`)
else if (type.startsWith('video/')) else if (type.startsWith('video/'))
insertText(`![Video=${name}](${name})`) insertText(`![Video=${name}](${name})`)
else else
@@ -227,7 +227,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
<mdui-tab value="Chat">{ <mdui-tab value="Chat">{
chatInfo.title chatInfo.title
}</mdui-tab> }</mdui-tab>
<mdui-tab value="Settings"></mdui-tab> <mdui-tab value="Settings"></mdui-tab>
<mdui-tab value="None" style={{ display: 'none' }}></mdui-tab> <mdui-tab value="None" style={{ display: 'none' }}></mdui-tab>
<div style={{ <div style={{
flexGrow: '1', flexGrow: '1',
@@ -265,7 +265,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
<span style={{ <span style={{
alignSelf: 'center', alignSelf: 'center',
paddingLeft: '12px', paddingLeft: '12px',
}}>...</span> }}>...</span>
</div> </div>
<div style={{ <div style={{
display: showNoMoreMessagesTip ? undefined : 'none', display: showNoMoreMessagesTip ? undefined : 'none',
@@ -372,7 +372,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
addFile(type as string, getFileNameOrRandom(url), re) addFile(type as string, getFileNameOrRandom(url), re)
} catch (e) { } catch (e) {
snackbar({ snackbar({
message: '法解析連結: ' + (e as Error).message, message: '法解析链接: ' + (e as Error).message,
placement: 'top', placement: 'top',
}) })
} }
@@ -385,7 +385,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
} }
} }
}}> }}>
<mdui-text-field variant="outlined" placeholder="喵呜~" autosize ref={inputRef as any} max-rows={6} onChange={() => { <mdui-text-field variant="outlined" placeholder="(。・ω・。)" autosize ref={inputRef as any} max-rows={6} onChange={() => {
if (inputRef.current?.value.trim() == '') if (inputRef.current?.value.trim() == '')
cachedFiles.current = {} cachedFiles.current = {}
}} onKeyDown={(event) => { }} onKeyDown={(event) => {
@@ -415,7 +415,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
<div style={{ <div style={{
display: 'none' display: 'none'
}}> }}>
<input accept="*/*" type="file" name="選擇附加文" multiple ref={attachFileInputRef}></input> <input accept="*/*" type="file" name="加文" multiple ref={attachFileInputRef}></input>
</div> </div>
</div> </div>
</mdui-tab-panel> </mdui-tab-panel>

View File

@@ -1,4 +1,4 @@
import { Dropdown, Dialog } from "mdui" import { Dropdown, Dialog, dialog } from "mdui"
import { $ } from "mdui/jq" import { $ } from "mdui/jq"
import Client from "../../api/Client.ts" import Client from "../../api/Client.ts"
import Data_Message from "../../api/client_data/Message.ts" import Data_Message from "../../api/client_data/Message.ts"
@@ -131,9 +131,9 @@ export default function Message({ userId, rawData, renderHTML, message, openUser
e.stopPropagation() e.stopPropagation()
setDropDownOpen(false) setDropDownOpen(false)
}}> }}>
<mdui-menu-item icon="content_copy" onClick={() => copyToClipboard($(dropDownRef.current as HTMLElement).find('#msg').text().trim())}></mdui-menu-item> <mdui-menu-item icon="content_copy" onClick={() => copyToClipboard($(dropDownRef.current as HTMLElement).find('#msg').text().trim())}></mdui-menu-item>
<mdui-menu-item icon="content_copy" onClick={() => copyToClipboard(rawData)}></mdui-menu-item> <mdui-menu-item icon="content_copy" onClick={() => copyToClipboard(rawData)}></mdui-menu-item>
<mdui-menu-item icon="info" onClick={() => messageJsonDialogRef.current!.open = true}></mdui-menu-item> <mdui-menu-item icon="info" onClick={() => messageJsonDialogRef.current!.open = true}>JSON</mdui-menu-item>
</mdui-menu> </mdui-menu>
</mdui-dropdown> </mdui-dropdown>
</mdui-card> </mdui-card>

View File

@@ -20,10 +20,10 @@ customElements.define('chat-image', class extends HTMLElement {
e.onerror = () => { e.onerror = () => {
const src = $(this).attr('src') const src = $(this).attr('src')
$(this).html(`<mdui-icon name="broken_image" style="font-size: 2rem;"></mdui-icon>`) $(this).html(`<mdui-icon name="broken_image" style="font-size: 2rem;"></mdui-icon>`)
$(this).attr('alt', '法加載圖像') $(this).attr('alt', '法加载: ' + $(this).attr('alt'))
$(this).on('click', () => { $(this).on('click', () => {
snackbar({ snackbar({
message: `片 (${src}) 法加!`, message: `片 (${src}) 法加!`,
placement: 'top' placement: 'top'
}) })
}) })

View File

@@ -6,7 +6,7 @@ customElements.define('chat-video', class extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this.style.display = 'block' this.style.display = 'block'
const e = new DOMParser().parseFromString(`<video controls>視頻無法播放</video>`, 'text/html').body.firstChild as HTMLVideoElement const e = new DOMParser().parseFromString(`<video controls></video>`, 'text/html').body.firstChild as HTMLVideoElement
e.style.width = "100%" e.style.width = "100%"
e.style.height = "100%" e.style.height = "100%"
e.style.borderRadius = "var(--mdui-shape-corner-medium)" e.style.borderRadius = "var(--mdui-shape-corner-medium)"

View File

@@ -35,8 +35,8 @@ export default function AddContactDialog({
} }
return ( return (
<mdui-dialog close-on-overlay-click close-on-esc headline="添加對話" ref={addContactDialogRef}> <mdui-dialog close-on-overlay-click close-on-esc headline="添加对话" ref={addContactDialogRef}>
<mdui-text-field clearable label="对话 ID / 用 ID / 用名" ref={inputTargetRef as any} onKeyDown={(event) => { <mdui-text-field clearable label="对话 ID / 用 ID / 用名" ref={inputTargetRef as any} onKeyDown={(event) => {
if (event.key == 'Enter') if (event.key == 'Enter')
addContact() addContact()
}}></mdui-text-field> }}></mdui-text-field>

View File

@@ -26,7 +26,7 @@ export default function ChatInfoDialog({ chat, chatInfoDialogRef, openChatFragme
target: chat.id, target: chat.id,
}) })
if (re.code != 200) if (re.code != 200)
return checkApiSuccessOrSncakbar(re, '獲取對話訊息失') return checkApiSuccessOrSncakbar(re, '获取对话信息失')
setChatInfo(re.data!.chat_info as Chat) setChatInfo(re.data!.chat_info as Chat)
}) })

View File

@@ -32,20 +32,21 @@ export default function LoginDialog({
password: CryptoJS.SHA256(password).toString(CryptoJS.enc.Hex), password: CryptoJS.SHA256(password).toString(CryptoJS.enc.Hex),
}) })
if (checkApiSuccessOrSncakbar(re, "登錄失敗")) return if (checkApiSuccessOrSncakbar(re, "登录失败")) return
data.access_token = re.data!.access_token as string data.access_token = re.data!.access_token as string
data.refresh_token = re.data!.refresh_token as string
data.apply() data.apply()
location.reload() location.reload()
}) })
return ( return (
<mdui-dialog headline="登" ref={loginDialogRef}> <mdui-dialog headline="登" ref={loginDialogRef}>
<mdui-text-field label="用 ID / 用名" ref={loginInputAccountRef as any}></mdui-text-field> <mdui-text-field label="用 ID / 用名" ref={loginInputAccountRef as any}></mdui-text-field>
<div style={{ <div style={{
height: "10px", height: "10px",
}}></div> }}></div>
<mdui-text-field label="密" type="password" toggle-password ref={loginInputPasswordRef as any}></mdui-text-field> <mdui-text-field label="密" type="password" toggle-password ref={loginInputPasswordRef as any}></mdui-text-field>
<mdui-button slot="action" variant="text" ref={registerButtonRef}></mdui-button> <mdui-button slot="action" variant="text" ref={registerButtonRef}></mdui-button>
<mdui-button slot="action" variant="text" ref={loginButtonRef}></mdui-button> <mdui-button slot="action" variant="text" ref={loginButtonRef}></mdui-button>

View File

@@ -30,9 +30,9 @@ export default function MyProfileDialog({
avatar: file avatar: file
}) })
if (checkApiSuccessOrSncakbar(re, "修改失")) return if (checkApiSuccessOrSncakbar(re, "修改失")) return
snackbar({ snackbar({
message: "修改成功 (刷新面以更新)", message: "修改成功 (刷新面以更新)",
placement: "top", placement: "top",
}) })
}) })
@@ -74,8 +74,8 @@ export default function MyProfileDialog({
marginBottom: "10px", marginBottom: "10px",
}}></mdui-divider> }}></mdui-divider>
<mdui-list-item icon="logout" rounded onClick={() => dialog({ <mdui-list-item icon="logout" rounded onClick={() => dialog({
headline: "退出登", headline: "退出登",
description: "確定要退出登錄嗎? (若您的賬號未設定 用戶名, 請無務必複製 用戶 ID, 以免丟失賬號!)", description: "请确保在退出登录前, 设定了用户名或者已经记录下了用户 ID, 以免无法登录账号",
actions: [ actions: [
{ {
text: "取消", text: "取消",
@@ -84,7 +84,7 @@ export default function MyProfileDialog({
}, },
}, },
{ {
text: "定", text: "定",
onClick: () => { onClick: () => {
data.access_token = '' data.access_token = ''
data.apply() data.apply()
@@ -93,7 +93,7 @@ export default function MyProfileDialog({
}, },
} }
], ],
})}>退</mdui-list-item> })}>退</mdui-list-item>
</mdui-list> </mdui-list>
</mdui-dialog> </mdui-dialog>
{ {
@@ -103,7 +103,7 @@ export default function MyProfileDialog({
<div style={{ <div style={{
display: "none" display: "none"
}}> }}>
<input type="file" name="選擇頭像" ref={chooseAvatarFileRef} <input type="file" name="选择头像" ref={chooseAvatarFileRef}
accept="image/*" /> accept="image/*" />
</div> </div>
@@ -115,7 +115,7 @@ export default function MyProfileDialog({
width: '50px', width: '50px',
height: '50px', height: '50px',
}} /> }} />
<mdui-text-field variant="outlined" placeholder="昵" ref={editNickNameRef as any} style={{ <mdui-text-field variant="outlined" placeholder="昵" ref={editNickNameRef as any} style={{
marginLeft: "15px", marginLeft: "15px",
}} value={user?.nickname}></mdui-text-field> }} value={user?.nickname}></mdui-text-field>
</div> </div>
@@ -123,12 +123,12 @@ export default function MyProfileDialog({
marginTop: "10px", marginTop: "10px",
}}></mdui-divider> }}></mdui-divider>
<mdui-text-field style={{ marginTop: "10px", }} variant="outlined" label="用 ID" value={user?.id || ''} readonly onClick={(e) => { <mdui-text-field style={{ marginTop: "10px", }} variant="outlined" label="用 ID" value={user?.id || ''} readonly onClick={(e) => {
const input = e.target as HTMLInputElement const input = e.target as HTMLInputElement
input.select() input.select()
input.setSelectionRange(0, 1145141919810) input.setSelectionRange(0, 1145141919810)
}}></mdui-text-field> }}></mdui-text-field>
<mdui-text-field style={{ marginTop: "20px", }} variant="outlined" label="用名" value={user?.username || ''} ref={editUserNameRef as any}></mdui-text-field> <mdui-text-field style={{ marginTop: "20px", }} variant="outlined" label="用名" value={user?.username || ''} ref={editUserNameRef as any}></mdui-text-field>
<mdui-button slot="action" variant="text" onClick={() => userProfileEditDialogRef.current!.open = false}></mdui-button> <mdui-button slot="action" variant="text" onClick={() => userProfileEditDialogRef.current!.open = false}></mdui-button>
<mdui-button slot="action" variant="text" onClick={async () => { <mdui-button slot="action" variant="text" onClick={async () => {
@@ -138,9 +138,9 @@ export default function MyProfileDialog({
username: editUserNameRef.current?.value, username: editUserNameRef.current?.value,
}) })
if (checkApiSuccessOrSncakbar(re, "修改失")) return if (checkApiSuccessOrSncakbar(re, "修改失")) return
snackbar({ snackbar({
message: "修改成功 (刷新面以更新)", message: "修改成功 (刷新面以更新)",
placement: "top", placement: "top",
}) })
userProfileEditDialogRef.current!.open = false userProfileEditDialogRef.current!.open = false

View File

@@ -34,7 +34,7 @@ export default function RegisterDialog({
password: CryptoJS.SHA256(registerInputPasswordRef.current!.value).toString(CryptoJS.enc.Hex), password: CryptoJS.SHA256(registerInputPasswordRef.current!.value).toString(CryptoJS.enc.Hex),
}) })
if (checkApiSuccessOrSncakbar(re, "注冊失敗")) return if (checkApiSuccessOrSncakbar(re, "注册失败")) return
loginInputAccountRef.current!.value = username == "" ? re.data!.userid as string : username loginInputAccountRef.current!.value = username == "" ? re.data!.userid as string : username
loginInputPasswordRef.current!.value = registerInputPasswordRef.current!.value loginInputPasswordRef.current!.value = registerInputPasswordRef.current!.value
@@ -43,25 +43,25 @@ export default function RegisterDialog({
registerInputPasswordRef.current!.value = "" registerInputPasswordRef.current!.value = ""
registerDialogRef.current!.open = false registerDialogRef.current!.open = false
snackbar({ snackbar({
message: "注成功!", message: "注成功!",
placement: "top", placement: "top",
}) })
}) })
return ( return (
<mdui-dialog headline="注" ref={registerDialogRef}> <mdui-dialog headline="注" ref={registerDialogRef}>
<mdui-text-field label="用名 (可)" ref={registerInputUserNameRef as any}></mdui-text-field> <mdui-text-field label="用名 (可)" ref={registerInputUserNameRef as any}></mdui-text-field>
<div style={{ <div style={{
height: "10px", height: "10px",
}}></div> }}></div>
<mdui-text-field label="昵" ref={registerInputNickNameRef as any}></mdui-text-field> <mdui-text-field label="昵" ref={registerInputNickNameRef as any}></mdui-text-field>
<div style={{ <div style={{
height: "10px", height: "10px",
}}></div> }}></div>
<mdui-text-field label="密码" type="password" toggle-password ref={registerInputPasswordRef as any}></mdui-text-field> <mdui-text-field label="密码" type="password" toggle-password ref={registerInputPasswordRef as any}></mdui-text-field>
<mdui-button slot="action" variant="text" ref={registerBackButtonRef}></mdui-button> <mdui-button slot="action" variant="text" ref={registerBackButtonRef}></mdui-button>
<mdui-button slot="action" variant="text" ref={doRegisterButtonRef}></mdui-button> <mdui-button slot="action" variant="text" ref={doRegisterButtonRef}></mdui-button>
</mdui-dialog> </mdui-dialog>
) )
} }

View File

@@ -38,7 +38,7 @@ export default function ContactsList({
token: data.access_token, token: data.access_token,
}) })
if (re.code != 200) if (re.code != 200)
return checkApiSuccessOrSncakbar(re, "获取對話列表失败") return checkApiSuccessOrSncakbar(re, "获取所有对话列表失败")
setContactsList(re.data!.contacts_list as Chat[]) setContactsList(re.data!.contacts_list as Chat[])
} }
@@ -61,7 +61,7 @@ export default function ContactsList({
<mdui-list-item rounded style={{ <mdui-list-item rounded style={{
width: '100%', width: '100%',
marginTop: '13px', marginTop: '13px',
}} icon="person_add" onClick={() => addContactDialogRef.current!.open = true}></mdui-list-item> }} icon="person_add" onClick={() => addContactDialogRef.current!.open = true}></mdui-list-item>
<mdui-list-item rounded style={{ <mdui-list-item rounded style={{
width: '100%', width: '100%',
}} icon="group_add" onClick={() => createGroupDialogRef.current!.open = true}></mdui-list-item> }} icon="group_add" onClick={() => createGroupDialogRef.current!.open = true}></mdui-list-item>

View File

@@ -36,7 +36,7 @@ export default function RecentsList({
token: data.access_token, token: data.access_token,
}) })
if (re.code != 200) if (re.code != 200)
return checkApiSuccessOrSncakbar(re, "获取最近對話列表失败") return checkApiSuccessOrSncakbar(re, "获取最近对话列表失败")
setRecentsList(re.data!.recent_chats as RecentChat[]) setRecentsList(re.data!.recent_chats as RecentChat[])
} }

View File

@@ -1,8 +1,8 @@
## TheWhiteSilk ## 铃之椅
這一個即時通訊項目————簡單, 量, 粹, 而天真 一个即时通讯项目——简单, 量, 粹, 而天真
後續會考慮改名為月之鴿 _仍在积极开发中. 目前是第四代, 版本代号为: the_white_silk_
### 目前實現了什麽? ### 目前實現了什麽?
@@ -13,16 +13,17 @@
- [x] 收發消息 - [x] 收發消息
- [x] 富文本 (based on Marked) - [x] 富文本 (based on Marked)
- [x] 圖片 - [x] 圖片
- [ ] 視頻 - [x] 視頻
- [ ] 文件 - [x] 文件
- [ ] 撤回消息 - [ ] 撤回消息
- [ ] 修改消息 - [ ] 修改消息
- 對話 - 對話
- [ ] _**最近對話**_ - [x] 最近對話
- [x] 添加對話 - [x] 添加對話
- [x] 添加用戶 - [x] 添加用戶
- [ ] 添加群組 (伺服器端群組都還沒做, 想什麽呢) - [x] 添加群組
- [ ] 群组管理
- 賬號 - 賬號
- [x] 登錄注冊 (廢話) - [x] 登錄注冊 (廢話)
@@ -41,7 +42,7 @@
- 基本對話類型 - 基本對話類型
- [x] 雙用戶私聊 - [x] 雙用戶私聊
- [ ] 群組 - [x] 群組
- 消息 - 消息
- [x] 收發消息 - [x] 收發消息
@@ -49,7 +50,7 @@
- [ ] 修改消息 - [ ] 修改消息
- 對話 - 對話
- [ ] _**最近對話**_ - [x] 最近對話
- [x] 添加對話 - [x] 添加對話
- 賬號 - 賬號
@@ -64,8 +65,8 @@
### 伺服器端運行 ### 伺服器端運行
```bash ```bash
git clone https://codeberg.org/CrescentLeaf/TheWhiteSilk git clone https://codeberg.org/LingChair/LingChair
cd TheWhiteSilk cd LingChair
# 編譯前端網頁 # 編譯前端網頁
deno task build deno task build
# 運行服務 # 運行服務

View File

@@ -75,17 +75,17 @@ export default class ApiManager {
socket.on('disconnect', (_reason) => { socket.on('disconnect', (_reason) => {
if (clientInfo.userId == '') if (clientInfo.userId == '')
console.log(chalk.yellow('[]') + ` ${ip} disconnected`) console.log(chalk.yellow('[]') + ` ${ip} disconnected`)
else { else {
console.log(chalk.green('[]') + ` ${ip} disconnected`) console.log(chalk.green('[]') + ` ${ip} disconnected`)
delete this.clients[clientInfo.userId][deviceId + '_' + sessionId] delete this.clients[clientInfo.userId][deviceId + '_' + sessionId]
} }
}) })
console.log(chalk.yellow('[]') + ` ${ip} connected`) console.log(chalk.yellow('[]') + ` ${ip} connected`)
socket.on("The_White_Silk", async (name: string, args: { [key: string]: unknown }, callback_: (ret: ApiCallbackMessage) => void) => { socket.on("The_White_Silk", async (name: string, args: { [key: string]: unknown }, callback_: (ret: ApiCallbackMessage) => void) => {
function callback(ret: ApiCallbackMessage) { function callback(ret: ApiCallbackMessage) {
console.log(chalk.blue('[]') + ` ${ip} <- ${ret.code == 200 ? chalk.green(ret.msg) : chalk.red(ret.msg)} [${ret.code}]${ret.data ? (' <extras: ' + stringifyNotIncludeArrayBuffer(ret.data) + '>') : ''}`) console.log(chalk.blue('[]') + ` ${ip} <- ${ret.code == 200 ? chalk.green(ret.msg) : chalk.red(ret.msg)} [${ret.code}]${ret.data ? (' <extras: ' + stringifyNotIncludeArrayBuffer(ret.data) + '>') : ''}`)
return callback_(ret) return callback_(ret)
} }
async function checkIsPromiseAndAwait(value: Promise<unknown> | unknown) { async function checkIsPromiseAndAwait(value: Promise<unknown> | unknown) {
@@ -106,11 +106,11 @@ export default class ApiManager {
}) })
} catch (e) { } catch (e) {
const err = e as Error const err = e as Error
console.log(chalk.yellow('[]') + ` ${err.message} (${err.stack})`) console.log(chalk.yellow('[]') + ` ${err.message} (${err.stack})`)
try { try {
callback({ callback({
code: err instanceof DataWrongError ? 400 : 500, code: err instanceof DataWrongError ? 400 : 500,
msg: "錯誤: " + err.message msg: "错误: " + err.message
}) })
} catch (_e) { } } catch (_e) { }
} }

View File

@@ -22,24 +22,24 @@ export default class ChatApi extends BaseApi {
*/ */
this.registerEvent("Chat.getInfo", (args, { deviceId }) => { this.registerEvent("Chat.getInfo", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return { if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const chat = Chat.findById(args.target as string) const chat = Chat.findById(args.target as string)
if (chat == null) return { if (chat == null) return {
code: 404, code: 404,
msg: "對話不存在", msg: "对话不存在",
} }
if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return {
code: 400, code: 400,
msg: "用戶無權訪問該對話", msg: "用户无权访问此对话",
} }
// 私聊 // 私聊
@@ -83,24 +83,24 @@ export default class ChatApi extends BaseApi {
*/ */
this.registerEvent("Chat.sendMessage", (args, { deviceId }) => { this.registerEvent("Chat.sendMessage", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target', 'text'])) return { if (this.checkArgsMissing(args, ['token', 'target', 'text'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const chat = Chat.findById(args.target as string) const chat = Chat.findById(args.target as string)
if (chat == null) return { if (chat == null) return {
code: 404, code: 404,
msg: "對話不存在", msg: "对话不存在",
} }
if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return {
code: 400, code: 400,
msg: "用戶無權訪問該對話", msg: "用户无权访问此对话",
} }
const msg = { const msg = {
@@ -142,24 +142,24 @@ export default class ChatApi extends BaseApi {
*/ */
this.registerEvent("Chat.getMessageHistory", (args, { deviceId }) => { this.registerEvent("Chat.getMessageHistory", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target', 'page'])) return { if (this.checkArgsMissing(args, ['token', 'target', 'page'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const chat = Chat.findById(args.target as string) const chat = Chat.findById(args.target as string)
if (chat == null) return { if (chat == null) return {
code: 404, code: 404,
msg: "對話不存在", msg: "对话不存在",
} }
if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return {
code: 400, code: 400,
msg: "用戶無權訪問該對話", msg: "用户无权访问此对话",
} }
return { return {
@@ -179,24 +179,24 @@ export default class ChatApi extends BaseApi {
*/ */
this.registerEvent("Chat.uploadFile", async (args, { deviceId }) => { this.registerEvent("Chat.uploadFile", async (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target', 'data', 'file_name'])) return { if (this.checkArgsMissing(args, ['token', 'target', 'data', 'file_name'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const chat = Chat.findById(args.target as string) const chat = Chat.findById(args.target as string)
if (chat == null) return { if (chat == null) return {
code: 404, code: 404,
msg: "對話不存在", msg: "对话不存在",
} }
if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return {
code: 400, code: 400,
msg: "用戶無權訪問該對話", msg: "用户无权访问此对话",
} }
const file = await FileManager.uploadFile(args.file_name as string, args.data as Buffer<ArrayBufferLike>, args.target as string) const file = await FileManager.uploadFile(args.file_name as string, args.data as Buffer<ArrayBufferLike>, args.target as string)
@@ -216,20 +216,20 @@ export default class ChatApi extends BaseApi {
*/ */
this.registerEvent("Chat.getIdForPrivate", (args, { deviceId }) => { this.registerEvent("Chat.getIdForPrivate", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return { if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const user = User.findById(token.author) as User const user = User.findById(token.author) as User
const targetUser = User.findById(args.target as string) as User const targetUser = User.findById(args.target as string) as User
if (targetUser == null) { if (targetUser == null) {
return { return {
msg: "找不到用", msg: "找不到用",
code: 404, code: 404,
} }
} }
@@ -251,18 +251,18 @@ export default class ChatApi extends BaseApi {
*/ */
this.registerEvent("Chat.createGroup", (args, { deviceId }) => { this.registerEvent("Chat.createGroup", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'title'])) return { if (this.checkArgsMissing(args, ['token', 'title'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
if (this.checkArgsEmpty(args, ['title'])) return { if (this.checkArgsEmpty(args, ['title'])) return {
msg: "參數不得空", msg: "参数不得空",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const user = User.findById(token.author) as User const user = User.findById(token.author) as User
@@ -291,14 +291,14 @@ export default class ChatApi extends BaseApi {
*/ */
this.registerEvent("Chat.getAnotherUserIdFromPrivate", (args, { deviceId }) => { this.registerEvent("Chat.getAnotherUserIdFromPrivate", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return { if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const user = User.findById(token.author) as User const user = User.findById(token.author) as User
@@ -306,11 +306,11 @@ export default class ChatApi extends BaseApi {
const chat = Chat.findById(args.target as string) const chat = Chat.findById(args.target as string)
if (chat == null) return { if (chat == null) return {
code: 404, code: 404,
msg: "對話不存在", msg: "对话不存在",
} }
if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return {
code: 400, code: 400,
msg: "用戶無權訪問該對話", msg: "用户无权访问此对话",
} }
if (chat.bean.type == 'private') if (chat.bean.type == 'private')

View File

@@ -25,7 +25,7 @@ export default class EventStorer {
CREATE TABLE IF NOT EXISTS ${this.getTableName()} ( CREATE TABLE IF NOT EXISTS ${this.getTableName()} (
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT, /* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
/* 事件 */ event_name TEXT NOT NULL, /* 事件 */ event_name TEXT NOT NULL,
/* 數據 */ data TEXT NOT NULL, /* 数据 */ data TEXT NOT NULL,
); );
`) `)
} }

View File

@@ -1,7 +1,10 @@
import TokenType from "./TokenType.ts"
export default interface Token { export default interface Token {
author: string author: string
auth: string auth: string
made_time: number made_time: number
expired_time: number expired_time: number
device_id: string device_id: string
type: TokenType
} }

View File

@@ -3,6 +3,7 @@ import config from "../config.ts"
import User from "../data/User.ts" import User from "../data/User.ts"
import crypto from 'node:crypto' import crypto from 'node:crypto'
import Token from "./Token.ts" import Token from "./Token.ts"
import TokenType from "./TokenType.ts"
function normalizeKey(key: string, keyLength = 32) { function normalizeKey(key: string, keyLength = 32) {
const hash = crypto.createHash('sha256') const hash = crypto.createHash('sha256')
@@ -31,38 +32,28 @@ export default class TokenManager {
} }
} }
static make(user: User, time_: number | null | undefined, device_id: string) { static make(user: User, time_: number | null | undefined, device_id: string, type: TokenType = "access_token") {
const time = (time_ || Date.now()) const time = (time_ || Date.now())
return this.encode({ return this.encode({
author: user.bean.id, author: user.bean.id,
auth: this.makeAuth(user), auth: this.makeAuth(user),
made_time: time, made_time: time,
expired_time: time + (1 * 1000 * 60 * 60 * 24), expired_time: time + (type == 'access_token' ? (1000 * 60 * 60 * 2) : (40 * 1000 * 60 * 60 * 24)),
device_id: device_id device_id: device_id,
type
}) })
} }
/**
* 獲取新令牌
* 注意: 只驗證用戶, 不驗證令牌有效性!
*/
static makeNewer(user: User, token: string) {
if (this.check(user, token))
return this.make(user, Date.now() + (1 * 1000 * 60 * 60 * 24), this.decode(token).device_id)
}
static check(user: User, token: string) {
const tk = this.decode(token)
return this.makeAuth(user) == tk.auth
}
/** /**
* 嚴格檢驗令牌: 時間, 用戶, (設備 ID) * 嚴格檢驗令牌: 時間, 用戶, (設備 ID)
*/ */
static checkToken(token: Token, deviceId?: string) { static checkToken(token: Token, deviceId?: string, type: TokenType = 'access_token') {
if (token.expired_time < Date.now()) return false if (token.expired_time < Date.now()) return false
if (!token.author || !User.findById(token.author)) return false if (!token.author || !User.findById(token.author)) return false
if (deviceId != null) if (deviceId != null)
if (token.device_id != deviceId) if (token.device_id != deviceId)
return false return false
if (token.type != type)
return false
return true return true
} }
} }

3
server/api/TokenType.ts Normal file
View File

@@ -0,0 +1,3 @@
type TokenType = "access_token" | "refresh_token"
export default TokenType

View File

@@ -15,7 +15,7 @@ export default class UserApi extends BaseApi {
// 驗證 // 驗證
this.registerEvent("User.auth", (args, clientInfo) => { this.registerEvent("User.auth", (args, clientInfo) => {
if (this.checkArgsMissing(args, ['access_token'])) return { if (this.checkArgsMissing(args, ['access_token'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const { deviceId, ip, socket, sessionId } = clientInfo const { deviceId, ip, socket, sessionId } = clientInfo
@@ -23,20 +23,20 @@ export default class UserApi extends BaseApi {
const access_token = TokenManager.decode(args.access_token as string) const access_token = TokenManager.decode(args.access_token as string)
if (access_token.expired_time < Date.now()) return { if (access_token.expired_time < Date.now()) return {
msg: "登令牌失效", msg: "登令牌失效",
code: 401, code: 401,
} }
if (!access_token.author || !User.findById(access_token.author)) return { if (!access_token.author || !User.findById(access_token.author)) return {
msg: "賬號不存在", msg: "账号不存在",
code: 401, code: 401,
} }
if (access_token.device_id != deviceId) return { if (access_token.device_id != deviceId) return {
msg: "驗證失敗", msg: "验证失败",
code: 401, code: 401,
} }
clientInfo.userId = access_token.author clientInfo.userId = access_token.author
console.log(chalk.green('[]') + ` ${access_token.author} authed on Client ${deviceId} (ip = ${ip})`) console.log(chalk.green('[]') + ` ${access_token.author} authed on Client ${deviceId} (ip = ${ip})`)
if (ApiManager.clients[clientInfo.userId] == null) ApiManager.clients[clientInfo.userId] = { if (ApiManager.clients[clientInfo.userId] == null) ApiManager.clients[clientInfo.userId] = {
[deviceId + '_' + sessionId]: socket [deviceId + '_' + sessionId]: socket
} }
@@ -50,7 +50,50 @@ export default class UserApi extends BaseApi {
const err = e as Error const err = e as Error
if (err.message.indexOf("JSON") != -1) if (err.message.indexOf("JSON") != -1)
return { return {
msg: "效的登錄令牌", msg: "效的用户令牌",
code: 401,
}
else
throw e
}
})
// 刷新访问令牌
this.registerEvent("User.refreshAccessToken", (args, clientInfo) => {
if (this.checkArgsMissing(args, ['refresh_token'])) return {
msg: "参数缺失",
code: 400,
}
const { deviceId } = clientInfo
try {
const refresh_token = TokenManager.decode(args.refresh_token as string)
if (refresh_token.expired_time < Date.now()) return {
msg: "登录令牌失效",
code: 401,
}
if (!refresh_token.author || !User.findById(refresh_token.author)) return {
msg: "账号不存在",
code: 401,
}
if (refresh_token.device_id != deviceId) return {
msg: "验证失败",
code: 401,
}
const user = User.findById(refresh_token.author) as User
return {
msg: "成功",
code: 200,
data: {
access_token: TokenManager.make(user, null, deviceId)
}
}
} catch (e) {
const err = e as Error
if (err.message.indexOf("JSON") != -1)
return {
msg: "无效的用户令牌",
code: 401, code: 401,
} }
else else
@@ -60,17 +103,17 @@ export default class UserApi extends BaseApi {
// 登錄 // 登錄
this.registerEvent("User.login", (args, { deviceId }) => { this.registerEvent("User.login", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['account', 'password'])) return { if (this.checkArgsMissing(args, ['account', 'password'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
if (this.checkArgsEmpty(args, ['account', 'password'])) return { if (this.checkArgsEmpty(args, ['account', 'password'])) return {
msg: "參數不得空", msg: "参数不得空",
code: 400, code: 400,
} }
const user = User.findByAccount(args.account as string) as User const user = User.findByAccount(args.account as string) as User
if (user == null) return { if (user == null) return {
msg: "賬號或密碼錯誤", msg: "账号或密码错误",
code: 400, code: 400,
} }
@@ -78,23 +121,24 @@ export default class UserApi extends BaseApi {
msg: "成功", msg: "成功",
code: 200, code: 200,
data: { data: {
access_token: TokenManager.make(user, null, deviceId) refresh_token: TokenManager.make(user, null, deviceId, 'refresh_token'),
access_token: TokenManager.make(user, null, deviceId),
}, },
} }
return { return {
msg: "賬號或密碼錯誤", msg: "账号或密码错误",
code: 400, code: 400,
} }
}) })
// 注冊 // 注冊
this.registerEvent("User.register", (args, { deviceId }) => { this.registerEvent("User.register", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['nickname', 'password'])) return { if (this.checkArgsMissing(args, ['nickname', 'password'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
if (this.checkArgsEmpty(args, ['nickname', 'password'])) return { if (this.checkArgsEmpty(args, ['nickname', 'password'])) return {
msg: "參數不得空", msg: "参数不得空",
code: 400, code: 400,
} }
@@ -120,17 +164,17 @@ export default class UserApi extends BaseApi {
// 更新頭像 // 更新頭像
this.registerEvent("User.setAvatar", (args, { deviceId }) => { this.registerEvent("User.setAvatar", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['avatar', 'token'])) return { if (this.checkArgsMissing(args, ['avatar', 'token'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
if (!(args.avatar instanceof Buffer)) return { if (!(args.avatar instanceof Buffer)) return {
msg: "參數不合法", msg: "参数不合法",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const avatar: Buffer = args.avatar as Buffer const avatar: Buffer = args.avatar as Buffer
@@ -145,14 +189,14 @@ export default class UserApi extends BaseApi {
// 更新資料 // 更新資料
this.registerEvent("User.updateProfile", (args, { deviceId }) => { this.registerEvent("User.updateProfile", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token'])) return { if (this.checkArgsMissing(args, ['token'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const user = User.findById(token.author) const user = User.findById(token.author)
@@ -169,14 +213,14 @@ export default class UserApi extends BaseApi {
// 獲取用戶信息 // 獲取用戶信息
this.registerEvent("User.getMyInfo", (args, { deviceId }) => { this.registerEvent("User.getMyInfo", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token'])) return { if (this.checkArgsMissing(args, ['token'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const user = User.findById(token.author) const user = User.findById(token.author)
@@ -195,14 +239,14 @@ export default class UserApi extends BaseApi {
// 獲取最近对话列表 // 獲取最近对话列表
this.registerEvent("User.getMyRecentChats", (args, { deviceId }) => { this.registerEvent("User.getMyRecentChats", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token'])) return { if (this.checkArgsMissing(args, ['token'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const user = User.findById(token.author) as User const user = User.findById(token.author) as User
@@ -229,14 +273,14 @@ export default class UserApi extends BaseApi {
// 獲取聯絡人列表 // 獲取聯絡人列表
this.registerEvent("User.getMyContacts", (args, { deviceId }) => { this.registerEvent("User.getMyContacts", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token'])) return { if (this.checkArgsMissing(args, ['token'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const user = User.findById(token.author) as User const user = User.findById(token.author) as User
@@ -262,14 +306,14 @@ export default class UserApi extends BaseApi {
// 添加聯絡人 // 添加聯絡人
this.registerEvent("User.addContact", (args, { deviceId }) => { this.registerEvent("User.addContact", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return { if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const user = User.findById(token.author) as User const user = User.findById(token.author) as User
@@ -300,14 +344,14 @@ export default class UserApi extends BaseApi {
// 獲取用戶信息 // 獲取用戶信息
this.registerEvent("User.getInfo", (args, { deviceId }) => { this.registerEvent("User.getInfo", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return { if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失", msg: "参数缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌效", msg: "令牌效",
} }
const user = User.findById(args.target as string) const user = User.findById(args.target as string)

View File

@@ -24,11 +24,10 @@ export default class Chat {
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS ${Chat.table_name} ( CREATE TABLE IF NOT EXISTS ${Chat.table_name} (
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT, /* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
/* 型 */ type TEXT NOT NULL, /* 型 */ type TEXT NOT NULL,
/* Chat ID */ id TEXT NOT NULL, /* ID */ id TEXT NOT NULL,
/* 標題 (群組) */ title TEXT, /* 标题 */ title TEXT,
/* 頭像 (群組) */ avatar BLOB, /* 头像 */ avatar BLOB,
/* 成員 */ members_list TEXT,
/* 设置 */ settings TEXT NOT NULL /* 设置 */ settings TEXT NOT NULL
); );
`) `)
@@ -50,7 +49,7 @@ export default class Chat {
static create(chatId: string, type: ChatType) { static create(chatId: string, type: ChatType) {
if (this.findAllBeansByCondition('id = ?', chatId).length > 0) if (this.findAllBeansByCondition('id = ?', chatId).length > 0)
throw new DataWrongError(`对话ID ${chatId} 已被使用`) throw new DataWrongError(`对话 ID ${chatId} 已被使用`)
const chat = new Chat( const chat = new Chat(
Chat.findAllBeansByCondition( Chat.findAllBeansByCondition(
'count = ?', 'count = ?',

View File

@@ -79,7 +79,7 @@ export default class FileManager {
/* 文件名称 */ name TEXT NOT NULL, /* 文件名称 */ name TEXT NOT NULL,
/* 文件哈希 */ hash TEXT NOT NULL, /* 文件哈希 */ hash TEXT NOT NULL,
/* MIME 类型 */ mime TEXT NOT NULL, /* MIME 类型 */ mime TEXT NOT NULL,
/* 来源 Chat, 可為空 */ chatid TEXT, /* 来源对话 */ chatid TEXT,
/* 上传时间 */ upload_time INT8 NOT NULL, /* 上传时间 */ upload_time INT8 NOT NULL,
/* 最后使用时间 */ last_used_time INT8 NOT NULL /* 最后使用时间 */ last_used_time INT8 NOT NULL
); );

View File

@@ -28,7 +28,7 @@ export default class MessagesManager {
/* 序号, MessageId */ id INTEGER PRIMARY KEY AUTOINCREMENT, /* 序号, MessageId */ id INTEGER PRIMARY KEY AUTOINCREMENT,
/* 消息文本 */ text TEXT NOT NULL, /* 消息文本 */ text TEXT NOT NULL,
/* 发送者 */ user_id TEXT NOT NULL, /* 发送者 */ user_id TEXT NOT NULL,
/* 發送時間 */ time INT8 NOT NULL /* 发送时间 */ time INT8 NOT NULL
); );
`) `)
} }

View File

@@ -32,13 +32,13 @@ export default class User {
CREATE TABLE IF NOT EXISTS ${User.table_name} ( CREATE TABLE IF NOT EXISTS ${User.table_name} (
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT, /* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
/* 用户 ID, UUID */ id TEXT, /* 用户 ID, UUID */ id TEXT,
/* 密碼, 哈希 */ password TEXT, /* 密码摘要 */ password TEXT,
/* 注册时间, 时间戳 */ registered_time INT8 NOT NULL, /* 注册时间 */ registered_time INT8 NOT NULL,
/* 用戶名, 可選 */ username TEXT, /* 用户名 */ username TEXT,
/* 昵称 */ nickname TEXT NOT NULL, /* 昵称 */ nickname TEXT NOT NULL,
/* 头像, 可选 */ avatar_file_hash TEXT, /* 头像, 可选 */ avatar_file_hash TEXT,
/* 聯絡人列表 */ contacts_list TEXT NOT NULL, /* 对话列表 */ contacts_list TEXT NOT NULL,
/* 最近對話 */ recent_chats TEXT NOT NULL, /* 最近对话 */ recent_chats TEXT NOT NULL,
/* 设置 */ settings TEXT NOT NULL /* 设置 */ settings TEXT NOT NULL
); );
`) `)
@@ -147,7 +147,7 @@ export default class User {
try { try {
return JSON.parse(this.bean.contacts_list) as string[] return JSON.parse(this.bean.contacts_list) as string[]
} catch (e) { } catch (e) {
console.log(chalk.yellow(`警告: 聯絡人組解析失: ${(e as Error).message}`)) console.log(chalk.yellow(`警告: 所有对话解析失: ${(e as Error).message}`))
return [] return []
} }
} }

View File

@@ -70,10 +70,10 @@ ApiManager.initEvents()
ApiManager.initAllApis() ApiManager.initAllApis()
httpServer.listen(config.server.listen) httpServer.listen(config.server.listen)
console.log(chalk.green("API & Web 服務已經開始運作")) console.log(chalk.green("API & Web 服务已启动"))
function help() { function help() {
console.log(chalk.yellow("===== TheWhiteSilk Server =====")) console.log(chalk.yellow("===== LingChair Server ====="))
console.log(chalk.yellow("b - 重新編譯前端")) console.log(chalk.yellow("b - 重新编译前端"))
} }
help() help()
@@ -83,11 +83,10 @@ const rl = readline.createInterface({
}) })
rl.on('line', (text) => { rl.on('line', (text) => {
if (text == 'b') { if (text == 'b') {
console.log(chalk.green("重新編譯...")) console.log(chalk.green("重新编译..."))
child_process.spawnSync("deno", ["task", "build"], { child_process.spawnSync("deno", ["task", "build"], {
stdio: [process.stdin, process.stdout, process.stderr] stdio: [process.stdin, process.stdout, process.stderr]
}) })
console.log(chalk.green("✓ 編譯完畢"))
help() help()
} }
}) })

View File

@@ -1,42 +0,0 @@
import { DatabaseSync } from "node:sqlite"
import fs from 'node:fs/promises'
await fs.mkdir('data', { recursive: true })
const db = new DatabaseSync("data/users.db")
const TABEL_NAME = "Users"
// 初始化表格
db.exec(
`
CREATE TABLE IF NOT EXISTS ${TABEL_NAME} (
/* 伺服器中 ID */ id INTEGER PRIMARY KEY AUTOINCREMENT,
/* 用戶名, 可選 */ username TEXT,
/* 姓名 */ nickname TEXT NOT NULL,
/* 头像, 可选 */ avatar BLOB
);
`,
)
// 插入测试数据
db.prepare(
`
INSERT INTO ${TABEL_NAME} (username, nickname, avatar) VALUES (?, ?, ?);
`,
).run("SisterWen", "文姐", null)
let rows = db.prepare(`SELECT id, username, nickname, avatar FROM ${TABEL_NAME}`).all();
for (const row of rows) {
console.log(row)
}
// 更新用户名
// 用户名要合规, 以免导致 SQL 注入!
db.prepare(`UPDATE ${TABEL_NAME} SET username = ? WHERE id = ?`).run("文姐", 1)
rows = db.prepare(`SELECT id, username, nickname, avatar FROM ${TABEL_NAME} WHERE username = ?`).all("文姐")
for (const row of rows) {
console.log(row)
}
db.close()