feat(wip): 新的客户端协议库

This commit is contained in:
CrescentLeaf
2025-11-08 16:17:58 +08:00
parent 230cc08182
commit 6ee209f9f6
17 changed files with 502 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
type ApiCallbackMessage = {
msg: string,
/**
* 200: 成功
* 400: 伺服器端無法理解客戶端請求
* 401: 需要身份驗證
* 403: 伺服器端拒絕執行客戶端請求
* 404: Not Found
* 500: 伺服器端錯誤
* 501: 伺服器端不支持請求的功能
*/
code: 200 | 400 | 401 | 403 | 404 | 500 | 501 | -1,
data?: { [key: string]: unknown },
}
export default ApiCallbackMessage

View File

@@ -0,0 +1,47 @@
export type CallMethod =
"User.auth" |
"User.register" |
"User.login" |
"User.refreshAccessToken" |
"User.setAvatar" |
"User.updateProfile" |
"User.getMyInfo" |
"User.resetPassword" |
"User.getInfo" |
"User.getMyContacts" |
"User.addContacts" |
"User.removeContacts" |
"User.getMyRecentChats" |
"Chat.getInfo" |
"Chat.updateSettings" |
"Chat.setAvatar" |
"Chat.createGroup" |
"Chat.getIdForPrivate" |
"Chat.getAnotherUserIdFromPrivate" |
"Chat.processJoinRequest" |
"Chat.sendJoinRequest" |
"Chat.getJoinRequests" |
"Chat.sendMessage" |
"Chat.getMessageHistory" |
"Chat.uploadFile"
export type ClientEvent =
"Client.onMessage"
export const CallableMethodBeforeAuth = [
"User.auth",
"User.register",
"User.login",
"User.refreshAccessToken",
]

View File

@@ -0,0 +1,8 @@
import LingChairClient from "./LingChairClient.ts"
export default class BaseClientObject {
declare client: LingChairClient
constructor(client: LingChairClient) {
this.client = client
}
}

View File

@@ -0,0 +1,7 @@
import ApiCallbackMessage from "./ApiCallbackMessage.ts"
export default class CallbackError extends Error {
constructor(re: ApiCallbackMessage) {
super(`[${re.code}] ${re.msg}${re.data ? ` (data: ${JSON.stringify(re.data)})` : ''}`)
}
}

View File

@@ -0,0 +1,114 @@
// deno-lint-ignore-file no-explicit-any
import { io, ManagerOptions, Socket, SocketOptions } from 'socket.io-client'
import crypto from 'node:crypto'
import { CallMethod, ClientEvent } from './ApiDeclare.ts'
import ApiCallbackMessage from './ApiCallbackMessage.ts'
import User from "./User.ts"
import UserMySelf from "./UserMySelf.ts"
export {
User,
UserMySelf,
}
export default class LingChairClient {
declare client: Socket
declare access_token: string
declare refresh_token?: string
constructor(args: {
server_url: string
device_id: string,
io?: Partial<ManagerOptions & SocketOptions>
}) {
this.client = io(args.server_url, {
transports: ["polling", "websocket", "webtransport"],
...args.io,
auth: {
...args.io?.auth,
device_id: args.device_id,
session_id: crypto.randomUUID(),
},
})
this.client.on("The_White_Silk", (name: string, data: unknown, _callback: (ret: unknown) => void) => {
try {
if (name == null || data == null) return
this.events[name]?.forEach((v) => v(data))
} catch (e) {
console.error(e)
}
})
}
connect() {
this.client.connect()
}
disconnect() {
this.client.disconnect()
}
reconnect() {
this.disconnect()
this.connect()
}
invoke(method: CallMethod, args: object = {}, timeout: number = 10000): Promise<ApiCallbackMessage> {
return new Promise((resolve) => {
this.client!.timeout(timeout).emit("The_White_Silk", method, args, (err: Error, res: ApiCallbackMessage) => {
// 错误处理
if (err) return resolve({
code: -1,
msg: err.message,
})
resolve(res)
})
})
}
events: { [key: string]: ((data: any) => void)[] } = {}
on(eventName: ClientEvent, func: (data: any) => void) {
if (this.events[eventName] == null)
this.events[eventName] = []
if (this.events[eventName].indexOf(func) == -1)
this.events[eventName].push(func)
}
off(eventName: ClientEvent, func: (data: any) => 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)
}
async auth(args: {
refresh_token?: string,
access_token?: string,
account?: string,
password?: string,
}) {
if ((!args.access_token && !args.refresh_token) && (!args.account && !args.password))
throw new Error('Access/Refresh token or account & password required')
this.refresh_token = args.refresh_token
let access_token = args.access_token
if (!access_token && args.refresh_token) {
const re = await this.invoke('User.refreshAccessToken', {
refresh_token: args.refresh_token,
})
if (re.code == 200) {
access_token = re.data!.access_token as string | undefined
} else return re
}
if (!access_token && (args.account && args.password)) {
const re = await this.invoke('User.login', {
account: args.account,
password: crypto.createHash('sha256').update(args.password).digest('hex'),
})
if (re.code == 200) {
access_token = re.data!.access_token as string | undefined
} else return re
}
const re = await this.invoke('User.auth', {
access_token: access_token
})
if (re.code == 200) this.access_token = access_token as string
return re
}
}

