feat(wip): 新的客户端协议库
This commit is contained in:
15
client-protocol/ApiCallbackMessage.ts
Normal file
15
client-protocol/ApiCallbackMessage.ts
Normal 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
|
||||
47
client-protocol/ApiDeclare.ts
Normal file
47
client-protocol/ApiDeclare.ts
Normal 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",
|
||||
]
|
||||
8
client-protocol/BaseClientObject.ts
Normal file
8
client-protocol/BaseClientObject.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import LingChairClient from "./LingChairClient.ts"
|
||||
|
||||
export default class BaseClientObject {
|
||||
declare client: LingChairClient
|
||||
constructor(client: LingChairClient) {
|
||||
this.client = client
|
||||
}
|
||||
}
|
||||
7
client-protocol/CallbackError.ts
Normal file
7
client-protocol/CallbackError.ts
Normal 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)})` : ''}`)
|
||||
}
|
||||
}
|
||||
114
client-protocol/LingChairClient.ts
Normal file
114
client-protocol/LingChairClient.ts
Normal 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
50
client-protocol/User.ts
Normal 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
|
||||
}
|
||||
}
|
||||
177
client-protocol/UserMySelf.ts
Normal file
177
client-protocol/UserMySelf.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
14
client-protocol/bean/ChatBean.ts
Normal file
14
client-protocol/bean/ChatBean.ts
Normal 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
|
||||
}
|
||||
3
client-protocol/bean/ChatType.ts
Normal file
3
client-protocol/bean/ChatType.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
type ChatType = 'private' | 'group'
|
||||
|
||||
export default ChatType
|
||||
14
client-protocol/bean/GroupSettingsBean.ts
Normal file
14
client-protocol/bean/GroupSettingsBean.ts
Normal 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
|
||||
8
client-protocol/bean/JoinRequestBean.ts
Normal file
8
client-protocol/bean/JoinRequestBean.ts
Normal 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
|
||||
}
|
||||
6
client-protocol/bean/MessageBean.ts
Normal file
6
client-protocol/bean/MessageBean.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default class MessageBean {
|
||||
declare id: number
|
||||
declare text: string
|
||||
declare user_id: string
|
||||
declare time: string
|
||||
}
|
||||
5
client-protocol/bean/RecentChatBean.ts
Normal file
5
client-protocol/bean/RecentChatBean.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import ChatBean from "./ChatBean.ts"
|
||||
|
||||
export default class RecentChat extends ChatBean {
|
||||
declare content: string
|
||||
}
|
||||
6
client-protocol/bean/UserBean.ts
Normal file
6
client-protocol/bean/UserBean.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default class UserBean {
|
||||
declare id: string
|
||||
declare username?: string
|
||||
declare nickname: string
|
||||
declare avatar_file_hash?: string
|
||||
}
|
||||
5
client-protocol/deno.jsonc
Normal file
5
client-protocol/deno.jsonc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"imports": {
|
||||
"socket.io-client": "npm:socket.io-client@4.8.1"
|
||||
}
|
||||
}
|
||||
15
client-protocol/test.ts
Normal file
15
client-protocol/test.ts
Normal 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())
|
||||
8
client-protocol/type/OnMessageData.ts
Normal file
8
client-protocol/type/OnMessageData.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import MessageBean from '../bean/MessageBean.ts'
|
||||
|
||||
interface OnMessageData {
|
||||
chat: string
|
||||
msg: MessageBean
|
||||
}
|
||||
|
||||
export default OnMessageData
|
||||
Reference in New Issue
Block a user