Compare commits

..

11 Commits

Author SHA1 Message Date
CrescentLeaf
791102c034 fix: MessageManager 建表失敗 2025-09-21 16:13:48 +08:00
CrescentLeaf
8bcb3e74b6 feat: 服務端可以獲取每個客戶端的連接 2025-09-21 16:13:31 +08:00
CrescentLeaf
e4c26a07cf feat: 緩存資料, 獲取任意用戶的資料 2025-09-21 16:13:01 +08:00
CrescentLeaf
f118c6b6f5 chore: 修繕客戶端請求允許等待連接 2025-09-21 16:12:31 +08:00
CrescentLeaf
cb947429fb feat: 收發消息 2025-09-21 16:11:58 +08:00
CrescentLeaf
b3d620a329 refactor: 重寫消息顯示相關邏輯 2025-09-21 16:11:13 +08:00
CrescentLeaf
28ffd134df feat: 服務端 Api 可以持有 client socket 2025-09-21 14:12:06 +08:00
CrescentLeaf
f600245d3b feat: BaseApi 有條件獲取更多的數據 2025-09-21 14:06:36 +08:00
CrescentLeaf
706d811087 feat(wip): 事件緩存以備離綫重連重發 2025-09-21 14:06:08 +08:00
CrescentLeaf
e5dd3ade51 feat: 檢驗用戶的 設備 ID 2025-09-21 12:28:44 +08:00
CrescentLeaf
83719f5f44 fix(ui): 兩個列表沒有吃滿寬度 2025-09-21 11:06:21 +08:00
21 changed files with 375 additions and 93 deletions

View File

@@ -21,9 +21,10 @@ const _data_cached = JSON.parse(_dec)
declare global {
interface Window {
data: {
split_sizes: number[];
split_sizes: number[]
apply(): void
access_token?: string
device_id: string
}
}
}

View File

@@ -8,8 +8,9 @@ type ApiCallbackMessage = {
* 404: Not Found
* 500: 伺服器端錯誤
* 501: 伺服器端不支持請求的功能
* -1: 客戶端錯誤
*/
code: 200 | 400 | 401 | 403 | 404 | 500 | 501,
data?: { [key: string]: unknown },
code: 200 | 400 | 401 | 403 | 404 | 500 | 501 | -1
data?: { [key: string]: unknown }
}
export default ApiCallbackMessage

View File

@@ -7,6 +7,8 @@ export type CallMethod =
"User.updateProfile" |
"User.getMyInfo" |
"User.getInfo" |
"User.getMyContacts" |
"User.addContact" |
"User.removeContacts" |

View File