50
client-protocol/User.ts Normal file
View File

@@ -0,0 +1,50 @@
import BaseClientObject from "./BaseClientObject.ts"
import UserBean from "./bean/UserBean.ts"
import CallbackError from "./CallbackError.ts"
import LingChairClient from "./LingChairClient.ts"
export default class User extends BaseClientObject {
declare bean: UserBean
constructor(client: LingChairClient, bean: UserBean) {
super(client)
this.bean = bean
}
/*
* ================================================
* 实例化方法
* ================================================
*/
static async getById(client: LingChairClient, id: string) {
try {
return await this.getByIdOrThrow(client, id)
} catch(_) {
return null
}
}
static async getByIdOrThrow(client: LingChairClient, id: string) {
const re = await client.invoke("User.getInfo", {
token: client.access_token,
target: id,
})
if (re.code == 200)
return new User(client, re.data as unknown as UserBean)
throw new CallbackError(re)
}
/*
* ================================================
* 基本 Bean
* ================================================
*/
getId() {
return this.bean.id
}
getUserName() {
return this.bean.username
}
getNickName() {
return this.bean.nickname
}
getAvatarFileHash() {
return this.bean.avatar_file_hash
}
}

View File

@@ -0,0 +1,177 @@
import CallbackError from "./CallbackError.ts"
import LingChairClient from "./LingChairClient.ts"
import User from "./User.ts"
import ChatBean from "./bean/ChatBean.ts"
import RecentChatBean from "./bean/RecentChatBean.ts"
import UserBean from "./bean/UserBean.ts"
export default class UserMySelf extends User {
/*
* ================================================
* 实例化方法
* ================================================
*/
static async getMySelf(client: LingChairClient) {
try {
return await this.getMySelfOrThrow(client)
} catch (_) {
return null
}
}
static async getMySelfOrThrow(client: LingChairClient) {
const re = await client.invoke("User.getMyInfo", {
token: client.access_token,
})
if (re.code == 200)
return new UserMySelf(client, re.data as unknown as UserBean)
throw new CallbackError(re)
}
/*
* ================================================
* 账号相关
* ================================================
*/
async resetPassword(old_password: string, new_password: string) {
try {
await this.resetPasswordOrThrow(old_password, new_password)
return true
} catch (_) {
return false
}
}
async resetPasswordOrThrow(old_password: string, new_password: string) {
const re = await this.client.invoke("User.resetPassword", {
token: this.client.access_token,
old_password,
new_password,
})
if (re.code != 200) throw new CallbackError(re)
}
/*
* ================================================
* 个人资料
* ================================================
*/
async setAvatarFileHash(file_hash: string) {
try {
await this.setAvatarFileHashOrThrow(file_hash)
return true
} catch (_) {
return false
}
}
async setAvatarFileHashOrThrow(file_hash: string) {
const re = await this.client.invoke("User.setAvatar", {
token: this.client.access_token,
file_hash,
})
if (re.code != 200) throw new CallbackError(re)
}
async setUserName(user_name: string) {
return await this.updateProfile({ username: user_name })
}
async setUserNameOrThrow(user_name: string) {
await this.updateProfileOrThrow({ username: user_name })
}
async setNickName(nick_name: string) {
return await this.updateProfile({ nickname: nick_name })
}
async setNickNameOrThrow(nick_name: string) {
await this.updateProfileOrThrow({ nickname: nick_name })
}
async updateProfile(args: {
username?: string,
nickname?: string
}) {
try {
await this.updateProfileOrThrow(args)
return true
} catch (_) {
return false
}
}
async updateProfileOrThrow({
username,
nickname
}: {
username?: string,
nickname?: string
}) {
const re = await this.client.invoke("User.updateProfile", {
token: this.client.access_token,
nickname,
username,
})
if (re.code != 200) throw new CallbackError(re)
}
/*
* ================================================
* 收藏对话
* ================================================
*/
async addFavouriteChats(chat_ids: string[]) {
try {
await this.addFavouriteChatsOrThrow(chat_ids)
return true
} catch (_) {
return false
}
}
async addFavouriteChatsOrThrow(chat_ids: string[]) {
const re = await this.client.invoke("User.addContacts", {
token: this.client.access_token,
targets: chat_ids,
})
if (re.code != 200) throw new CallbackError(re)
}
async removeFavouriteChats(chat_ids: string[]) {
try {
await this.removeFavouriteChatsOrThrow(chat_ids)
return true
} catch (_) {
return false
}
}
async removeFavouriteChatsOrThrow(chat_ids: string[]) {
const re = await this.client.invoke("User.removeContacts", {
token: this.client.access_token,
targets: chat_ids,
})
if (re.code != 200) throw new CallbackError(re)
}
async getMyFavouriteChatBeans() {
try {
return await this.getMyFavouriteChatBeansOrThrow()
} catch (_) {
return null
}
}
async getMyFavouriteChatBeansOrThrow() {
const re = await this.client.invoke("User.getMyContacts", {
token: this.client.access_token
})
if (re.code == 200)
return re.data!.recent_chats as ChatBean[]
throw new CallbackError(re)
}
/*
* ================================================
* 最近对话
* ================================================
*/
async getMyRecentChatBeans() {
try {
return await this.getMyRecentChatBeansOrThrow()
} catch (_) {
return null
}
}
async getMyRecentChatBeansOrThrow() {
const re = await this.client.invoke("User.getMyRecentChats", {
token: this.client.access_token
})
if (re.code == 200)
return re.data!.recent_chats as RecentChatBean[]
throw new CallbackError(re)
}
}

