Compare commits

...

9 Commits

Author SHA1 Message Date
CrescentLeaf
f0ca0fbbd4 feat: 全新的客户端协议库! 2025-11-09 01:00:01 +08:00
CrescentLeaf
3e5fc722e6 fix: typo 2025-11-09 00:38:43 +08:00
CrescentLeaf
a646d7908a 添加两个客户端协议的类型文件 2025-11-09 00:33:01 +08:00
CrescentLeaf
cfe8df43d1 为 MessageBean 添加 chat_id 字段?
* 不知道有没有用, 有可能会被移除
* 有可能是史山
2025-11-09 00:32:05 +08:00
CrescentLeaf
743ccd1172 chore: 提取公共类 2025-11-08 23:50:43 +08:00
CrescentLeaf
3c5bd187b7 chore: 修缮非 throw 方法返回值 2025-11-08 23:49:58 +08:00
CrescentLeaf
27035eb2ca fix: 更新头像 file_hash 忘记检测 target 存在与否 2025-11-08 23:06:19 +08:00
CrescentLeaf
7a308e2261 fix: 客户端协议修改用户资料后没有在原对象 Bean 同步 2025-11-08 22:55:15 +08:00
CrescentLeaf
3cc60986ab fix: add missing import 2025-11-08 22:54:41 +08:00
16 changed files with 414 additions and 22 deletions

229
client-protocol/Chat.ts Normal file
View File

@@ -0,0 +1,229 @@
import BaseClientObject from "./BaseClientObject.ts"
import BaseChatSettingsBean from "./bean/BaseChatSettingsBean.ts"
import ChatBean from "./bean/ChatBean.ts"
import JoinRequestBean from "./bean/JoinRequestBean.ts"
import MessageBean from "./bean/MessageBean.ts"
import CallbackError from "./CallbackError.ts"
import JoinRequest from "./JoinRequest.ts"
import LingChairClient from "./LingChairClient.ts"
import Message from "./Message.ts"
export default class Chat extends BaseClientObject {
declare bean: ChatBean
constructor(client: LingChairClient, bean: ChatBean) {
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("Chat.getInfo", {
token: client.access_token,
target: id,
})
if (re.code == 200)
return new Chat(client, re.data as unknown as ChatBean)
throw new CallbackError(re)
}
/**
* ================================================
* 创建对话 (另类实例化方法)
* ================================================
*/
static async getOrCreatePrivateChat(client: LingChairClient, user_id: string) {
try {
return await this.getOrCreatePrivateChatOrThrow(client, user_id)
} catch (_) {
return null
}
}
static async getOrCreatePrivateChatOrThrow(client: LingChairClient, user_id: string) {
const re = await client.invoke("Chat.getIdForPrivate", {
token: client.access_token,
target: user_id,
})
if (re.code != 200) throw new CallbackError(re)
return new Chat(client, re.data as unknown as ChatBean)
}
static async createGroup(client: LingChairClient, title: string, name?: string) {
try {
return await this.createGroupOrThrow(client, title, name)
} catch (_) {
return null
}
}
static async createGroupOrThrow(client: LingChairClient, title: string, name?: string) {
const re = await client.invoke("Chat.createGroup", {
token: client.access_token,
title,
name,
})
if (re.code != 200) throw new CallbackError(re)
return new Chat(client, re.data as unknown as ChatBean)
}
/**
* ================================================
* 对话消息
* ================================================
*/
async getMessages(page: number = 0) {
return (await this.getMessageBeans(page)).map((v) => new Message(this.client, v))
}
async getMessagesOrThrow(page: number = 0) {
return (await this.getMessageBeansOrThrow(page)).map((v) => new Message(this.client, v))
}
async getMessageBeans(page: number = 0) {
try {
return await this.getMessageBeansOrThrow(page)
} catch (_) {
return []
}
}
async getMessageBeansOrThrow(page: number = 0) {
const re = await this.client.invoke("Chat.getMessageHistory", {
token: this.client.access_token,
page,
target: this.bean.id,
})
if (re.code == 200) return re.data!.messages as MessageBean[]
throw new CallbackError(re)
}
async sendMessage(text: string) {
try {
return await this.sendMessageOrThrow(text)
} catch (_) {
return null
}
}
async sendMessageOrThrow(text: string) {
const re = await this.client.invoke("Chat.sendMessage", {
token: this.client.access_token,
text,
target: this.bean.id,
})
if (re.code == 200)
return new Message(this.client, re.data!.message as MessageBean)
throw new CallbackError(re)
}
/**
* ================================================
* 加入对话申请
* ================================================
*/
async getJoinRequests() {
try {
return await this.getJoinRequestsOrThrow()
} catch (_) {
return []
}
}
async getJoinRequestsOrThrow() {
const join_requests = await this.getJoinRequestBeansOrThrow()
return join_requests.map((jr) => new JoinRequest(this.client, jr, this.bean.id))
}
async getJoinRequestBeans() {
try {
return await this.getJoinRequestBeansOrThrow()
} catch (_) {
return []
}
}
async getJoinRequestBeansOrThrow() {
const re = await this.client.invoke("Chat.getJoinRequests", {
token: this.client.access_token
})
if (re.code == 200)
return re.data!.join_requests as JoinRequestBean[]
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("Chat.setAvatar", {
token: this.client.access_token,
file_hash,
target: this.bean.id,
})
if (re.code != 200) throw new CallbackError(re)
this.bean.avatar_file_hash = file_hash
}
async updateSettings(args: BaseChatSettingsBean) {
try {
await this.updateSettingsOrThrow(args)
return true
} catch (_) {
return false
}
}
async updateSettingsOrThrow(args: BaseChatSettingsBean) {
const re = await this.client.invoke("Chat.updateSettings", {
token: this.client.access_token,
target: this.bean.id,
settings: args
})
if (re.code != 200) throw new CallbackError(re)
this.bean.settings = args
}
async getTheOtherUserId() {
try {
return await this.getTheOtherUserIdOrThrow()
} catch (_) {
return null
}
}
async getTheOtherUserIdOrThrow() {
const re = await this.client.invoke("Chat.updateSettings", {
token: this.client.access_token,
target: this.bean.id,
})
if (re.code != 200) throw new CallbackError(re)
return re.data!.user_id as string
}
/*
* ================================================
* 基本 Bean
* ================================================
*/
getId() {
return this.bean.id
}
getTitle() {
return this.bean.title
}
getType() {
return this.bean.type
}
isMember() {
return this.bean.is_member
}
isAdmin() {
return this.bean.is_admin
}
getAvatarFileHash() {
return this.bean.avatar_file_hash
}
getSettings() {
return this.bean.settings
}
}