@@ -2,19 +2,24 @@ import { io, Socket } from 'socket.io-client'
import { CallMethod, ClientEvent } from './ApiDeclare.ts'
import ApiCallbackMessage from './ApiCallbackMessage.ts'
import User from "./client_data/User.ts"
import data from "../Data.ts";
import data from "../Data.ts"
type UnknownObject = { [key: string]: unknown }
class Client {
static myUserProfile?: User
static socket?: Socket
static events: { [key: string]: (data: UnknownObject) => UnknownObject } = {}
static events: { [key: string]: (data: UnknownObject) => UnknownObject | void } = {}
static connect() {
if (data.device_id == null)
data.device_id = crypto.randomUUID()
this.socket?.disconnect()
this.socket && delete this.socket
this.socket = io({
transports: ['websocket']
transports: ['websocket'],
auth: {
device_id: data.device_id
},
})
this.socket!.on("The_White_Silk", (name: string, data: UnknownObject, callback: (ret: UnknownObject) => void) => {
try {
@@ -32,9 +37,12 @@ class Client {
setTimeout(async () => reslove(await this.invoke(method, args, timeout)), 500)
})
}
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
this.socket!.timeout(timeout).emit("The_White_Silk", method, args, (err: string, res: ApiCallbackMessage) => {
if (err) return reject(err)
if (err) return resolve({
code: -1,
msg: err,
})
resolve(res)
})
})
@@ -52,7 +60,7 @@ class Client {
token: data.access_token
})).data as unknown as User
}
static on(eventName: ClientEvent, func: (data: UnknownObject) => UnknownObject) {
static on(eventName: ClientEvent, func: (data: UnknownObject) => UnknownObject | void) {
this.events[eventName] = func
}
static off(eventName: ClientEvent) {

19
client/api/DataCaches.ts Normal file
View File

@@ -0,0 +1,19 @@
import data from "../Data.ts"
import Client from "./Client.ts"
import User from "./client_data/User.ts"
export default class DataCaches {
static userProfiles: { [key: string]: User} = {}
static async getUserProfile(userId: string): Promise<User> {
if (this.userProfiles[userId]) return this.userProfiles[userId]
const re = await Client.invoke("User.getInfo", {
token: data.access_token,
target: userId
})
if (re.code != 200) return {
id: '',
nickname: "",
}
return this.userProfiles[userId] = (re.data as unknown as User)
}
}

View File

@@ -1,14 +1,14 @@
import { Tab } from "mdui"
import { Tab, TextField } from "mdui"
import useEventListener from "../useEventListener.ts"
import Element_Message from "./Message.jsx"
import MessageContainer from "./MessageContainer.jsx"
import Element_Message from "./Message.tsx"
import MessageContainer from "./MessageContainer.tsx"
import * as React from 'react'
import Client from "../../api/Client.ts"
import Message from "../../api/client_data/Message.ts"
import Chat from "../../api/client_data/Chat.ts"
import data from "../../Data.ts"
import { checkApiSuccessOrSncakbar } from "../snackbar.ts"
import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts"
import useAsyncEffect from "../useAsyncEffect.ts"
interface Args extends React.HTMLAttributes<HTMLElement> {
@@ -23,6 +23,7 @@ export default function ChatFragment({ target, ...props }: Args) {
const [tabItemSelected, setTabItemSelected] = React.useState('Chat')
const tabRef = React.useRef<Tab>(null)
const chatPanelRef = React.useRef<HTMLElement>(null)
useEventListener(tabRef, 'change', () => {
setTabItemSelected(tabRef.current?.value || "Chat")
})
@@ -35,28 +36,71 @@ export default function ChatFragment({ target, ...props }: Args) {
if (re.code != 200)
return checkApiSuccessOrSncakbar(re, "對話錯誤")
setChatInfo(re.data as Chat)
loadMore()
}, [target])
let page = 0
const page = React.useRef(0)
async function loadMore() {
const re = await Client.invoke("Chat.getMessageHistory", {
token: data.access_token,
target,
page,
page: page.current,
})
if (checkApiSuccessOrSncakbar(re, "拉取歷史記錄失敗")) return
page++
setMessagesList(messagesList.concat())
const returnMsgs = (re.data!.messages as Message[]).reverse()
if (returnMsgs.length == 0)
return snackbar({
message: "已經沒有消息了哦~",
placement: 'top',
})
setMessagesList(returnMsgs.concat(messagesList))
if (page.current == 0 + 1)
setTimeout(() => chatPanelRef.current!.scrollTo({
top: 10000000000,
behavior: "smooth",
}), 100)
page.current++
}
React.useEffect(() => {
interface OnMessageData {
chat: string
msg: Message
}
Client.on('Client.onMessage', (data: unknown) => {
const { chat, msg } = (data as OnMessageData)
if (target == chat) {
setMessagesList(messagesList.concat([msg]))
}
})
return () => {
Client.off('Client.onMessage')
}
})
const inputRef = React.useRef<TextField>(null)
async function sendMessage() {
const text = inputRef.current!.value
const re = await Client.invoke("Chat.sendMessage", {
token: data.access_token,
target,
text,
}, 5000)
if (checkApiSuccessOrSncakbar(re, "發送失敗")) return
inputRef.current!.value = ''
chatPanelRef.current!.scrollTo({
top: 10000000000,
behavior: "smooth",
})
}
return (
<div style={{
width: '100%',
@@ -76,7 +120,7 @@ export default function ChatFragment({ target, ...props }: Args) {
}</mdui-tab>
<mdui-tab value="Settings"></mdui-tab>
<mdui-tab-panel slot="panel" value="Chat" style={{
<mdui-tab-panel slot="panel" value="Chat" ref={chatPanelRef} style={{
display: tabItemSelected == "Chat" ? "flex" : "none",
flexDirection: "column",
height: "100%",
@@ -84,11 +128,24 @@ export default function ChatFragment({ target, ...props }: Args) {
<div style={{
display: "flex",
justifyContent: "center",
marginTop: "15px"
paddingTop: "15px",
}}>
<mdui-button variant="text"></mdui-button>
<mdui-button variant="text" onClick={() => loadMore()}></mdui-button>
</div>
<MessageContainer>
<MessageContainer style={{
paddingTop: "15px",
flexGrow: '1',
}}>
{
messagesList.map((msg) =>
<Element_Message
key={msg.id}
userId={msg.user_id}>
{msg.text}
</Element_Message>
)
}
</MessageContainer>
{
// 输入框
@@ -96,16 +153,19 @@ export default function ChatFragment({ target, ...props }: Args) {
<div style={{
display: 'flex',
alignItems: 'center',
paddingBottom: '0.1rem',
paddingBottom: '2px',
paddingTop: '0.1rem',
height: '4rem',
position: 'sticky',
bottom: '2px',
bottom: '0',
marginLeft: '5px',
marginRight: '4px',
backgroundColor: 'rgb(var(--mdui-color-background))',
}}>
<mdui-text-field variant="outlined" placeholder="喵呜~" autosize max-rows={1} style={{
<mdui-text-field variant="outlined" placeholder="喵呜~" autosize ref={inputRef as any} max-rows={1} onKeyDown={(event) => {
if (event.ctrlKey && event.key == 'Enter')
sendMessage()
}} style={{
marginRight: '10px',
}}></mdui-text-field>
<mdui-button-icon slot="end-icon" icon="more_vert" style={{
@@ -113,7 +173,7 @@ export default function ChatFragment({ target, ...props }: Args) {
}}></mdui-button-icon>
<mdui-button-icon icon="send" style={{
marginRight: '7px',
}}></mdui-button-icon>
}} onClick={() => sendMessage()}></mdui-button-icon>
</div>
</mdui-tab-panel>
<mdui-tab-panel slot="panel" value="Settings" style={{

View File

@@ -1,15 +1,25 @@
import Client from "../../api/Client.ts"
import DataCaches from "../../api/DataCaches.ts"
import Avatar from "../Avatar.tsx"
import useAsyncEffect from "../useAsyncEffect.ts"
import React from "react"
interface Args extends React.HTMLAttributes<HTMLElement> {
userId: string
}
export default function Message({ userId, children, ...props }: Args) {
const isAtRight = Client.myUserProfile?.id == userId
const [ nickName, setNickName ] = React.useState("")
const [ avatarUrl, setAvatarUrl ] = React.useState<string | undefined>("")
useAsyncEffect(async () => {
const user = await DataCaches.getUserProfile(userId)
setNickName(user.nickname)
setAvatarUrl(user?.avatar)
}, [userId])
/**
*
* @param { Object } param
* @param { "left" | "right" } [param.direction="left"]
* @param { String } [param.avatar]
* @param { String } [param.nickName]
* @returns { React.JSX.Element }
*/
export default function Message({ direction = 'left', avatar, nickName, children, ...props } = {}) {
let isAtRight = direction == 'right'
return (
<div
slot="trigger"
@@ -39,7 +49,7 @@ export default function Message({ direction = 'left', avatar, nickName, children
// 发送者头像
}
<Avatar
src={avatar}
src={avatarUrl}
text={nickName}
style={{
width: "43px",

View File

@@ -1,8 +1,6 @@
/**
*
* @returns { React.JSX.Element }
*/
export default function MessageContainer({ children, style, ...props } = {}) {
interface Args extends React.HTMLAttributes<HTMLElement> {}
export default function MessageContainer({ children, style, ...props }: Args) {
return (
<div style={{
display: 'flex',
@@ -10,7 +8,6 @@ export default function MessageContainer({ children, style, ...props } = {}) {
justifyContent: 'flex-end',
alignItems: 'center',
marginBottom: '20px',
height: "100%",
...style,
}}
{...props}>

View File

@@ -45,6 +45,7 @@ export default function ContactsList({
paddingRight: '10px',
display: display ? undefined : 'none',
height: '100%',
width: '100%',
}} {...props}>
<mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{
marginTop: '5px',

View File

@@ -32,6 +32,7 @@ export default function RecentsList({
paddingRight: '10px',
display: display ? undefined : 'none',
height: '100%',
width: '100%',
}} {...props}>
<mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{
marginTop: '5px',

View File

@@ -7,6 +7,8 @@ export type CallMethod =
"User.updateProfile" |
"User.getMyInfo" |
"User.getInfo" |
"User.getMyContacts" |
"User.addContact" |
"User.removeContacts" |

View File

@@ -32,12 +32,43 @@ export default class ApiManager {
static addEventListener(name: string, func: EventCallbackFunction) {
this.event_listeners[name] = func
}
static clients: { [key: string]: { [key: string]: SocketIo.Socket<SocketIo.DefaultEventsMap, SocketIo.DefaultEventsMap, SocketIo.DefaultEventsMap, any> } } = {}
static checkUserIsOnline(userId: string) {
return this.getUserClientSockets(userId) != null
}
static getUserClientSockets(userId: string) {
return this.clients[userId]
}
static initEvents() {
const io = this.socketIoServer
io.on('connection', (socket) => {
// TODO: fix ip == undefined
// https://github.com/denoland/deno/blob/7938d5d2a448b876479287de61e9e3b8c6109bc8/ext/node/polyfills/net.ts#L1713
const ip = socket.conn.remoteAddress
const deviceId = socket.handshake.auth.device_id as string
const clientInfo = {
userId: '',
deviceId,
ip,
socket,
}
socket.on('disconnect', (_reason) => {
if (clientInfo.userId == '')
console.log(chalk.yellow('[斷]') + ` ${ip} disconnected`)
else {
console.log(chalk.green('[斷]') + ` ${ip} disconnected`)
delete this.clients[clientInfo.userId][deviceId]
}
})
console.log(chalk.yellow('[連]') + ` ${ip} connected`)
socket.on("The_White_Silk", (name: string, args: { [key: string]: unknown }, callback_: (ret: ApiCallbackMessage) => void) => {
function callback(ret: ApiCallbackMessage) {
console.log(chalk.blue('[發]') + ` ${socket.request.socket.remoteAddress} <- ${ret.code == 200 ? chalk.green(ret.msg) : chalk.red(ret.msg)} [${ret.code}]${ ret.data ? (' <extras: ' + JSON.stringify(ret.data) + '>') : ''}`)
console.log(chalk.blue('[發]') + ` ${ip} <- ${ret.code == 200 ? chalk.green(ret.msg) : chalk.red(ret.msg)} [${ret.code}]${ret.data ? (' <extras: ' + JSON.stringify(ret.data) + '>') : ''}`)
return callback_(ret)
}
try {
@@ -45,9 +76,9 @@ export default class ApiManager {
msg: "Invalid request.",
code: 400
})
console.log(chalk.red('[收]') + ` ${socket.request.socket.remoteAddress} -> ${chalk.yellow(name)} <args: ${JSON.stringify(args)}>`)
console.log(chalk.red('[收]') + ` ${ip} -> ${chalk.yellow(name)} <args: ${JSON.stringify(args)}>`)
return callback(this.event_listeners[name]?.(args) || {
return callback(this.event_listeners[name]?.(args, clientInfo) || {
code: 501,
msg: "Not implmented",
})
@@ -59,7 +90,7 @@ export default class ApiManager {
code: err instanceof DataWrongError ? 400 : 500,
msg: "錯誤: " + err.message
})
} catch(_e) {}
} catch (_e) { }
}
})
})

View File

@@ -1,8 +1,9 @@
import EventCallbackFunction from "../typedef/EventCallbackFunction.ts"
import ApiManager from "./ApiManager.ts"
import { CallMethod } from './ApiDeclare.ts'
import { CallMethod, ClientEvent } from './ApiDeclare.ts'
import User from "../data/User.ts"
import Token from "./Token.ts"
import * as SocketIo from "socket.io"
export default abstract class BaseApi {
abstract getName(): string
@@ -22,18 +23,19 @@ export default abstract class BaseApi {
return true
return false
}
checkUserToken(user: User, token: Token) {
if (!this.checkToken(token)) return false
if (token.author != user.bean.id) return false
return true
}
checkToken(token: Token) {
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
}
registerEvent(name: CallMethod, func: EventCallbackFunction) {
if (!name.startsWith(this.getName() + ".")) throw Error("注冊的事件應該與接口集合命名空間相匹配: " + name)
ApiManager.addEventListener(name, func)
}
emitToClient(client: SocketIo.Socket, name: ClientEvent, args: { [key: string]: unknown }) {
client.emit("The_White_Silk", name, args)
}
}

View File

@@ -2,6 +2,7 @@ import Chat from "../data/Chat.ts";
import ChatPrivate from "../data/ChatPrivate.ts";
import MessagesManager from "../data/MessagesManager.ts";
import User from "../data/User.ts"
import ApiManager from "./ApiManager.ts";
import BaseApi from "./BaseApi.ts"
import TokenManager from "./TokenManager.ts"
@@ -15,14 +16,14 @@ export default class ChatApi extends BaseApi {
* @param token 令牌
* @param target 目標對話
*/
this.registerEvent("Chat.getInfo", (args) => {
this.registerEvent("Chat.getInfo", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}
@@ -57,23 +58,56 @@ export default class ChatApi extends BaseApi {
* 發送訊息
* @param token 令牌
* @param target 目標對話
* @param
* @param text 消息内容
*/
this.registerEvent("Chat.sendMessage", (args) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return {
this.registerEvent("Chat.sendMessage", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target', 'text'])) return {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}
const chat = Chat.findById(args.target as string)
if (chat == null) return {
code: 404,
msg: "對話不存在",
}
const msg = {
text: args.text as string,
user_id: token.author,
}
const id = MessagesManager.getInstanceForChat(chat).addMessage(msg)
const users: string[] = []
if (chat.bean.type == 'private') {
users.push(token.author as string)
}
for (const user of users) {
if (ApiManager.checkUserIsOnline(user)) {
const sockets = ApiManager.getUserClientSockets(user)
for (const socket of Object.keys(sockets))
this.emitToClient(sockets[socket], 'Client.onMessage', {
chat: chat.bean.id,
msg: {
...msg,
id
}
})
} else {
// TODO: EventStore
}
}
return {
code: 501,
msg: "未實現",
code: 200,
msg: "成功",
}
})
/**
@@ -82,14 +116,14 @@ export default class ChatApi extends BaseApi {
* @param target 目標對話
* @param page 頁面
*/
this.registerEvent("Chat.getMessageHistory", (args) => {
this.registerEvent("Chat.getMessageHistory", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target', 'page'])) return {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}
@@ -104,7 +138,7 @@ export default class ChatApi extends BaseApi {
code: 200,
msg: "成功",
data: {
messages: MessagesManager.getInstanceForChat(chat).getMessagesWithPage(),
messages: MessagesManager.getInstanceForChat(chat).getMessagesWithPage(15, args.page as number),
},
}
})

4
server/api/EventBean.ts Normal file
View File

@@ -0,0 +1,4 @@
export default class EventBean {
declare event_name: string
declare data: unknown
}

53
server/api/EventStorer.ts Normal file
View File

@@ -0,0 +1,53 @@
import { DatabaseSync } from "node:sqlite"
import path from 'node:path'
import config from "../config.ts"
import User from "../data/User.ts";
import EventBean from "./EventBean.ts";
export default class EventStorer {
static database: DatabaseSync = this.init()
private static init(): DatabaseSync {
const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, 'Events.db'))
return db
}
static getInstanceForUser(user: User) {
return new EventStorer(user)
}
declare user: User
constructor(user: User) {
this.user = user
EventStorer.database.exec(`
CREATE TABLE IF NOT EXISTS ${this.getTableName()} (
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
/* 事件 */ event_name TEXT NOT NULL,
/* 數據 */ data TEXT NOT NULL,
);
`)
}
protected getTableName() {
return `events_${this.user.bean.id}`
}
addEvent(eventName: string, data: unknown) {
EventStorer.database.prepare(`INSERT INTO ${this.getTableName()} (
event_name,
data
) VALUES (?, ?);`).run(
eventName,
JSON.stringify(data)
)
}
getEvents() {
return EventStorer.database.prepare(`SELECT * FROM ${this.getTableName()};`).all().map((v: any) => ({
...v,
data: JSON.parse(v.data)
})) as unknown as EventBean[]
}
clearEvents() {
EventStorer.database.prepare(`DELETE FROM ${this.getTableName()};`).run()
}
}

View File

@@ -3,4 +3,5 @@ export default interface Token {
auth: string
made_time: number
expired_time: number
device_id: string
}

View File

@@ -22,22 +22,29 @@ export default class TokenManager {
).toString('hex')
}
static decode(token: string) {
if (token == null) throw new Error('令牌為空!')
return JSON.parse(crypto.createDecipheriv("aes-256-gcm", normalizeKey(config.aes_key), '01234567890123456').update(
Buffer.from(token, 'hex')
).toString()) as Token
}
static make(user: User, time: number = Date.now()) {
static make(user: User, time_: number | null | undefined, device_id: string) {
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
})
}
/**
* 獲取新令牌
* 注意: 只驗證用戶, 不驗證令牌有效性!
*/
static makeNewer(user: User, token: string) {
if (this.check(user, token))
return this.make(user, Date.now() + (1 * 1000 * 60 * 60 * 24))
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)

View File

@@ -1,9 +1,11 @@
import { Buffer } from "node:buffer";
import User from "../data/User.ts";
import { Buffer } from "node:buffer"
import User from "../data/User.ts"
import BaseApi from "./BaseApi.ts"
import TokenManager from "./TokenManager.ts";
import ChatPrivate from "../data/ChatPrivate.ts";
import Chat from "../data/Chat.ts";
import TokenManager from "./TokenManager.ts"
import ChatPrivate from "../data/ChatPrivate.ts"
import Chat from "../data/Chat.ts"
import chalk from "chalk"
import ApiManager from "./ApiManager.ts"
export default class UserApi extends BaseApi {
override getName(): string {
@@ -11,11 +13,12 @@ export default class UserApi extends BaseApi {
}
override onInit(): void {
// 驗證
this.registerEvent("User.auth", (args) => {
this.registerEvent("User.auth", (args, clientInfo) => {
if (this.checkArgsMissing(args, ['access_token'])) return {
msg: "參數缺失",
code: 400,
}
const { deviceId, ip, socket } = clientInfo
try {
const access_token = TokenManager.decode(args.access_token as string)
@@ -23,11 +26,21 @@ export default class UserApi extends BaseApi {
msg: "登錄令牌失效",
code: 401,
}
if (!User.findById(access_token.author)) return {
msg: "賬號不存在",
code: 401,
}
if (access_token.device_id != deviceId) return {
msg: "驗證失敗",
code: 401,
}
clientInfo.userId = access_token.author
console.log(chalk.green('[驗]') + ` ${access_token.author} authed on Client ${deviceId} (ip = ${ip})`)
if (ApiManager.clients[clientInfo.userId] == null) ApiManager.clients[clientInfo.userId] = {
[deviceId]: socket
}
else ApiManager.clients[clientInfo.userId][deviceId] = socket
return {
msg: "成功",
@@ -45,7 +58,7 @@ export default class UserApi extends BaseApi {
}
})
// 登錄
this.registerEvent("User.login", (args) => {
this.registerEvent("User.login", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['account', 'password'])) return {
msg: "參數缺失",
code: 400,
@@ -65,7 +78,7 @@ export default class UserApi extends BaseApi {
msg: "成功",
code: 200,
data: {
access_token: TokenManager.make(user)
access_token: TokenManager.make(user, null, deviceId)
},
}
@@ -75,7 +88,7 @@ export default class UserApi extends BaseApi {
}
})
// 注冊
this.registerEvent("User.register", (args) => {
this.registerEvent("User.register", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['nickname', 'password'])) return {
msg: "參數缺失",
code: 400,
@@ -105,7 +118,7 @@ export default class UserApi extends BaseApi {
* ================================================
*/
// 更新頭像
this.registerEvent("User.setAvatar", (args) => {
this.registerEvent("User.setAvatar", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['avatar', 'token'])) return {
msg: "參數缺失",
code: 400,
@@ -115,7 +128,7 @@ export default class UserApi extends BaseApi {
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}
@@ -130,14 +143,14 @@ export default class UserApi extends BaseApi {
}
})
// 更新資料
this.registerEvent("User.updateProfile", (args) => {
this.registerEvent("User.updateProfile", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token'])) return {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}
@@ -154,14 +167,14 @@ export default class UserApi extends BaseApi {
}
})
// 獲取用戶信息
this.registerEvent("User.getMyInfo", (args) => {
this.registerEvent("User.getMyInfo", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token'])) return {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}
@@ -180,14 +193,14 @@ export default class UserApi extends BaseApi {
}
})
// 獲取聯絡人列表
this.registerEvent("User.getMyContacts", (args) => {
this.registerEvent("User.getMyContacts", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token'])) return {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}
@@ -212,14 +225,14 @@ export default class UserApi extends BaseApi {
}
})
// 添加聯絡人
this.registerEvent("User.addContact", (args) => {
this.registerEvent("User.addContact", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'contact_chat_id'])) return {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}
@@ -237,6 +250,35 @@ export default class UserApi extends BaseApi {
* 公開資料
* ================================================
*/
// 獲取用戶信息
this.registerEvent("User.getInfo", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}
const user = User.findById(args.target as string)
if (user == null) return {
code: 404,
msg: "用戶不存在",
}
return {
msg: "成功",
code: 200,
data: {
username: user!.getUserName(),
nickname: user!.getNickName(),
avatar: user!.getAvatarFileHash() ? "uploaded_files/" + user!.getAvatarFileHash() : null,
id: token.author,
}
}
})
}
}

View File

@@ -27,12 +27,12 @@ export default class MessagesManager {
CREATE TABLE IF NOT EXISTS ${this.getTableName()} (
/* 序号, MessageId */ id INTEGER PRIMARY KEY AUTOINCREMENT,
/* 消息文本 */ text TEXT NOT NULL,
/* 发送者 */ user_id TEXT NOT NULL,
/* 发送者 */ user_id TEXT NOT NULL
);
`)
}
protected getTableName() {
return `messages_${this.chat.bean.id}`
return `messages_${this.chat.bean.id}`.replaceAll('-', '_')
}
addMessage({
text,
@@ -41,13 +41,13 @@ export default class MessagesManager {
text: string,
user_id?: string
}) {
MessagesManager.database.prepare(`INSERT INTO ${this.getTableName()} (
return MessagesManager.database.prepare(`INSERT INTO ${this.getTableName()} (
text,
user_id
) VALUES (?, ?);`).run(
text,
user_id || null
)
).lastInsertRowid
}
addSystemMessage(text: string) {
this.addMessage({

View File

@@ -1,5 +1,11 @@
import ApiCallbackMessage from "../api/ApiCallbackMessage.ts"
import * as SocketIo from "socket.io"
type EventCallbackFunction = (args: { [key: string]: unknown }) => ApiCallbackMessage
type EventCallbackFunction = (args: { [key: string]: unknown }, clientInfo: {
userId: string
deviceId: string
ip: string
socket: SocketIo.Socket<SocketIo.DefaultEventsMap, SocketIo.DefaultEventsMap, SocketIo.DefaultEventsMap, any>
}) => ApiCallbackMessage
export default EventCallbackFunction