Compare commits
26 Commits
4bcc6e4347
...
19657fd150
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19657fd150 | ||
|
|
a6cef76ecf | ||
|
|
ee8e0e531e | ||
|
|
151dc31f2c | ||
|
|
0b1a4a53a5 | ||
|
|
02efac9a8e | ||
|
|
8d739dd863 | ||
|
|
c0c6c6ed1c | ||
|
|
d26c67f06d | ||
|
|
35d60642c0 | ||
|
|
a928577f2a | ||
|
|
4b93e5fd67 | ||
|
|
d6454f51c8 | ||
|
|
efc0f49b66 | ||
|
|
a860da96a0 | ||
|
|
692eb3d2a3 | ||
|
|
b6be09ef7c | ||
|
|
8e15c8126f | ||
|
|
80a42d5d86 | ||
|
|
b8f3886a1b | ||
|
|
5a80041ec3 | ||
|
|
d76e7e2bf5 | ||
|
|
4fa3e16ab7 | ||
|
|
9cc3a2149e | ||
|
|
fdf52c0548 | ||
|
|
a6ee231ad5 |
12
client/EventBus.ts
Normal file
12
client/EventBus.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export default class EventBus {
|
||||
static events: { [key: string]: () => void } = {}
|
||||
static on(eventName: string, func: () => void) {
|
||||
this.events[eventName] = func
|
||||
}
|
||||
static off(eventName: string) {
|
||||
delete this.events[eventName]
|
||||
}
|
||||
static emit(eventName: string) {
|
||||
this.events[eventName]()
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,11 @@ import data from "../Data.ts"
|
||||
import { checkApiSuccessOrSncakbar } from "../ui/snackbar.ts"
|
||||
import randomUUID from "../randomUUID.ts"
|
||||
|
||||
type UnknownObject = { [key: string]: unknown }
|
||||
|
||||
class Client {
|
||||
static sessionId = randomUUID()
|
||||
static myUserProfile?: User
|
||||
static socket?: Socket
|
||||
static events: { [key: string]: (data: UnknownObject) => UnknownObject | void } = {}
|
||||
static events: { [key: string]: ((data: unknown) => void)[] } = {}
|
||||
static connected = false
|
||||
static connect() {
|
||||
if (data.device_id == null)
|
||||
@@ -37,17 +35,16 @@ class Client {
|
||||
this.socket!.on("disconnect", () => {
|
||||
this.connected = false
|
||||
})
|
||||
this.socket!.on("The_White_Silk", (name: string, data: UnknownObject, callback: (ret: UnknownObject) => void) => {
|
||||
this.socket!.on("The_White_Silk", (name: string, data: unknown, callback: (ret: unknown) => void) => {
|
||||
try {
|
||||
if (name == null || data == null) return
|
||||
const re = this.events[name]?.(data)
|
||||
re && callback(re)
|
||||
this.events[name]?.forEach((v) => v(data))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
static invoke(method: CallMethod, args: UnknownObject = {}, timeout: number = 5000): Promise<ApiCallbackMessage> {
|
||||
static invoke(method: CallMethod, args: unknown = {}, timeout: number = 5000): Promise<ApiCallbackMessage> {
|
||||
if (this.socket == null || (!this.connected && !CallableMethodBeforeAuth.includes(method))) {
|
||||
return new Promise((reslove) => {
|
||||
setTimeout(async () => reslove(await this.invoke(method, args, timeout)), 500)
|
||||
@@ -67,8 +64,11 @@ class Client {
|
||||
const re = await this.invoke("User.auth", {
|
||||
access_token: token
|
||||
}, timeout)
|
||||
if (re.code == 200)
|
||||
if (re.code == 200) {
|
||||
await this.updateCachedProfile()
|
||||
document.cookie = 'token=' + token
|
||||
document.cookie = 'device_id=' + data.device_id
|
||||
}
|
||||
return re
|
||||
}
|
||||
static async updateCachedProfile() {
|
||||
@@ -76,11 +76,18 @@ class Client {
|
||||
token: data.access_token
|
||||
})).data as unknown as User
|
||||
}
|
||||
static on(eventName: ClientEvent, func: (data: UnknownObject) => UnknownObject | void) {
|
||||
this.events[eventName] = func
|
||||
static on(eventName: ClientEvent, func: (data: unknown) => void) {
|
||||
if (this.events[eventName] == null)
|
||||
this.events[eventName] = []
|
||||
if (this.events[eventName].indexOf(func) == -1)
|
||||
this.events[eventName].push(func)
|
||||
}
|
||||
static off(eventName: ClientEvent) {
|
||||
delete this.events[eventName]
|
||||
static off(eventName: ClientEvent, func: (data: unknown) => void) {
|
||||
if (this.events[eventName] == null)
|
||||
this.events[eventName] = []
|
||||
const index = this.events[eventName].indexOf(func)
|
||||
if (index != -1)
|
||||
this.events[eventName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<span slot="headline">错误</span>
|
||||
<span slot="description" id="ErrorDialog_Message"></span>
|
||||
</mdui-dialog>
|
||||
<input style="display: none;" id="copy_to_clipboard_fallback"></input>
|
||||
|
||||
<script nomodule>
|
||||
alert('很抱歉, 此应用无法在较旧的浏览器运行, 请使用基于 Chromium 89+ 的浏览器(内核)使用 :(')
|
||||
|
||||
@@ -18,8 +18,9 @@ import UserProfileDialog from "./dialog/UserProfileDialog.tsx"
|
||||
import ContactsList from "./main/ContactsList.tsx"
|
||||
import RecentsList from "./main/RecentsList.tsx"
|
||||
import useAsyncEffect from "./useAsyncEffect.ts"
|
||||
import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx";
|
||||
import Chat from "../api/client_data/Chat.ts";
|
||||
import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx"
|
||||
import Chat from "../api/client_data/Chat.ts"
|
||||
import AddContactDialog from './dialog/AddContactDialog.tsx'
|
||||
|
||||
declare global {
|
||||
namespace React {
|
||||
@@ -57,6 +58,8 @@ export default function App() {
|
||||
userProfileDialogRef.current!.open = true
|
||||
})
|
||||
|
||||
const addContactDialogRef = React.useRef<Dialog>(null)
|
||||
|
||||
const chatInfoDialogRef = React.useRef<Dialog>(null)
|
||||
const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat)
|
||||
|
||||
@@ -121,13 +124,16 @@ export default function App() {
|
||||
}}
|
||||
chat={chatInfo} />
|
||||
|
||||
<AddContactDialog
|
||||
addContactDialogRef={addContactDialogRef} />
|
||||
|
||||
<mdui-navigation-rail contained value="Recents" ref={navigationRailRef}>
|
||||
<mdui-button-icon slot="top">
|
||||
<Avatar src={myUserProfileCache?.avatar} text={myUserProfileCache?.nickname} avatarRef={openMyUserProfileDialogButtonRef} />
|
||||
</mdui-button-icon>
|
||||
|
||||
<mdui-navigation-rail-item icon="watch_later--outlined" active-icon="watch_later--filled" value="Recents"></mdui-navigation-rail-item>
|
||||
<mdui-navigation-rail-item icon="contacts--outlined" active-icon="contacts--filled" value="Contacts"></mdui-navigation-rail-item>
|
||||
<mdui-navigation-rail-item icon="chat--outlined" active-icon="chat--filled" value="Contacts"></mdui-navigation-rail-item>
|
||||
|
||||
<mdui-button-icon icon="settings" slot="bottom"></mdui-button-icon>
|
||||
</mdui-navigation-rail>
|
||||
@@ -148,9 +154,10 @@ export default function App() {
|
||||
setRecentsList={setRecentsList} />
|
||||
}
|
||||
{
|
||||
// 联系人列表
|
||||
// 對話列表
|
||||
<ContactsList
|
||||
setChatInfo={setChatInfo}
|
||||
addContactDialogRef={addContactDialogRef as any}
|
||||
chatInfoDialogRef={chatInfoDialogRef as any}
|
||||
display={navigationItemSelected == "Contacts"} />
|
||||
}
|
||||
@@ -168,12 +175,13 @@ export default function App() {
|
||||
textAlign: 'center',
|
||||
alignSelf: 'center',
|
||||
}}>
|
||||
選擇聯絡人以開始對話...
|
||||
選擇以開始對話...
|
||||
</div>
|
||||
}
|
||||
{
|
||||
isShowChatFragment && <ChatFragment
|
||||
target={currentChatId} />
|
||||
target={currentChatId}
|
||||
key={currentChatId} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import RecentsList from "./main/RecentsList.tsx"
|
||||
import useAsyncEffect from "./useAsyncEffect.ts"
|
||||
import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx"
|
||||
import Chat from "../api/client_data/Chat.ts"
|
||||
import AddContactDialog from './dialog/AddContactDialog.tsx'
|
||||
|
||||
declare global {
|
||||
namespace React {
|
||||
@@ -56,6 +57,8 @@ export default function AppMobile() {
|
||||
userProfileDialogRef.current!.open = true
|
||||
})
|
||||
|
||||
const addContactDialogRef = React.useRef<Dialog>(null)
|
||||
|
||||
const chatInfoDialogRef = React.useRef<Dialog>(null)
|
||||
const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat)
|
||||
|
||||
@@ -109,6 +112,7 @@ export default function AppMobile() {
|
||||
<ChatFragment
|
||||
showReturnButton={true}
|
||||
onReturnButtonClicked={() => setIsShowChatFragment(false)}
|
||||
key={currentChatId}
|
||||
target={currentChatId} />
|
||||
</div>
|
||||
</mdui-dialog>
|
||||
@@ -139,6 +143,9 @@ export default function AppMobile() {
|
||||
}}
|
||||
chat={chatInfo} />
|
||||
|
||||
<AddContactDialog
|
||||
addContactDialogRef={addContactDialogRef} />
|
||||
|
||||
<mdui-top-app-bar style={{
|
||||
position: 'sticky',
|
||||
marginTop: '3px',
|
||||
@@ -149,7 +156,7 @@ export default function AppMobile() {
|
||||
<mdui-top-app-bar-title>{
|
||||
({
|
||||
Recents: "最近對話",
|
||||
Contacts: "聯絡人"
|
||||
Contacts: "所有對話"
|
||||
})[navigationItemSelected]
|
||||
}</mdui-top-app-bar-title>
|
||||
<div style={{
|
||||
@@ -181,9 +188,10 @@ export default function AppMobile() {
|
||||
setRecentsList={setRecentsList} />
|
||||
}
|
||||
{
|
||||
// 联系人列表
|
||||
// 對話列表
|
||||
<ContactsList
|
||||
setChatInfo={setChatInfo}
|
||||
addContactDialogRef={addContactDialogRef as any}
|
||||
chatInfoDialogRef={chatInfoDialogRef as any}
|
||||
display={navigationItemSelected == "Contacts"} />
|
||||
}
|
||||
@@ -193,7 +201,7 @@ export default function AppMobile() {
|
||||
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="contacts--outlined" active-icon="contacts--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>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
chat: string
|
||||
msg: Message
|
||||
}
|
||||
Client.on('Client.onMessage', (data: unknown) => {
|
||||
function callback(data: unknown) {
|
||||
const { chat, msg } = (data as OnMessageData)
|
||||
if (target == chat) {
|
||||
setMessagesList(messagesList.concat([msg]))
|
||||
@@ -110,9 +110,11 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
behavior: "smooth",
|
||||
}), 100)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Client.on('Client.onMessage', callback)
|
||||
return () => {
|
||||
Client.off('Client.onMessage')
|
||||
Client.off('Client.onMessage', callback)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
14
client/ui/copyToClipboard.ts
Normal file
14
client/ui/copyToClipboard.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { $ } from 'mdui/jq'
|
||||
|
||||
export default function copyToClipboard(text: string) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
const input = $('#copy_to_clipboard_fallback').get(0) as HTMLInputElement
|
||||
input.value = text
|
||||
input.select()
|
||||
input.setSelectionRange(0, 1145141919810)
|
||||
document.execCommand('copy')
|
||||
input.clearSelection()
|
||||
}
|
||||
}
|
||||
41
client/ui/dialog/AddContactDialog.tsx
Normal file
41
client/ui/dialog/AddContactDialog.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react'
|
||||
import { Button, Dialog, TextField } from "mdui"
|
||||
import useEventListener from "../useEventListener.ts"
|
||||
import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts"
|
||||
import Client from "../../api/Client.ts"
|
||||
|
||||
import * as CryptoJS from 'crypto-js'
|
||||
import data from "../../Data.ts"
|
||||
import EventBus from "../../EventBus.ts"
|
||||
|
||||
interface Refs {
|
||||
addContactDialogRef: React.MutableRefObject<Dialog | null>
|
||||
}
|
||||
|
||||
export default function AddContactDialog({
|
||||
addContactDialogRef,
|
||||
}: Refs) {
|
||||
const inputUserAccountRef = React.useRef<TextField>(null)
|
||||
return (
|
||||
<mdui-dialog close-on-overlay-click close-on-esc headline="添加對話" ref={addContactDialogRef}>
|
||||
現階段只支持添加用戶, 對話敬請期待...
|
||||
<mdui-text-field style={{ marginTop: "10px", }} label="對方的 用戶 ID / 用戶名" ref={inputUserAccountRef as any}></mdui-text-field>
|
||||
<mdui-button slot="action" variant="text" onClick={() => addContactDialogRef.current!.open = false}>取消</mdui-button>
|
||||
<mdui-button slot="action" variant="text" onClick={async () => {
|
||||
const re = await Client.invoke("User.addContact", {
|
||||
account: inputUserAccountRef.current!.value,
|
||||
token: data.access_token,
|
||||
})
|
||||
|
||||
if (checkApiSuccessOrSncakbar(re, "添加失敗")) return
|
||||
snackbar({
|
||||
message: "添加成功!",
|
||||
placement: "top",
|
||||
})
|
||||
EventBus.emit('ContactsList.updateContacts')
|
||||
|
||||
addContactDialogRef.current!.open = false
|
||||
}}>添加</mdui-button>
|
||||
</mdui-dialog>
|
||||
)
|
||||
}
|
||||
@@ -20,8 +20,8 @@ export default function LoginDialog({
|
||||
loginDialogRef,
|
||||
registerDialogRef
|
||||
}: Refs) {
|
||||
const loginButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||
const registerButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||
const loginButtonRef = React.useRef<Button>(null)
|
||||
const registerButtonRef = React.useRef<Button>(null)
|
||||
useEventListener(registerButtonRef, 'click', () => registerDialogRef.current!.open = true)
|
||||
useEventListener(loginButtonRef, 'click', async () => {
|
||||
const account = loginInputAccountRef.current!.value
|
||||
@@ -41,11 +41,11 @@ export default function LoginDialog({
|
||||
return (
|
||||
<mdui-dialog headline="登錄" ref={loginDialogRef}>
|
||||
|
||||
<mdui-text-field label="用戶 ID / 用戶名" ref={loginInputAccountRef}></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}></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>
|
||||
|
||||
@@ -23,8 +23,8 @@ export default function RegisterDialog({
|
||||
registerInputPasswordRef,
|
||||
registerDialogRef
|
||||
}: Refs) {
|
||||
const registerBackButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||
const doRegisterButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||
const registerBackButtonRef = React.useRef<Button>(null)
|
||||
const doRegisterButtonRef = React.useRef<Button>(null)
|
||||
useEventListener(registerBackButtonRef, 'click', () => registerDialogRef.current!.open = false)
|
||||
useEventListener(doRegisterButtonRef, 'click', async () => {
|
||||
const username = registerInputUserNameRef.current!.value
|
||||
@@ -50,15 +50,15 @@ export default function RegisterDialog({
|
||||
return (
|
||||
<mdui-dialog headline="注冊" ref={registerDialogRef}>
|
||||
|
||||
<mdui-text-field label="用戶名 (可選)" ref={registerInputUserNameRef}></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}></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}></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={doRegisterButtonRef}>注冊</mdui-button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react'
|
||||
import { Button, Dialog, TextField } from "mdui"
|
||||
import { Button, Dialog, TextField, dialog } from "mdui"
|
||||
import useEventListener from "../useEventListener.ts"
|
||||
import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts"
|
||||
import Client from "../../api/Client.ts"
|
||||
@@ -63,7 +63,6 @@ export default function UserProfileDialog({
|
||||
</div>
|
||||
<mdui-divider style={{
|
||||
marginTop: "10px",
|
||||
marginBottom: "10px",
|
||||
}}></mdui-divider>
|
||||
|
||||
<mdui-list>
|
||||
@@ -71,8 +70,35 @@ export default function UserProfileDialog({
|
||||
{
|
||||
isMySelf && <>
|
||||
<mdui-list-item icon="edit" rounded onClick={() => userProfileEditDialogRef.current!.open = true}>編輯資料</mdui-list-item>
|
||||
{/*
|
||||
<mdui-list-item icon="settings" rounded>賬號設定</mdui-list-item>
|
||||
<mdui-list-item icon="lock" rounded>隱私設定</mdui-list-item>
|
||||
*/}
|
||||
<mdui-divider style={{
|
||||
marginTop: "10px",
|
||||
marginBottom: "10px",
|
||||
}}></mdui-divider>
|
||||
<mdui-list-item icon="logout" rounded onClick={() => dialog({
|
||||
headline: "退出登錄",
|
||||
description: "確定要退出登錄嗎? (若您的賬號未設定 用戶名, 請無務必複製 用戶 ID, 以免丟失賬號!)",
|
||||
actions: [
|
||||
{
|
||||
text: "取消",
|
||||
onClick: () => {
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "確定",
|
||||
onClick: () => {
|
||||
data.access_token = ''
|
||||
data.apply()
|
||||
location.reload()
|
||||
return true
|
||||
},
|
||||
}
|
||||
],
|
||||
})}>退出登錄</mdui-list-item>
|
||||
</>
|
||||
}
|
||||
</mdui-list>
|
||||
@@ -102,10 +128,14 @@ export default function UserProfileDialog({
|
||||
</div>
|
||||
<mdui-divider style={{
|
||||
marginTop: "10px",
|
||||
marginBottom: "10px",
|
||||
}}></mdui-divider>
|
||||
|
||||
<mdui-text-field variant="outlined" label="用戶名" value={user?.username || ''} ref={editUserNameRef as any}></mdui-text-field>
|
||||
<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-button slot="action" variant="text" onClick={() => userProfileEditDialogRef.current!.open = false}>取消</mdui-button>
|
||||
<mdui-button slot="action" variant="text" onClick={async () => {
|
||||
|
||||
@@ -7,10 +7,12 @@ import Client from "../../api/Client.ts"
|
||||
import data from "../../Data.ts"
|
||||
import { checkApiSuccessOrSncakbar } from "../snackbar.ts"
|
||||
import Chat from "../../api/client_data/Chat.ts"
|
||||
import EventBus from "../../EventBus.ts"
|
||||
|
||||
interface Args extends React.HTMLAttributes<HTMLElement> {
|
||||
display: boolean
|
||||
chatInfoDialogRef: React.MutableRefObject<Dialog>
|
||||
addContactDialogRef: React.MutableRefObject<Dialog>
|
||||
setChatInfo: React.Dispatch<React.SetStateAction<Chat>>
|
||||
}
|
||||
|
||||
@@ -18,6 +20,7 @@ export default function ContactsList({
|
||||
display,
|
||||
setChatInfo,
|
||||
chatInfoDialogRef,
|
||||
addContactDialogRef,
|
||||
...props
|
||||
}: Args) {
|
||||
const searchRef = React.useRef<HTMLElement>(null)
|
||||
@@ -30,13 +33,17 @@ export default function ContactsList({
|
||||
})
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
async function updateContacts() {
|
||||
const re = await Client.invoke("User.getMyContacts", {
|
||||
token: data.access_token,
|
||||
})
|
||||
if (re.code != 200)
|
||||
return checkApiSuccessOrSncakbar(re, "获取联络人列表失败")
|
||||
return checkApiSuccessOrSncakbar(re, "获取對話列表失败")
|
||||
|
||||
setContactsList(re.data!.contacts_list as Chat[])
|
||||
}
|
||||
updateContacts()
|
||||
EventBus.on('ContactsList.updateContacts', () => updateContacts())
|
||||
})
|
||||
|
||||
return <mdui-list style={{
|
||||
@@ -54,8 +61,11 @@ export default function ContactsList({
|
||||
<mdui-list-item rounded style={{
|
||||
width: '100%',
|
||||
marginTop: '13px',
|
||||
}} icon="person_add" onClick={() => addContactDialogRef.current!.open = true}>添加對話</mdui-list-item>
|
||||
<mdui-list-item rounded style={{
|
||||
width: '100%',
|
||||
marginBottom: '15px',
|
||||
}} icon="person_add">添加聯絡人</mdui-list-item>
|
||||
}} icon="refresh" onClick={() => EventBus.emit('ContactsList.updateContacts')}>刷新</mdui-list-item>
|
||||
{/* <mdui-list-item rounded style={{
|
||||
width: '100%',
|
||||
marginBottom: '15px',
|
||||
|
||||
@@ -3,6 +3,6 @@ import * as React from 'react'
|
||||
export default function useEventListener<T extends HTMLElement | null>(ref: React.MutableRefObject<T>, eventName: string, callback: (event: Event) => void) {
|
||||
React.useEffect(() => {
|
||||
ref.current!.addEventListener(eventName, callback)
|
||||
return () => ref.current!.removeEventListener(eventName, callback)
|
||||
return () => ref.current?.removeEventListener(eventName, callback)
|
||||
}, [ref, eventName, callback])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"tasks": {
|
||||
"server": "deno task build && deno run --allow-read --allow-write --allow-env --allow-net --allow-sys --allow-run=deno ./server/main.ts",
|
||||
"server": "deno run --allow-read --allow-write --allow-env --allow-net --allow-sys --allow-run=deno ./server/main.ts",
|
||||
"debug": "deno task build && deno run --watch --allow-read --allow-write --allow-env --allow-net --allow-sys --allow-run=deno ./server/main.ts",
|
||||
"build": "cd ./client && deno task build"
|
||||
},
|
||||
@@ -8,6 +8,7 @@
|
||||
"chalk": "npm:chalk@5.4.1",
|
||||
"file-type": "npm:file-type@21.0.0",
|
||||
"express": "npm:express@5.1.0",
|
||||
"socket.io": "npm:socket.io@4.8.1"
|
||||
"socket.io": "npm:socket.io@4.8.1",
|
||||
"cookie-parser": "npm:cookie-parser@1.4.7"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import ApiManager from "./ApiManager.ts"
|
||||
import { CallMethod, ClientEvent } from './ApiDeclare.ts'
|
||||
import User from "../data/User.ts"
|
||||
import Token from "./Token.ts"
|
||||
import TokenManager from './TokenManager.ts'
|
||||
import * as SocketIo from "socket.io"
|
||||
|
||||
export default abstract class BaseApi {
|
||||
@@ -24,12 +25,7 @@ export default abstract class BaseApi {
|
||||
return false
|
||||
}
|
||||
checkToken(token: Token, deviceId: string) {
|
||||
if (token.expired_time < Date.now()) return false
|
||||
if (!User.findById(token.author)) return false
|
||||
if (deviceId != null)
|
||||
if (token.device_id != deviceId)
|
||||
return false
|
||||
return true
|
||||
return TokenManager.checkToken(token, deviceId)
|
||||
}
|
||||
registerEvent(name: CallMethod, func: EventCallbackFunction) {
|
||||
if (!name.startsWith(this.getName() + ".")) throw Error("注冊的事件應該與接口集合命名空間相匹配: " + name)
|
||||
|
||||
@@ -90,11 +90,7 @@ export default class ChatApi extends BaseApi {
|
||||
}
|
||||
const id = MessagesManager.getInstanceForChat(chat).addMessage(msg)
|
||||
|
||||
const users: string[] = []
|
||||
if (chat.bean.type == 'private') {
|
||||
users.push(token.author as string)
|
||||
}
|
||||
|
||||
const users: string[] = UserChatLinker.getChatMembers(chat.bean.id)
|
||||
for (const user of users) {
|
||||
if (ApiManager.checkUserIsOnline(user)) {
|
||||
const sockets = ApiManager.getUserClientSockets(user)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Buffer } from "node:buffer"
|
||||
import config from "../config.ts"
|
||||
import User from "../data/User.ts"
|
||||
import crypto from 'node:crypto'
|
||||
import Token from "./Token.ts"
|
||||
|
||||
function normalizeKey(key: string, keyLength = 32) {
|
||||
const hash = crypto.createHash('sha256')
|
||||
hash.update(key)
|
||||
const keyBuffer = hash.digest()
|
||||
return keyLength ? keyBuffer.slice(0, keyLength) : keyBuffer
|
||||
}
|
||||
|
||||
export default class FileTokenManager {
|
||||
static makeAuth(user: User) {
|
||||
return crypto.createHash("sha256").update(user.bean.id + user.getPassword() + config.salt + '_file').digest().toString('hex')
|
||||
}
|
||||
static encode(token: Token) {
|
||||
return crypto.createCipheriv("aes-256-gcm", normalizeKey(config.aes_key + '_file'), '01234567890123456').update(
|
||||
JSON.stringify(token)
|
||||
).toString('hex')
|
||||
}
|
||||
static decode(token: string) {
|
||||
if (token == null) throw new Error('令牌為空!')
|
||||
return JSON.parse(crypto.createDecipheriv("aes-256-gcm", normalizeKey(config.aes_key + '_file'), '01234567890123456').update(
|
||||
Buffer.from(token, 'hex')
|
||||
).toString()) as Token
|
||||
}
|
||||
|
||||
/**
|
||||
* 簽發文件令牌
|
||||
*/
|
||||
static make(user: User, device_id: string) {
|
||||
const time = Date.now()
|
||||
return this.encode({
|
||||
author: user.bean.id,
|
||||
auth: this.makeAuth(user),
|
||||
made_time: time,
|
||||
// 過期時間: 2分鐘
|
||||
expired_time: time + (1 * 1000 * 60 * 2),
|
||||
device_id: device_id
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 校驗文件令牌
|
||||
*/
|
||||
static check(user: User, token: string) {
|
||||
const tk = this.decode(token)
|
||||
|
||||
return (
|
||||
this.makeAuth(user) == tk.auth
|
||||
&& tk.expired_time < Date.now()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,13 @@ export default class TokenManager {
|
||||
).toString('hex')
|
||||
}
|
||||
static decode(token: string) {
|
||||
if (token == null) throw new Error('令牌為空!')
|
||||
try {
|
||||
return JSON.parse(crypto.createDecipheriv("aes-256-gcm", normalizeKey(config.aes_key), '01234567890123456').update(
|
||||
Buffer.from(token, 'hex')
|
||||
).toString()) as Token
|
||||
} catch(e) {
|
||||
return {} as Token
|
||||
}
|
||||
}
|
||||
|
||||
static make(user: User, time_: number | null | undefined, device_id: string) {
|
||||
@@ -51,4 +54,15 @@ export default class TokenManager {
|
||||
|
||||
return this.makeAuth(user) == tk.auth
|
||||
}
|
||||
/**
|
||||
* 嚴格檢驗令牌: 時間, 用戶, (設備 ID)
|
||||
*/
|
||||
static checkToken(token: Token, deviceId?: string) {
|
||||
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
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class UserApi extends BaseApi {
|
||||
msg: "登錄令牌失效",
|
||||
code: 401,
|
||||
}
|
||||
if (!User.findById(access_token.author)) return {
|
||||
if (!access_token.author || !User.findById(access_token.author)) return {
|
||||
msg: "賬號不存在",
|
||||
code: 401,
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export default class UserApi extends BaseApi {
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const user = (User.findByUserName(args.account as string) || User.findById(args.account as string)) as User
|
||||
const user = User.findByAccount(args.account as string) as User
|
||||
if (user == null) return {
|
||||
msg: "賬號或密碼錯誤",
|
||||
code: 400,
|
||||
@@ -226,7 +226,7 @@ export default class UserApi extends BaseApi {
|
||||
})
|
||||
// 添加聯絡人
|
||||
this.registerEvent("User.addContact", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'contact_chat_id'])) return {
|
||||
if (this.checkArgsMissing(args, ['token']) || (args.chat_id == null && args.account == null)) return {
|
||||
msg: "參數缺失",
|
||||
code: 400,
|
||||
}
|
||||
@@ -237,8 +237,20 @@ export default class UserApi extends BaseApi {
|
||||
msg: "令牌無效",
|
||||
}
|
||||
|
||||
const user = User.findById(token.author)
|
||||
user!.addContact(args.contact_chat_id as string)
|
||||
const user = User.findById(token.author) as User
|
||||
if (args.chat_id)
|
||||
user!.addContact(args.chat_id as string)
|
||||
else if (args.account) {
|
||||
const targetUser = User.findByAccount(args.account as string) as User
|
||||
if (targetUser == null) {
|
||||
return {
|
||||
msg: "找不到用戶",
|
||||
code: 404,
|
||||
}
|
||||
}
|
||||
const chat = ChatPrivate.findOrCreateForPrivate(user, targetUser)
|
||||
user!.addContact(chat.bean.id)
|
||||
}
|
||||
|
||||
return {
|
||||
msg: "成功",
|
||||
|
||||
@@ -88,13 +88,16 @@ export default class Chat {
|
||||
userIds.forEach((v) => UserChatLinker.unlinkUserAndChat(v, this.bean.id))
|
||||
}
|
||||
getAnotherUserForPrivate(userMySelf: User) {
|
||||
const user_a_id = this.getMembersList()[0]
|
||||
const user_b_id = this.getMembersList()[0]
|
||||
const members = this.getMembersList()
|
||||
const user_a_id = members[0]
|
||||
const user_b_id = members[1]
|
||||
if (members.length == 1 && user_a_id == userMySelf.bean.id)
|
||||
return userMySelf
|
||||
// 注意: 這裏已經確定了 Chat, 不需要再指定對方用戶
|
||||
if (user_a_id == userMySelf.bean.id)
|
||||
return User.findById(user_b_id as string)
|
||||
if (user_b_id == userMySelf.bean.id)
|
||||
return userMySelf
|
||||
return User.findById(user_a_id as string)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default class ChatPrivate extends Chat {
|
||||
}
|
||||
|
||||
static getChatIdByUsersId(userIdA: string, userIdB: string) {
|
||||
return [userIdA, userIdB].sort().join('-')
|
||||
return [userIdA, userIdB].sort().join('------')
|
||||
}
|
||||
|
||||
static createForPrivate(userA: User, userB: User) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { SQLInputValue } from "node:sqlite"
|
||||
import DataWrongError from "../api/DataWrongError.ts"
|
||||
import ChatPrivate from "./ChatPrivate.ts"
|
||||
import Chat from "./Chat.ts"
|
||||
import ChatBean from "./ChatBean.ts";
|
||||
import ChatBean from "./ChatBean.ts"
|
||||
|
||||
type UserBeanKey = keyof UserBean
|
||||
|
||||
@@ -103,6 +103,9 @@ export default class User {
|
||||
console.error(chalk.red(`警告: 查询 username = ${userName} 时, 查询到多个相同用户名的用户`))
|
||||
return new User(beans[0])
|
||||
}
|
||||
static findByAccount(account: string) {
|
||||
return User.findByUserName(account) || User.findById(account)
|
||||
}
|
||||
|
||||
declare bean: UserBean
|
||||
constructor(bean: UserBean) {
|
||||
@@ -121,6 +124,7 @@ export default class User {
|
||||
}
|
||||
addContact(chatId: string) {
|
||||
const ls = this.getContactsList()
|
||||
if (ls.indexOf(chatId) != -1 || ChatPrivate.getChatIdByUsersId(this.bean.id, this.bean.id) == chatId) return
|
||||
ls.push(chatId)
|
||||
this.setAttr("contacts_list", JSON.stringify(ls))
|
||||
}
|
||||
|
||||
@@ -11,21 +11,38 @@ import process from "node:process"
|
||||
import chalk from "chalk"
|
||||
import child_process from "node:child_process"
|
||||
import FileManager from "./data/FileManager.ts"
|
||||
import TokenManager from "./api/TokenManager.ts"
|
||||
import UserChatLinker from "./data/UserChatLinker.ts"
|
||||
import path from "node:path"
|
||||
import cookieParser from 'cookie-parser'
|
||||
|
||||
const app = express()
|
||||
app.use('/', express.static(config.data_path + '/page_compiled'))
|
||||
app.use(cookieParser())
|
||||
app.get('/uploaded_files/:hash', (req, res) => {
|
||||
const hash = req.params.hash as string
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
if (hash == null) {
|
||||
res.sendStatus(404)
|
||||
res.send("404 Not Found")
|
||||
res.send("404 Not Found", 404)
|
||||
return
|
||||
}
|
||||
const file = FileManager.findByHash(hash)
|
||||
|
||||
if (file.getChatId() != null) {
|
||||
const userToken = TokenManager.decode(req.cookies.token)
|
||||
console.log(userToken, req.cookies.device_id)
|
||||
if (!TokenManager.checkToken(userToken, req.cookies.device_id)) {
|
||||
res.send("401 UnAuthorized", 401)
|
||||
return
|
||||
}
|
||||
if (!UserChatLinker.checkUserIsLinkedToChat(userToken.author, file.getChatId())) {
|
||||
res.send("403 Forbidden", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
res.sendStatus(404)
|
||||
res.send("404 Not Found")
|
||||
res.send("404 Not Found", 404)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user