View File

@@ -0,0 +1,66 @@
import BaseClientObject from "./BaseClientObject.ts"
import JoinRequestBean from "./bean/JoinRequestBean.ts"
import CallbackError from "./CallbackError.ts"
import LingChairClient from "./LingChairClient.ts"
import JoinRequestAction from "./type/JoinRequestAction.ts"
export default class JoinRequest extends BaseClientObject {
declare bean: JoinRequestBean
declare chat_id: string
constructor(client: LingChairClient, bean: JoinRequestBean, chat_id: string) {
super(client)
this.bean = bean
this.chat_id = chat_id
}
/*
* ================================================
* 操作
* ================================================
*/
async accept() {
return await this.process('accept')
}
async acceptOrThrow() {
return await this.processOrThrow('accept')
}
async remove() {
return await this.process('remove')
}
async removOrThrow() {
return await this.processOrThrow('remove')
}
async process(action: JoinRequestAction) {
try {
await this.processOrThrow(action)
return true
} catch (_) {
return false
}
}
async processOrThrow(action: JoinRequestAction) {
const re = await this.client.invoke("Chat.processJoinRequest", {
token: this.client.access_token,
chat_id: this.chat_id,
user_id: this.bean.user_id,
action,
})
if (re.code != 200) throw new CallbackError(re)
}
/*
* ================================================
* 基本 Bean
* ================================================
*/
getAvatarFileHash() {
return this.bean.avatar_file_hash
}
getUserId() {
return this.bean.user_id
}
getNickName() {
return this.bean.title
}
getReason() {
return this.bean.reason
}
}

View File