View File

@@ -0,0 +1,14 @@
import ChatType from "./ChatType.ts"
export default class ChatBean {
declare type: ChatType
declare id: string
declare title: string
declare avatar_file_hash?: string
declare settings?: { [key: string]: unknown }
declare is_member: boolean
declare is_admin: boolean
[key: string]: unknown
}

View File

@@ -0,0 +1,3 @@
type ChatType = 'private' | 'group'
export default ChatType

View File

@@ -0,0 +1,14 @@
interface GroupSettingsBean {
allow_new_member_join?: boolean
allow_new_member_from_invitation?: boolean
new_member_join_method?: 'disabled' | 'allowed_by_admin' | 'answered_and_allowed_by_admin'
answered_and_allowed_by_admin_question?: string
// 下面两个比较特殊, 由服务端给予
group_title: string
group_name: string
[key: string]: unknown
}
export default GroupSettingsBean

View File

@@ -0,0 +1,8 @@
export default class JoinRequestBean {
declare user_id: string
declare title: string
declare avatar?: string
declare reason?: string
[key: string]: unknown
}

View File

@@ -0,0 +1,6 @@
export default class MessageBean {
declare id: number
declare text: string
declare user_id: string
declare time: string
}

View File

@@ -0,0 +1,5 @@
import ChatBean from "./ChatBean.ts"
export default class RecentChat extends ChatBean {
declare content: string
}

View File

@@ -0,0 +1,6 @@
export default class UserBean {
declare id: string
declare username?: string
declare nickname: string
declare avatar_file_hash?: string
}

View File

@@ -0,0 +1,5 @@
{
"imports": {
"socket.io-client": "npm:socket.io-client@4.8.1"
}
}

15
client-protocol/test.ts Normal file
View File

@@ -0,0 +1,15 @@
import LingChairClient, { User, UserMySelf } from "./LingChairClient.ts"
import OnMessageData from "./type/OnMessageData.ts"
const client = new LingChairClient({
server_url: 'ws://localhost:3601',
device_id: 'test01'
})
await client.auth({
account: '满月',
password: '满月',
})
client.on('Client.onMessage', (data: OnMessageData) => {
console.log(data)
})
console.log(await (await UserMySelf.getMySelf(client))?.getMyRecentChatBeans())

View File

@@ -0,0 +1,8 @@
import MessageBean from '../bean/MessageBean.ts'
interface OnMessageData {
chat: string
msg: MessageBean
}
export default OnMessageData