Compare commits
7 Commits
fb48c44655
...
059078ea8f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
059078ea8f | ||
|
|
674fe000f4 | ||
|
|
85477fe46e | ||
|
|
dced175d7a | ||
|
|
bd857b840b | ||
|
|
5d1c395340 | ||
|
|
0e17b37156 |
@@ -51,15 +51,33 @@ class Client {
|
||||
})
|
||||
}
|
||||
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({
|
||||
code: -1,
|
||||
msg: err.message.indexOf("timed out") != -1 ? "請求超時" : err.message,
|
||||
})
|
||||
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) {
|
||||
const re = await this.invoke("User.auth", {
|
||||
access_token: token
|
||||
|
||||
BIN
client/icon.ico
Normal file
BIN
client/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -10,7 +10,7 @@
|
||||
<link rel="icon" href="icon.ico" />
|
||||
<link rel="stylesheet" href="./static/material_icons.css" />
|
||||
|
||||
<title>TheWhiteSilk</title>
|
||||
<title>LingChair</title>
|
||||
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function App() {
|
||||
if (re.code == 401)
|
||||
loginDialogRef.current!.open = true
|
||||
else if (re.code != 200) {
|
||||
if (checkApiSuccessOrSncakbar(re, "驗證失敗")) return
|
||||
if (checkApiSuccessOrSncakbar(re, "验证失败")) return
|
||||
} else if (re.code == 200) {
|
||||
setMyUserProfileCache(Client.myUserProfile as User)
|
||||
}
|
||||
@@ -201,7 +201,7 @@ export default function App() {
|
||||
textAlign: 'center',
|
||||
alignSelf: 'center',
|
||||
}}>
|
||||
選擇以開始對話...
|
||||
选择以开始对话......
|
||||
</div>
|
||||
}
|
||||
{
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function AppMobile() {
|
||||
if (re.code == 401)
|
||||
loginDialogRef.current!.open = true
|
||||
else if (re.code != 200) {
|
||||
if (checkApiSuccessOrSncakbar(re, "驗證失敗")) return
|
||||
if (checkApiSuccessOrSncakbar(re, "验证失败")) return
|
||||
} else if (re.code == 200) {
|
||||
setMyUserProfileCache(Client.myUserProfile as User)
|
||||
}
|
||||
@@ -191,8 +191,8 @@ export default function AppMobile() {
|
||||
}}>
|
||||
<mdui-top-app-bar-title>{
|
||||
({
|
||||
Recents: "最近對話",
|
||||
Contacts: "所有對話"
|
||||
Recents: "最近对话",
|
||||
Contacts: "所有对话"
|
||||
})[navigationItemSelected]
|
||||
}</mdui-top-app-bar-title>
|
||||
<div style={{
|
||||
@@ -234,8 +234,8 @@ export default function AppMobile() {
|
||||
position: 'sticky',
|
||||
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="chat--outlined" active-icon="chat--filled" value="Contacts">對話</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>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ const markedInstance = new marked.Marked({
|
||||
export default function ChatFragment({ target, showReturnButton, onReturnButtonClicked, openChatInfoDialog, openUserInfoDialog, ...props }: Args) {
|
||||
const [messagesList, setMessagesList] = React.useState([] as Message[])
|
||||
const [chatInfo, setChatInfo] = React.useState({
|
||||
title: '加載中...'
|
||||
title: '加载中...'
|
||||
} as Chat)
|
||||
|
||||
const [tabItemSelected, setTabItemSelected] = React.useState('None')
|
||||
@@ -68,7 +68,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
target: target,
|
||||
})
|
||||
if (re.code != 200)
|
||||
return target != '' && checkApiSuccessOrSncakbar(re, "對話錯誤")
|
||||
return target != '' && checkApiSuccessOrSncakbar(re, "获取对话信息失败")
|
||||
setChatInfo(re.data as Chat)
|
||||
|
||||
await loadMore()
|
||||
@@ -90,7 +90,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
page: page.current,
|
||||
})
|
||||
|
||||
if (checkApiSuccessOrSncakbar(re, "拉取歷史記錄失敗")) return
|
||||
if (checkApiSuccessOrSncakbar(re, "拉取对话记录失败")) return
|
||||
const returnMsgs = (re.data!.messages as Message[]).reverse()
|
||||
page.current++
|
||||
if (returnMsgs.length == 0) {
|
||||
@@ -150,7 +150,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
target,
|
||||
data: cachedFiles.current[fileName],
|
||||
}, 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 + ')')
|
||||
}
|
||||
}
|
||||
@@ -160,12 +160,12 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
target,
|
||||
text,
|
||||
}, 5000)
|
||||
if (checkApiSuccessOrSncakbar(re, "發送失敗")) return setIsMessageSending(false)
|
||||
if (checkApiSuccessOrSncakbar(re, "发送失败")) return setIsMessageSending(false)
|
||||
inputRef.current!.value = ''
|
||||
cachedFiles.current = {}
|
||||
} catch (e) {
|
||||
snackbar({
|
||||
message: '發送失敗: ' + (e as Error).message,
|
||||
message: '发送失败: ' + (e as Error).message,
|
||||
placement: 'top',
|
||||
})
|
||||
}
|
||||
@@ -188,7 +188,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
cachedFiles.current[name] = await data.arrayBuffer()
|
||||
cachedFileNamesCount.current[name] = 1
|
||||
if (type.startsWith('image/'))
|
||||
insertText(``)
|
||||
insertText(``)
|
||||
else if (type.startsWith('video/'))
|
||||
insertText(``)
|
||||
else
|
||||
@@ -227,7 +227,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
<mdui-tab value="Chat">{
|
||||
chatInfo.title
|
||||
}</mdui-tab>
|
||||
<mdui-tab value="Settings">設定</mdui-tab>
|
||||
<mdui-tab value="Settings">设置</mdui-tab>
|
||||
<mdui-tab value="None" style={{ display: 'none' }}></mdui-tab>
|
||||
<div style={{
|
||||
flexGrow: '1',
|
||||
@@ -265,7 +265,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
<span style={{
|
||||
alignSelf: 'center',
|
||||
paddingLeft: '12px',
|
||||
}}>加載中...</span>
|
||||
}}>加载中...</span>
|
||||
</div>
|
||||
<div style={{
|
||||
display: showNoMoreMessagesTip ? undefined : 'none',
|
||||
@@ -372,7 +372,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
addFile(type as string, getFileNameOrRandom(url), re)
|
||||
} catch (e) {
|
||||
snackbar({
|
||||
message: '無法解析連結: ' + (e as Error).message,
|
||||
message: '无法解析链接: ' + (e as Error).message,
|
||||
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() == '')
|
||||
cachedFiles.current = {}
|
||||
}} onKeyDown={(event) => {
|
||||
@@ -415,7 +415,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
<div style={{
|
||||
display: 'none'
|
||||
}}>
|
||||
<input accept="*/*" type="file" name="選擇附加文檔" multiple ref={attachFileInputRef}></input>
|
||||
<input accept="*/*" type="file" name="添加文件" multiple ref={attachFileInputRef}></input>
|
||||
</div>
|
||||
</div>
|
||||
</mdui-tab-panel>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Dropdown, Dialog } from "mdui"
|
||||
import { Dropdown, Dialog, dialog } from "mdui"
|
||||
import { $ } from "mdui/jq"
|
||||
import Client from "../../api/Client.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()
|
||||
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(rawData)}>複製原文</mdui-menu-item>
|
||||
<mdui-menu-item icon="info" onClick={() => messageJsonDialogRef.current!.open = true}>查看詳情</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="info" onClick={() => messageJsonDialogRef.current!.open = true}>JSON</mdui-menu-item>
|
||||
</mdui-menu>
|
||||
</mdui-dropdown>
|
||||
</mdui-card>
|
||||
|
||||
@@ -20,10 +20,10 @@ customElements.define('chat-image', class extends HTMLElement {
|
||||
e.onerror = () => {
|
||||
const src = $(this).attr('src')
|
||||
$(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', () => {
|
||||
snackbar({
|
||||
message: `圖片 (${src}) 無法加載!`,
|
||||
message: `图片 (${src}) 无法加载!`,
|
||||
placement: 'top'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ customElements.define('chat-video', class extends HTMLElement {
|
||||
}
|
||||
connectedCallback() {
|
||||
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.height = "100%"
|
||||
e.style.borderRadius = "var(--mdui-shape-corner-medium)"
|
||||
|
||||
@@ -35,8 +35,8 @@ export default function AddContactDialog({
|
||||
}
|
||||
|
||||
return (
|
||||
<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-dialog close-on-overlay-click close-on-esc headline="添加对话" ref={addContactDialogRef}>
|
||||
<mdui-text-field clearable label="对话 ID / 用户 ID / 用户名" ref={inputTargetRef as any} onKeyDown={(event) => {
|
||||
if (event.key == 'Enter')
|
||||
addContact()
|
||||
}}></mdui-text-field>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function ChatInfoDialog({ chat, chatInfoDialogRef, openChatFragme
|
||||
target: chat.id,
|
||||
})
|
||||
if (re.code != 200)
|
||||
return checkApiSuccessOrSncakbar(re, '獲取對話訊息失敗')
|
||||
return checkApiSuccessOrSncakbar(re, '获取对话信息失败')
|
||||
setChatInfo(re.data!.chat_info as Chat)
|
||||
})
|
||||
|
||||
|
||||
@@ -32,20 +32,21 @@ export default function LoginDialog({
|
||||
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.refresh_token = re.data!.refresh_token as string
|
||||
data.apply()
|
||||
location.reload()
|
||||
})
|
||||
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={{
|
||||
height: "10px",
|
||||
}}></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={loginButtonRef}>登录</mdui-button>
|
||||
|
||||
@@ -30,9 +30,9 @@ export default function MyProfileDialog({
|
||||
avatar: file
|
||||
})
|
||||
|
||||
if (checkApiSuccessOrSncakbar(re, "修改失敗")) return
|
||||
if (checkApiSuccessOrSncakbar(re, "修改失败")) return
|
||||
snackbar({
|
||||
message: "修改成功 (刷新頁面以更新)",
|
||||
message: "修改成功 (刷新页面以更新)",
|
||||
placement: "top",
|
||||
})
|
||||
})
|
||||
@@ -74,8 +74,8 @@ export default function MyProfileDialog({
|
||||
marginBottom: "10px",
|
||||
}}></mdui-divider>
|
||||
<mdui-list-item icon="logout" rounded onClick={() => dialog({
|
||||
headline: "退出登錄",
|
||||
description: "確定要退出登錄嗎? (若您的賬號未設定 用戶名, 請無務必複製 用戶 ID, 以免丟失賬號!)",
|
||||
headline: "退出登录",
|
||||
description: "请确保在退出登录前, 设定了用户名或者已经记录下了用户 ID, 以免无法登录账号",
|
||||
actions: [
|
||||
{
|
||||
text: "取消",
|
||||
@@ -84,7 +84,7 @@ export default function MyProfileDialog({
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "確定",
|
||||
text: "确定",
|
||||
onClick: () => {
|
||||
data.access_token = ''
|
||||
data.apply()
|
||||
@@ -93,7 +93,7 @@ export default function MyProfileDialog({
|
||||
},
|
||||
}
|
||||
],
|
||||
})}>退出登錄</mdui-list-item>
|
||||
})}>退出登录</mdui-list-item>
|
||||
</mdui-list>
|
||||
</mdui-dialog>
|
||||
{
|
||||
@@ -103,7 +103,7 @@ export default function MyProfileDialog({
|
||||
<div style={{
|
||||
display: "none"
|
||||
}}>
|
||||
<input type="file" name="選擇頭像" ref={chooseAvatarFileRef}
|
||||
<input type="file" name="选择头像" ref={chooseAvatarFileRef}
|
||||
accept="image/*" />
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function MyProfileDialog({
|
||||
width: '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",
|
||||
}} value={user?.nickname}></mdui-text-field>
|
||||
</div>
|
||||
@@ -123,12 +123,12 @@ export default function MyProfileDialog({
|
||||
marginTop: "10px",
|
||||
}}></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
|
||||
input.select()
|
||||
input.setSelectionRange(0, 1145141919810)
|
||||
}}></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={async () => {
|
||||
@@ -138,9 +138,9 @@ export default function MyProfileDialog({
|
||||
username: editUserNameRef.current?.value,
|
||||
})
|
||||
|
||||
if (checkApiSuccessOrSncakbar(re, "修改失敗")) return
|
||||
if (checkApiSuccessOrSncakbar(re, "修改失败")) return
|
||||
snackbar({
|
||||
message: "修改成功 (刷新頁面以更新)",
|
||||
message: "修改成功 (刷新页面以更新)",
|
||||
placement: "top",
|
||||
})
|
||||
userProfileEditDialogRef.current!.open = false
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function RegisterDialog({
|
||||
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
|
||||
loginInputPasswordRef.current!.value = registerInputPasswordRef.current!.value
|
||||
|
||||
@@ -43,25 +43,25 @@ export default function RegisterDialog({
|
||||
registerInputPasswordRef.current!.value = ""
|
||||
registerDialogRef.current!.open = false
|
||||
snackbar({
|
||||
message: "注冊成功!",
|
||||
message: "注册成功!",
|
||||
placement: "top",
|
||||
})
|
||||
})
|
||||
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={{
|
||||
height: "10px",
|
||||
}}></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={{
|
||||
height: "10px",
|
||||
}}></div>
|
||||
<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={doRegisterButtonRef}>注冊</mdui-button>
|
||||
<mdui-button slot="action" variant="text" ref={doRegisterButtonRef}>注册</mdui-button>
|
||||
</mdui-dialog>
|
||||
)
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export default function ContactsList({
|
||||
token: data.access_token,
|
||||
})
|
||||
if (re.code != 200)
|
||||
return checkApiSuccessOrSncakbar(re, "获取對話列表失败")
|
||||
return checkApiSuccessOrSncakbar(re, "获取所有对话列表失败")
|
||||
|
||||
setContactsList(re.data!.contacts_list as Chat[])
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export default function ContactsList({
|
||||
<mdui-list-item rounded style={{
|
||||
width: '100%',
|
||||
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={{
|
||||
width: '100%',
|
||||
}} icon="group_add" onClick={() => createGroupDialogRef.current!.open = true}>创建群组</mdui-list-item>
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function RecentsList({
|
||||
token: data.access_token,
|
||||
})
|
||||
if (re.code != 200)
|
||||
return checkApiSuccessOrSncakbar(re, "获取最近對話列表失败")
|
||||
return checkApiSuccessOrSncakbar(re, "获取最近对话列表失败")
|
||||
|
||||
setRecentsList(re.data!.recent_chats as RecentChat[])
|
||||
}
|
||||
|
||||
23
readme.md
23
readme.md
@@ -1,8 +1,8 @@
|
||||
## TheWhiteSilk
|
||||
## 铃之椅
|
||||
|
||||
這一個即時通訊項目————簡單, 輕量, 純粹, 時而天真
|
||||
一个即时通讯项目——简单, 轻量, 纯粹, 时而天真
|
||||
|
||||
後續會考慮改名為月之鴿
|
||||
_仍在积极开发中. 目前是第四代, 版本代号为: the_white_silk_
|
||||
|
||||
### 目前實現了什麽?
|
||||
|
||||
@@ -13,16 +13,17 @@
|
||||
- [x] 收發消息
|
||||
- [x] 富文本 (based on Marked)
|
||||
- [x] 圖片
|
||||
- [ ] 視頻
|
||||
- [ ] 文件
|
||||
- [x] 視頻
|
||||
- [x] 文件
|
||||
- [ ] 撤回消息
|
||||
- [ ] 修改消息
|
||||
|
||||
- 對話
|
||||
- [ ] _**最近對話**_
|
||||
- [x] 最近對話
|
||||
- [x] 添加對話
|
||||
- [x] 添加用戶
|
||||
- [ ] 添加群組 (伺服器端群組都還沒做, 想什麽呢)
|
||||
- [x] 添加群組
|
||||
- [ ] 群组管理
|
||||
|
||||
- 賬號
|
||||
- [x] 登錄注冊 (廢話)
|
||||
@@ -41,7 +42,7 @@
|
||||
|
||||
- 基本對話類型
|
||||
- [x] 雙用戶私聊
|
||||
- [ ] 群組
|
||||
- [x] 群組
|
||||
|
||||
- 消息
|
||||
- [x] 收發消息
|
||||
@@ -49,7 +50,7 @@
|
||||
- [ ] 修改消息
|
||||
|
||||
- 對話
|
||||
- [ ] _**最近對話**_
|
||||
- [x] 最近對話
|
||||
- [x] 添加對話
|
||||
|
||||
- 賬號
|
||||
@@ -64,8 +65,8 @@
|
||||
### 伺服器端運行
|
||||
|
||||
```bash
|
||||
git clone https://codeberg.org/CrescentLeaf/TheWhiteSilk
|
||||
cd TheWhiteSilk
|
||||
git clone https://codeberg.org/LingChair/LingChair
|
||||
cd LingChair
|
||||
# 編譯前端網頁
|
||||
deno task build
|
||||
# 運行服務
|
||||
|
||||
@@ -75,17 +75,17 @@ export default class ApiManager {
|
||||
|
||||
socket.on('disconnect', (_reason) => {
|
||||
if (clientInfo.userId == '')
|
||||
console.log(chalk.yellow('[斷]') + ` ${ip} disconnected`)
|
||||
console.log(chalk.yellow('[断]') + ` ${ip} disconnected`)
|
||||
else {
|
||||
console.log(chalk.green('[斷]') + ` ${ip} disconnected`)
|
||||
console.log(chalk.green('[断]') + ` ${ip} disconnected`)
|
||||
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) => {
|
||||
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)
|
||||
}
|
||||
async function checkIsPromiseAndAwait(value: Promise<unknown> | unknown) {
|
||||
@@ -106,11 +106,11 @@ export default class ApiManager {
|
||||
})
|
||||
} catch (e) {
|
||||
const err = e as Error
|
||||
console.log(chalk.yellow('[壞]') + ` ${err.message} (${err.stack})`)
|
||||
console.log(chalk.yellow('[坏]') + ` ${err.message} (${err.stack})`)
|
||||
try {
|
||||
callback({
|
||||
code: err instanceof DataWrongError ? 400 : 500,
|
||||
msg: "錯誤: " + err.message
|
||||
msg: "错误: " + err.message
|
||||
})
|
||||
} catch (_e) { }
|
||||
}
|
||||
|
||||
@@ -22,24 +22,24 @@ export default class ChatApi extends BaseApi {
|
||||
*/
|
||||
this.registerEvent("Chat.getInfo", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'target'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const chat = Chat.findById(args.target as string)
|
||||
if (chat == null) return {
|
||||
code: 404,
|
||||
msg: "對話不存在",
|
||||
msg: "对话不存在",
|
||||
}
|
||||
if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return {
|
||||
code: 400,
|
||||
msg: "用戶無權訪問該對話",
|
||||
msg: "用户无权访问此对话",
|
||||
}
|
||||
|
||||
// 私聊
|
||||
@@ -83,24 +83,24 @@ export default class ChatApi extends BaseApi {
|
||||
*/
|
||||
this.registerEvent("Chat.sendMessage", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'target', 'text'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const chat = Chat.findById(args.target as string)
|
||||
if (chat == null) return {
|
||||
code: 404,
|
||||
msg: "對話不存在",
|
||||
msg: "对话不存在",
|
||||
}
|
||||
if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return {
|
||||
code: 400,
|
||||
msg: "用戶無權訪問該對話",
|
||||
msg: "用户无权访问此对话",
|
||||
}
|
||||
|
||||
const msg = {
|
||||
@@ -142,24 +142,24 @@ export default class ChatApi extends BaseApi {
|
||||
*/
|
||||
this.registerEvent("Chat.getMessageHistory", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'target', 'page'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const chat = Chat.findById(args.target as string)
|
||||
if (chat == null) return {
|
||||
code: 404,
|
||||
msg: "對話不存在",
|
||||
msg: "对话不存在",
|
||||
}
|
||||
if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return {
|
||||
code: 400,
|
||||
msg: "用戶無權訪問該對話",
|
||||
msg: "用户无权访问此对话",
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -179,24 +179,24 @@ export default class ChatApi extends BaseApi {
|
||||
*/
|
||||
this.registerEvent("Chat.uploadFile", async (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'target', 'data', 'file_name'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const chat = Chat.findById(args.target as string)
|
||||
if (chat == null) return {
|
||||
code: 404,
|
||||
msg: "對話不存在",
|
||||
msg: "对话不存在",
|
||||
}
|
||||
if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return {
|
||||
code: 400,
|
||||
msg: "用戶無權訪問該對話",
|
||||
msg: "用户无权访问此对话",
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'target'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
const user = User.findById(token.author) as User
|
||||
const targetUser = User.findById(args.target as string) as User
|
||||
if (targetUser == null) {
|
||||
return {
|
||||
msg: "找不到用戶",
|
||||
msg: "找不到用户",
|
||||
code: 404,
|
||||
}
|
||||
}
|
||||
@@ -251,18 +251,18 @@ export default class ChatApi extends BaseApi {
|
||||
*/
|
||||
this.registerEvent("Chat.createGroup", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'title'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
if (this.checkArgsEmpty(args, ['title'])) return {
|
||||
msg: "參數不得為空",
|
||||
msg: "参数不得为空",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
const user = User.findById(token.author) as User
|
||||
|
||||
@@ -291,14 +291,14 @@ export default class ChatApi extends BaseApi {
|
||||
*/
|
||||
this.registerEvent("Chat.getAnotherUserIdFromPrivate", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'target'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
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)
|
||||
if (chat == null) return {
|
||||
code: 404,
|
||||
msg: "對話不存在",
|
||||
msg: "对话不存在",
|
||||
}
|
||||
if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return {
|
||||
code: 400,
|
||||
msg: "用戶無權訪問該對話",
|
||||
msg: "用户无权访问此对话",
|
||||
}
|
||||
|
||||
if (chat.bean.type == 'private')
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class EventStorer {
|
||||
CREATE TABLE IF NOT EXISTS ${this.getTableName()} (
|
||||
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/* 事件 */ event_name TEXT NOT NULL,
|
||||
/* 數據 */ data TEXT NOT NULL,
|
||||
/* 数据 */ data TEXT NOT NULL,
|
||||
);
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import TokenType from "./TokenType.ts"
|
||||
|
||||
export default interface Token {
|
||||
author: string
|
||||
auth: string
|
||||
made_time: number
|
||||
expired_time: number
|
||||
device_id: string
|
||||
type: TokenType
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import config from "../config.ts"
|
||||
import User from "../data/User.ts"
|
||||
import crypto from 'node:crypto'
|
||||
import Token from "./Token.ts"
|
||||
import TokenType from "./TokenType.ts"
|
||||
|
||||
function normalizeKey(key: string, keyLength = 32) {
|
||||
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())
|
||||
return this.encode({
|
||||
author: user.bean.id,
|
||||
auth: this.makeAuth(user),
|
||||
made_time: time,
|
||||
expired_time: time + (1 * 1000 * 60 * 60 * 24),
|
||||
device_id: device_id
|
||||
expired_time: time + (type == 'access_token' ? (1000 * 60 * 60 * 2) : (40 * 1000 * 60 * 60 * 24)),
|
||||
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)
|
||||
*/
|
||||
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.author || !User.findById(token.author)) return false
|
||||
if (deviceId != null)
|
||||
if (token.device_id != deviceId)
|
||||
return false
|
||||
if (token.type != type)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
3
server/api/TokenType.ts
Normal file
3
server/api/TokenType.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
type TokenType = "access_token" | "refresh_token"
|
||||
|
||||
export default TokenType
|
||||
@@ -15,7 +15,7 @@ export default class UserApi extends BaseApi {
|
||||
// 驗證
|
||||
this.registerEvent("User.auth", (args, clientInfo) => {
|
||||
if (this.checkArgsMissing(args, ['access_token'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
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)
|
||||
|
||||
if (access_token.expired_time < Date.now()) return {
|
||||
msg: "登錄令牌失效",
|
||||
msg: "登录令牌失效",
|
||||
code: 401,
|
||||
}
|
||||
if (!access_token.author || !User.findById(access_token.author)) return {
|
||||
msg: "賬號不存在",
|
||||
msg: "账号不存在",
|
||||
code: 401,
|
||||
}
|
||||
if (access_token.device_id != deviceId) return {
|
||||
msg: "驗證失敗",
|
||||
msg: "验证失败",
|
||||
code: 401,
|
||||
}
|
||||
|
||||
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] = {
|
||||
[deviceId + '_' + sessionId]: socket
|
||||
}
|
||||
@@ -50,7 +50,50 @@ export default class UserApi extends BaseApi {
|
||||
const err = e as Error
|
||||
if (err.message.indexOf("JSON") != -1)
|
||||
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,
|
||||
}
|
||||
else
|
||||
@@ -60,17 +103,17 @@ export default class UserApi extends BaseApi {
|
||||
// 登錄
|
||||
this.registerEvent("User.login", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['account', 'password'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
if (this.checkArgsEmpty(args, ['account', 'password'])) return {
|
||||
msg: "參數不得為空",
|
||||
msg: "参数不得为空",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const user = User.findByAccount(args.account as string) as User
|
||||
if (user == null) return {
|
||||
msg: "賬號或密碼錯誤",
|
||||
msg: "账号或密码错误",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
@@ -78,23 +121,24 @@ export default class UserApi extends BaseApi {
|
||||
msg: "成功",
|
||||
code: 200,
|
||||
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 {
|
||||
msg: "賬號或密碼錯誤",
|
||||
msg: "账号或密码错误",
|
||||
code: 400,
|
||||
}
|
||||
})
|
||||
// 注冊
|
||||
this.registerEvent("User.register", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['nickname', 'password'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
if (this.checkArgsEmpty(args, ['nickname', 'password'])) return {
|
||||
msg: "參數不得為空",
|
||||
msg: "参数不得为空",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
@@ -120,17 +164,17 @@ export default class UserApi extends BaseApi {
|
||||
// 更新頭像
|
||||
this.registerEvent("User.setAvatar", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['avatar', 'token'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
if (!(args.avatar instanceof Buffer)) return {
|
||||
msg: "參數不合法",
|
||||
msg: "参数不合法",
|
||||
code: 400,
|
||||
}
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const avatar: Buffer = args.avatar as Buffer
|
||||
@@ -145,14 +189,14 @@ export default class UserApi extends BaseApi {
|
||||
// 更新資料
|
||||
this.registerEvent("User.updateProfile", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const user = User.findById(token.author)
|
||||
@@ -169,14 +213,14 @@ export default class UserApi extends BaseApi {
|
||||
// 獲取用戶信息
|
||||
this.registerEvent("User.getMyInfo", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const user = User.findById(token.author)
|
||||
@@ -195,14 +239,14 @@ export default class UserApi extends BaseApi {
|
||||
// 獲取最近对话列表
|
||||
this.registerEvent("User.getMyRecentChats", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const user = User.findById(token.author) as User
|
||||
@@ -229,14 +273,14 @@ export default class UserApi extends BaseApi {
|
||||
// 獲取聯絡人列表
|
||||
this.registerEvent("User.getMyContacts", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const user = User.findById(token.author) as User
|
||||
@@ -262,14 +306,14 @@ export default class UserApi extends BaseApi {
|
||||
// 添加聯絡人
|
||||
this.registerEvent("User.addContact", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'target'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const user = User.findById(token.author) as User
|
||||
@@ -300,14 +344,14 @@ export default class UserApi extends BaseApi {
|
||||
// 獲取用戶信息
|
||||
this.registerEvent("User.getInfo", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'target'])) return {
|
||||
msg: "參數缺失",
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const token = TokenManager.decode(args.token as string)
|
||||
if (!this.checkToken(token, deviceId)) return {
|
||||
code: 401,
|
||||
msg: "令牌無效",
|
||||
msg: "令牌无效",
|
||||
}
|
||||
|
||||
const user = User.findById(args.target as string)
|
||||
|
||||
@@ -24,11 +24,10 @@ export default class Chat {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ${Chat.table_name} (
|
||||
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/* 類型 */ type TEXT NOT NULL,
|
||||
/* Chat ID */ id TEXT NOT NULL,
|
||||
/* 標題 (群組) */ title TEXT,
|
||||
/* 頭像 (群組) */ avatar BLOB,
|
||||
/* 成員 */ members_list TEXT,
|
||||
/* 类型 */ type TEXT NOT NULL,
|
||||
/* ID */ id TEXT NOT NULL,
|
||||
/* 标题 */ title TEXT,
|
||||
/* 头像 */ avatar BLOB,
|
||||
/* 设置 */ settings TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
@@ -50,7 +49,7 @@ export default class Chat {
|
||||
|
||||
static create(chatId: string, type: ChatType) {
|
||||
if (this.findAllBeansByCondition('id = ?', chatId).length > 0)
|
||||
throw new DataWrongError(`对话ID ${chatId} 已被使用`)
|
||||
throw new DataWrongError(`对话 ID ${chatId} 已被使用`)
|
||||
const chat = new Chat(
|
||||
Chat.findAllBeansByCondition(
|
||||
'count = ?',
|
||||
|
||||
@@ -79,7 +79,7 @@ export default class FileManager {
|
||||
/* 文件名称 */ name TEXT NOT NULL,
|
||||
/* 文件哈希 */ hash TEXT NOT NULL,
|
||||
/* MIME 类型 */ mime TEXT NOT NULL,
|
||||
/* 来源 Chat, 可為空 */ chatid TEXT,
|
||||
/* 来源对话 */ chatid TEXT,
|
||||
/* 上传时间 */ upload_time INT8 NOT NULL,
|
||||
/* 最后使用时间 */ last_used_time INT8 NOT NULL
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ export default class MessagesManager {
|
||||
/* 序号, MessageId */ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/* 消息文本 */ text TEXT NOT NULL,
|
||||
/* 发送者 */ user_id TEXT NOT NULL,
|
||||
/* 發送時間 */ time INT8 NOT NULL
|
||||
/* 发送时间 */ time INT8 NOT NULL
|
||||
);
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -32,13 +32,13 @@ export default class User {
|
||||
CREATE TABLE IF NOT EXISTS ${User.table_name} (
|
||||
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/* 用户 ID, UUID */ id TEXT,
|
||||
/* 密碼, 哈希 */ password TEXT,
|
||||
/* 注册时间, 时间戳 */ registered_time INT8 NOT NULL,
|
||||
/* 用戶名, 可選 */ username TEXT,
|
||||
/* 密码摘要 */ password TEXT,
|
||||
/* 注册时间 */ registered_time INT8 NOT NULL,
|
||||
/* 用户名 */ username TEXT,
|
||||
/* 昵称 */ nickname TEXT NOT NULL,
|
||||
/* 头像, 可选 */ avatar_file_hash TEXT,
|
||||
/* 聯絡人列表 */ contacts_list TEXT NOT NULL,
|
||||
/* 最近對話 */ recent_chats TEXT NOT NULL,
|
||||
/* 对话列表 */ contacts_list TEXT NOT NULL,
|
||||
/* 最近对话 */ recent_chats TEXT NOT NULL,
|
||||
/* 设置 */ settings TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
@@ -147,7 +147,7 @@ export default class User {
|
||||
try {
|
||||
return JSON.parse(this.bean.contacts_list) as string[]
|
||||
} catch (e) {
|
||||
console.log(chalk.yellow(`警告: 聯絡人組解析失敗: ${(e as Error).message}`))
|
||||
console.log(chalk.yellow(`警告: 所有对话解析失败: ${(e as Error).message}`))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +70,10 @@ ApiManager.initEvents()
|
||||
ApiManager.initAllApis()
|
||||
|
||||
httpServer.listen(config.server.listen)
|
||||
console.log(chalk.green("API & Web 服務已經開始運作"))
|
||||
console.log(chalk.green("API & Web 服务已启动"))
|
||||
function help() {
|
||||
console.log(chalk.yellow("===== TheWhiteSilk Server ====="))
|
||||
console.log(chalk.yellow("b - 重新編譯前端"))
|
||||
console.log(chalk.yellow("===== LingChair Server ====="))
|
||||
console.log(chalk.yellow("b - 重新编译前端"))
|
||||
}
|
||||
help()
|
||||
|
||||
@@ -83,11 +83,10 @@ const rl = readline.createInterface({
|
||||
})
|
||||
rl.on('line', (text) => {
|
||||
if (text == 'b') {
|
||||
console.log(chalk.green("重新編譯..."))
|
||||
console.log(chalk.green("重新编译..."))
|
||||
child_process.spawnSync("deno", ["task", "build"], {
|
||||
stdio: [process.stdin, process.stdout, process.stderr]
|
||||
})
|
||||
console.log(chalk.green("✓ 編譯完畢"))
|
||||
help()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user