@@ -5,9 +5,12 @@ import { CallMethod, ClientEvent } from './ApiDeclare.ts'
import ApiCallbackMessage from './ApiCallbackMessage.ts'
import User from "./User.ts"
import UserMySelf from "./UserMySelf.ts"
import CallbackError from "./CallbackError.ts"
import Chat from "./Chat.ts"
export {
User,
Chat,
UserMySelf,
}
@@ -81,7 +84,7 @@ export default class LingChairClient {
password?: string,
}) {
try {
this.authOrThrow(args)
await this.authOrThrow(args)
return true
} catch (_) {
return false

View File

@@ -0,0 +1,39 @@
import BaseClientObject from "./BaseClientObject.ts"
import MessageBean from "./bean/MessageBean.ts"
import LingChairClient from "./LingChairClient.ts"
import Chat from "./Chat.ts"
import User from "./User.ts"
export default class Message extends BaseClientObject {
declare bean: MessageBean
constructor(client: LingChairClient, bean: MessageBean) {
super(client)
this.bean = bean
}
/*
* ================================================
* 基本 Bean
* ================================================
*/
getId() {
return this.bean.id
}
getChatId() {
return this.bean.chat_id
}
async getChat() {
return await Chat.getById(this.client, this.bean.chat_id as string)
}
getText() {
return this.bean.text
}
getUserId() {
return this.bean.user_id
}
async getUser() {
return await User.getById(this.client, this.bean.user_id as string)
}
getTime() {
return this.bean.time
}
}

View File

@@ -42,7 +42,7 @@ export default class UserMySelf extends User {
async resetPasswordOrThrow(old_password: string, new_password: string) {
const re = await this.client.invoke("User.resetPassword", {
token: this.client.access_token,
old_password,
old_password,
new_password,
})
if (re.code != 200) throw new CallbackError(re)
@@ -66,6 +66,7 @@ export default class UserMySelf extends User {
file_hash,
})
if (re.code != 200) throw new CallbackError(re)
this.bean.avatar_file_hash = file_hash
}
async setUserName(user_name: string) {
return await this.updateProfile({ username: user_name })
@@ -103,6 +104,8 @@ export default class UserMySelf extends User {
username,
})
if (re.code != 200) throw new CallbackError(re)
nickname && (this.bean.nickname = nickname)
username && (this.bean.username = username)
}
/*
* ================================================
@@ -143,7 +146,7 @@ export default class UserMySelf extends User {
try {
return await this.getMyFavouriteChatBeansOrThrow()
} catch (_) {
return null
return []
}
}
async getMyFavouriteChatBeansOrThrow() {
@@ -163,7 +166,7 @@ export default class UserMySelf extends User {
try {
return await this.getMyRecentChatBeansOrThrow()
} catch (_) {
return null
return []
}
}
async getMyRecentChatBeansOrThrow() {

View File

@@ -0,0 +1,5 @@
interface BaseChatSettings {
[key: string]: unknown
}
export default BaseChatSettings

View File

@@ -1,11 +1,12 @@
import ChatType from "./ChatType.ts"
import BaseChatSettingsBean from "./BaseChatSettingsBean.ts"
import ChatType from "../type/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 settings?: BaseChatSettingsBean
declare is_member: boolean
declare is_admin: boolean

View File

@@ -1,4 +1,6 @@
interface GroupSettingsBean {
import BaseChatSettings from "./BaseChatSettingsBean.ts"
interface GroupSettingsBean extends BaseChatSettings {
allow_new_member_join?: boolean
allow_new_member_from_invitation?: boolean
new_member_join_method?: 'disabled' | 'allowed_by_admin' | 'answered_and_allowed_by_admin'
@@ -7,8 +9,6 @@ interface GroupSettingsBean {
// 下面两个比较特殊, 由服务端给予
group_title: string
group_name: string
[key: string]: unknown
}
export default GroupSettingsBean

View File

@@ -1,7 +1,7 @@
export default class JoinRequestBean {
declare user_id: string
declare title: string
declare avatar?: string
declare nickname: string
declare avatar_file_hash?: string
declare reason?: string
[key: string]: unknown

View File

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

View File

@@ -1,4 +1,4 @@
import LingChairClient, { User, UserMySelf } from "./LingChairClient.ts"
import LingChairClient, { Chat, UserMySelf } from "./LingChairClient.ts"
import OnMessageData from "./type/OnMessageData.ts"
const client = new LingChairClient({
@@ -9,7 +9,10 @@ await client.auth({
account: '满月',
password: '满月',
})
client.on('Client.onMessage', (data: OnMessageData) => {
console.log(data)
client.on('Client.onMessage', async (data: OnMessageData) => {
const chat = await Chat.getById(client, data.chat)
const regexp = /^test (.*)/g.exec(data.msg.text)
if (regexp?.[0] != null) {
chat?.sendMessage(`Hello, ${regexp[1]}`)
}
})
console.log(await (await UserMySelf.getMySelf(client))?.getMyRecentChatBeans())

View File

@@ -0,0 +1,3 @@
type JoinRequestAction = 'accept' | 'remove'
export default JoinRequestAction

View File

@@ -62,17 +62,22 @@ export default class ChatApi extends BaseApi {
const userInst = User.findById(id)
userInst?.updateRecentChat(chat.bean.id, args.text as string)
})
const m = {
...msg,
id,
chat_id: chat.bean.id,
}
this.boardcastToUsers(users, 'Client.onMessage', {
chat: chat.bean.id,
msg: {
...msg,
id
}
msg: m
})
return {
code: 200,
msg: "成功",
data: {
message: m,
}
}
})
/**
@@ -191,6 +196,8 @@ export default class ChatApi extends BaseApi {
return {
user_id: user?.bean.id,
reason: v.reason,
nickname: user!.getNickName(),
// TODO: 这个得删掉, 应该用 nickname
title: user!.getNickName(),
avatar_file_hash: user!.getAvatarFileHash() ? user!.getAvatarFileHash() : null,
}
@@ -337,7 +344,18 @@ export default class ChatApi extends BaseApi {
code: 200,
msg: '成功',
data: {
// TODO: 移除这个, 将本方法重命名为 getOrCreatePrivateChat
// 并重构原 Web 客户端所引用的内容
chat_id: chat.bean.id,
id: chat.bean.id,
name: chat.bean.name,
type: chat.bean.type,
title: chat.getTitle(user),
avatar_file_hash: chat.getAvatarFileHash(user) ? chat.getAvatarFileHash(user) : undefined,
settings: JSON.parse(chat.bean.settings),
is_member: true,
is_admin: true,
}
}
})
@@ -379,7 +397,23 @@ export default class ChatApi extends BaseApi {
code: 200,
msg: '成功',
data: {
// TODO: 移除这个
// 并重构原 Web 客户端所引用的内容
chat_id: chat.bean.id,
id: chat.bean.id,
name: chat.bean.name,
type: chat.bean.type,
title: chat.getTitle(),
avatar_file_hash: chat.getAvatarFileHash() ? chat.getAvatarFileHash() : undefined,
settings: {
...JSON.parse(chat.bean.settings),
// 下面两个比较特殊, 用于群设置
group_name: chat.bean.name,
group_title: chat.getTitle(),
},
is_member: UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id),
is_admin: chat.checkUserIsAdmin(token.author),
}
}
})
@@ -463,7 +497,7 @@ export default class ChatApi extends BaseApi {
})
// 更新頭像
this.registerEvent("Chat.setAvatar", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['file_hash', 'token'])) return {
if (this.checkArgsMissing(args, ['file_hash', 'token', 'target'])) return {
msg: "参数缺失",
code: 400,
}

View File

@@ -2,6 +2,7 @@ export default class MessageBean {
declare id: number
declare text: string
declare user_id?: string
declare chat_id?: string
declare time: string
[key: string]: unknown

View File

@@ -60,7 +60,11 @@ export default class MessagesManager {
})
}
getMessages(limit: number = 15, offset: number = 0) {
return MessagesManager.database.prepare(`SELECT * FROM ${this.getTableName()} ORDER BY id DESC LIMIT ? OFFSET ?;`).all(limit, offset) as unknown as MessageBean[]
const ls = MessagesManager.database.prepare(`SELECT * FROM ${this.getTableName()} ORDER BY id DESC LIMIT ? OFFSET ?;`).all(limit, offset) as unknown as MessageBean[]
return ls.map((v) => ({
...v,
chat_id: this.chat.bean.id,
}))
}
getMessagesWithPage(limit: number = 15, page: number = 0) {
return this.getMessages(limit, limit * page)