This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
export * as common from './common';
export * as config from './config';
export * as converse from './converse';
export * as friend from './friend';
export * as group from './group';
export * as message from './message';
export * as plugin from './plugin';
export * as user from './user';
export * as inbox from './inbox';

View File

@@ -0,0 +1,19 @@
import { request } from '../api/request';
import { buildCachedRequest } from '../cache/utils';
/**
* 获取可用的微服务列表
*/
export const fetchAvailableServices = buildCachedRequest(
'fetchAvailableServices',
async (): Promise<string[]> => {
const { data } = await request.get<{
nodeID: string;
cpu: unknown;
memory: unknown;
services: string[];
}>('/api/gateway/health');
return data.services;
}
);

View File

@@ -0,0 +1,94 @@
import { request } from '../api/request';
import { useGlobalConfigStore } from '../store/globalConfig';
import { defaultGlobalConfig } from '../utils/consts';
/**
* 后端的全局设置
*/
export interface GlobalConfig {
/**
* Tianji 配置
*/
tianji: {
scriptUrl?: string;
websiteId?: string;
};
/**
* 上传文件体积
* 默认1m
*/
uploadFileLimit: number;
/**
* 是否在注册时校验邮箱
*/
emailVerification: boolean;
/**
* 服务器名
*/
serverName?: string;
/**
* 服务器入口背景图
*/
serverEntryImage?: string;
/**
* 是否禁用 Socketio 的 Msgpack 解析器
*/
disableMsgpack?: boolean;
/**
* 是否禁用注册功能
*/
disableUserRegister?: boolean;
/**
* 是否禁用游客登录
*/
disableGuestLogin?: boolean;
/**
* 是否禁用创建群组功能
*/
disableCreateGroup?: boolean;
/**
* 是否禁用插件中心
*/
disablePluginStore?: boolean;
/**
* 是否禁用添加好友功能
*/
disableAddFriend?: boolean;
/**
* 是否禁用遥测
*/
disableTelemetry?: boolean;
announcement?:
| false
| {
id: string;
text: string;
link?: string;
};
}
export function getGlobalConfig(): GlobalConfig {
return useGlobalConfigStore.getState();
}
export async function fetchGlobalClientConfig(): Promise<GlobalConfig> {
const { data: config } = await request.get('/api/config/client');
useGlobalConfigStore.setState({
...defaultGlobalConfig,
...config,
});
return config;
}

View File

@@ -0,0 +1,144 @@
import { request } from '../api/request';
import {
createAutoMergedRequest,
createAutoSplitRequest,
} from '../utils/request';
import _uniq from 'lodash/uniq';
import _flatten from 'lodash/flatten';
import _zipObject from 'lodash/zipObject';
export enum ChatConverseType {
DM = 'DM', // 单人会话
Multi = 'Multi', // 多人会话
Group = 'Group', // 群组会话(暂时无用)
}
export interface ChatConverseInfo {
_id: string;
name: string;
type: ChatConverseType;
members: string[];
}
/**
* 尝试创建私聊会话
* 如果已创建则返回之前的
*/
export async function createDMConverse(
memberIds: string[]
): Promise<ChatConverseInfo> {
const { data } = await request.post('/api/chat/converse/createDMConverse', {
memberIds,
});
return data;
}
/**
* 在多人会话中添加成员
*/
export async function appendDMConverseMembers(
converseId: string,
memberIds: string[]
) {
const { data } = await request.post(
'/api/chat/converse/appendDMConverseMembers',
{
converseId,
memberIds,
}
);
return data;
}
/**
* 获取会话信息
* @param converseId 会话ID
*/
export async function fetchConverseInfo(
converseId: string
): Promise<ChatConverseInfo> {
const { data } = await request.get('/api/chat/converse/findConverseInfo', {
params: {
converseId,
},
});
return data;
}
/**
* 更新会话已读
* @param converseId 会话ID
* @param lastMessageId 最后一条消息ID
*/
export async function updateAck(converseId: string, lastMessageId: string) {
await request.post('/api/chat/ack/update', { converseId, lastMessageId });
}
interface AckInfo {
userId: string;
converseId: string;
lastMessageId: string;
}
/**
* 获取用户存储在远程的会话信息
*/
export async function fetchUserAck(): Promise<AckInfo[]> {
const { data } = await request.get('/api/chat/ack/all');
if (!Array.isArray(data)) {
return [];
}
return data;
}
/**
* 获取用户存储在远程的会话信息
*/
export async function fetchUserAckList(
converseIds: string[]
): Promise<(AckInfo | null)[]> {
const { data } = await request.post('/api/chat/ack/list', {
converseIds,
});
if (!Array.isArray(data)) {
return [];
}
return data;
}
const _fetchConverseAckInfo = createAutoMergedRequest<
string[],
(AckInfo | null)[]
>(
createAutoSplitRequest(
async (converseIdsList) => {
const uniqList = _uniq(_flatten(converseIdsList));
const infoList = await fetchUserAckList(uniqList);
const map = _zipObject<AckInfo | null>(uniqList, infoList);
// 将请求结果根据传输来源重新分组
return converseIdsList.map((converseIds) =>
converseIds.map((converseId) => map[converseId] ?? null)
);
},
'serial',
100
)
);
/**
* 获取会话信息
*/
export async function getConverseAckInfo(
converseIds: string[]
): Promise<(AckInfo | null)[]> {
return _fetchConverseAckInfo(converseIds);
}

View File

@@ -0,0 +1,74 @@
import { request } from '../api/request';
export interface FriendRequest {
_id: string;
from: string;
to: string;
message: string;
}
/**
* 发送好友请求
* @param targetId 目标用户id
*/
export async function addFriendRequest(
targetId: string
): Promise<FriendRequest> {
const { data } = await request.post('/api/friend/request/add', {
to: targetId,
});
return data;
}
/**
* 同意好友请求
* @param requestId 好友请求ID
*/
export async function acceptFriendRequest(requestId: string): Promise<void> {
await request.post('/api/friend/request/accept', {
requestId,
});
}
/**
* 拒绝好友请求
* @param requestId 好友请求ID
*/
export async function denyFriendRequest(requestId: string): Promise<void> {
await request.post('/api/friend/request/deny', {
requestId,
});
}
/**
* 取消好友请求
* @param requestId 好友请求ID
*/
export async function cancelFriendRequest(requestId: string): Promise<void> {
await request.post('/api/friend/request/cancel', {
requestId,
});
}
/**
* 移除好友(单项)
*/
export async function removeFriend(friendUserId: string): Promise<void> {
await request.post('/api/friend/removeFriend', {
friendUserId,
});
}
/**
* 设置好友昵称
*/
export async function setFriendNickname(
targetId: string,
nickname: string
): Promise<void> {
await request.post('/api/friend/setFriendNickname', {
targetId,
nickname,
});
}

View File

@@ -0,0 +1,480 @@
import { request } from '../api/request';
import {
GroupPanelType,
GroupPanel,
GroupRole,
GroupInfo as IGroupInfo,
GroupBasicInfo,
GroupInvite,
} from 'tailchat-types';
export { GroupPanelType };
export type { GroupPanel, GroupRole, GroupBasicInfo, GroupInvite };
export const groupConfigNames = [
// 隐藏群组成员标识位
'hideGroupMemberDiscriminator',
// 禁止从群组中发起私信
'disableCreateConverseFromGroup',
// 群组背景图
'groupBackgroundImage',
] as const;
export type GroupConfigNames = (typeof groupConfigNames)[number] | string; // string is plugin config
export interface GroupMember {
roles: string[]; // 角色组
userId: string;
/**
* 日期字符串 禁言到xxx
*/
muteUntil?: string;
}
/**
* 群组面板特性
*/
export type GroupPanelFeature =
| 'subscribe' // 订阅事件变更状态用于加入socket.io群组
| 'ack'; // 是否包含已读未读检查,如果包含的话需要同时开启 subscribe 特性
export interface GroupInfo extends Omit<IGroupInfo, 'config'> {
config?: Partial<Record<GroupConfigNames, any>>;
pinnedPanelId?: string;
}
/**
* 获取群组设置信息
*/
export function getGroupConfigWithInfo(
groupInfo: GroupInfo | null | undefined
): {
hideGroupMemberDiscriminator: boolean;
disableCreateConverseFromGroup: boolean;
[key: string]: unknown;
} {
const config = groupInfo?.config ?? {};
return {
...config,
hideGroupMemberDiscriminator: config.hideGroupMemberDiscriminator ?? false,
disableCreateConverseFromGroup:
config.disableCreateConverseFromGroup ?? false,
};
}
/**
* 创建群组
* @param name 群组名
* @param panels 初始面板
*/
export async function createGroup(
name: string,
panels: GroupPanel[]
): Promise<GroupInfo> {
const { data } = await request.post('/api/group/createGroup', {
name,
panels,
});
return data;
}
/**
* 获取群组基本信息
*/
export async function getGroupBasicInfo(
groupId: string
): Promise<GroupBasicInfo | null> {
const { data } = await request.get('/api/group/getGroupBasicInfo', {
params: {
groupId,
},
});
return data;
}
/**
* 修改群组属性
* @param groupId 群组ID
* @param fieldName 要修改的群组属性
* @param fieldValue 要修改的属性的值
*/
type AllowedModifyField =
| 'name'
| 'avatar'
| 'description'
| 'panels'
| 'roles'
| 'fallbackPermissions';
export async function modifyGroupField(
groupId: string,
fieldName: AllowedModifyField,
fieldValue: unknown
) {
await request.post('/api/group/updateGroupField', {
groupId,
fieldName,
fieldValue,
});
}
/**
* 修改群组配置
* @param groupId 群组ID
* @param configName 要修改的群组属性
* @param configValue 要修改的属性的值
*/
export async function modifyGroupConfig(
groupId: string,
configName: GroupConfigNames,
configValue: unknown
) {
await request.post('/api/group/updateGroupConfig', {
groupId,
configName,
configValue,
});
}
/**
* 退出群组(群组拥有者是解散群组)
* 这里必须是一个socket请求因为后端需要进行房间的退出操作
* @param groupId 群组ID
*/
export async function quitGroup(groupId: string) {
await request.post('/api/group/quitGroup', {
groupId,
});
}
/**
* 检查当前用户是否是群组成员
* @param groupId 群组ID
*/
export async function isMember(groupId: string): Promise<boolean> {
const { data } = await request.post('/api/group/isMember', {
groupId,
});
return data;
}
/**
* 更新用户所在的权限组
* @param groupId 群组ID
* @param memberIds 成员信息
* @param roles 权限组名
*/
export async function appendGroupMemberRoles(
groupId: string,
memberIds: string[],
roles: string[]
) {
await request.post('/api/group/appendGroupMemberRoles', {
groupId,
memberIds,
roles,
});
}
/**
* 更新用户所在的权限组
* @param groupId 群组ID
* @param memberIds 成员信息
* @param roles 权限组名
*/
export async function removeGroupMemberRoles(
groupId: string,
memberIds: string[],
roles: string[]
) {
await request.post('/api/group/removeGroupMemberRoles', {
groupId,
memberIds,
roles,
});
}
/**
* 创建群组邀请码
* 邀请码默认 7天有效期
* @param groupId 群组id
*/
export async function createGroupInviteCode(
groupId: string,
inviteType: 'normal' | 'permanent'
): Promise<GroupInvite> {
const { data } = await request.post('/api/group/invite/createGroupInvite', {
groupId,
inviteType,
});
return data;
}
/**
* 编辑群组邀请链接
* @param groupId 群组ID
* @param code 邀请码
* @param expiredAt 过期时间是一个时间戳单位ms为undefined则为不限制
* @param usageLimit 最大使用次数为undefined则不限制
*/
export async function editGroupInvite(
groupId: string,
code: string,
expiredAt?: number,
usageLimit?: number
) {
await request.post('/api/group/invite/editGroupInvite', {
groupId,
code,
expiredAt,
usageLimit,
});
}
/**
* 获取群组所有邀请码
* @param groupId 群组ID
*/
export async function getAllGroupInviteCode(
groupId: string
): Promise<GroupInvite[]> {
const { data } = await request.get(
'/api/group/invite/getAllGroupInviteCode',
{
params: {
groupId,
},
}
);
return data;
}
/**
* 根据邀请码查找邀请信息
* @param inviteCode 邀请码
*/
export async function findGroupInviteByCode(
inviteCode: string
): Promise<GroupInvite | null> {
const { data } = await request.get('/api/group/invite/findInviteByCode', {
params: {
code: inviteCode,
},
});
return data;
}
/**
* 使用群组邀请
* 即通过群组邀请加入群组
*/
export async function applyGroupInvite(inviteCode: string): Promise<void> {
await request.post('/api/group/invite/applyInvite', {
code: inviteCode,
});
}
/**
* 删除群组邀请
*/
export async function deleteGroupInvite(
groupId: string,
inviteId: string
): Promise<void> {
await request.post('/api/group/invite/deleteInvite', {
groupId,
inviteId,
});
}
/**
* 创建群组面板
*/
export async function createGroupPanel(
groupId: string,
options: {
name: string;
type: number;
parentId?: string;
provider?: string;
pluginPanelName?: string;
meta?: Record<string, unknown>;
}
) {
await request.post('/api/group/createGroupPanel', {
...options,
groupId,
});
}
/**
* 创建群组面板
*/
export async function modifyGroupPanel(
groupId: string,
panelId: string,
options: {
name: string;
type: number;
parentId?: string;
provider?: string;
pluginPanelName?: string;
meta?: Record<string, unknown>;
}
) {
await request.post('/api/group/modifyGroupPanel', {
...options,
groupId,
panelId,
});
}
/**
* 删除群组面板
* @param groupId 群组Id
* @param panelId 面板Id
*/
export async function deleteGroupPanel(groupId: string, panelId: string) {
await request.post('/api/group/deleteGroupPanel', {
groupId,
panelId,
});
}
/**
* 创建群组身份组
* @param groupId 群组id
* @param roleName 群组名
* @param permissions 初始权限
*/
export async function createGroupRole(
groupId: string,
roleName: string,
permissions: string[]
) {
await request.post('/api/group/createGroupRole', {
groupId,
roleName,
permissions,
});
}
/**
* 删除群组身份组
* @param groupId 群组Id
* @param roleId 身份组Id
*/
export async function deleteGroupRole(groupId: string, roleId: string) {
await request.post('/api/group/deleteGroupRole', {
groupId,
roleId,
});
}
/**
* 删除群组身份组
* @param groupId 群组Id
* @param roleId 身份组Id
* @param roleName 新身份组名
*/
export async function updateGroupRoleName(
groupId: string,
roleId: string,
roleName: string
) {
await request.post('/api/group/updateGroupRoleName', {
groupId,
roleId,
roleName,
});
}
/**
* 删除群组身份组
* @param groupId 群组Id
* @param roleId 身份组Id
* @param permissions 全量权限列表
*/
export async function updateGroupRolePermission(
groupId: string,
roleId: string,
permissions: string[]
) {
await request.post('/api/group/updateGroupRolePermission', {
groupId,
roleId,
permissions,
});
}
/**
* 禁言群组成员
* @param groupId 群组ID
* @param memberId 成员ID
* @param muteMs 禁言到xxx, 精确到毫秒
*/
export async function muteGroupMember(
groupId: string,
memberId: string,
muteMs: number
) {
await request.post('/api/group/muteGroupMember', {
groupId,
memberId,
muteMs,
});
}
/**
* 移出群组成员
* @param groupId 群组ID
* @param memberId 成员ID
*/
export async function deleteGroupMember(groupId: string, memberId: string) {
await request.post('/api/group/deleteGroupMember', {
groupId,
memberId,
});
}
/**
* Get Group Panel Data from group.extra service
*/
export async function getGroupPanelExtraData(
groupId: string,
panelId: string,
name: string
): Promise<string | null> {
const { data } = await request.post('/api/group/extra/getPanelData', {
groupId,
panelId,
name,
});
return data.data ?? null;
}
/**
* Save Group Panel Data to group.extra service
*/
export async function saveGroupPanelExtraData(
groupId: string,
panelId: string,
name: string,
data: any
): Promise<void> {
await request.post('/api/group/extra/savePanelData', {
groupId,
panelId,
name,
data: typeof data === 'string' ? data : JSON.stringify(data),
});
}

View File

@@ -0,0 +1,23 @@
import { request } from '../api/request';
export type {
BasicInboxItem,
MessageInboxItem,
MarkdownInboxItem,
InboxItem,
} from 'tailchat-types';
/**
* 设置收件箱某条记录已读
*/
export async function setInboxAck(inboxItemIds: string[]) {
await request.post('/api/chat/inbox/ack', {
inboxItemIds,
});
}
/**
* 清空收件箱
*/
export async function clearInbox() {
await request.post('/api/chat/inbox/clear');
}

View File

@@ -0,0 +1,204 @@
import { request } from '../api/request';
import type { ChatMessageReaction, ChatMessage } from 'tailchat-types';
import {
createAutoMergedRequest,
createAutoSplitRequest,
} from '../utils/request';
import _uniq from 'lodash/uniq';
import _flatten from 'lodash/flatten';
import _zipObject from 'lodash/zipObject';
export { ChatMessageReaction, ChatMessage };
export interface LocalChatMessage extends ChatMessage {
/**
* 本地添加消息的标识,用于标记该条消息尚未确定已经发送到服务端
*/
isLocal?: boolean;
/**
* 判断是否发送失败
*/
sendFailed?: boolean;
}
export interface SimpleMessagePayload {
groupId?: string;
converseId: string;
content: string;
}
export interface SendMessagePayloadMeta {
mentions?: string[];
}
export interface SendMessagePayload extends SimpleMessagePayload {
/**
* content的plain内容
* 用于inbox
*/
plain?: string;
meta?: SendMessagePayloadMeta;
}
/**
* 获取会话消息
* @param converseId 会话ID
* @param startId 开始ID
*/
export async function fetchConverseMessage(
converseId: string,
startId?: string
): Promise<ChatMessage[]> {
const { data } = await request.get('/api/chat/message/fetchConverseMessage', {
params: {
converseId,
startId,
},
});
return data;
}
/**
* 发送消息
* @param payload 消息体
*/
export async function sendMessage(
payload: SendMessagePayload
): Promise<ChatMessage> {
const { data } = await request.post('/api/chat/message/sendMessage', payload);
return data;
}
/**
* 撤回消息
* @param messageId 消息ID
*/
export async function recallMessage(messageId: string): Promise<ChatMessage> {
const { data } = await request.post('/api/chat/message/recallMessage', {
messageId,
});
return data;
}
export async function deleteMessage(messageId: string): Promise<boolean> {
const { data } = await request.post('/api/chat/message/deleteMessage', {
messageId,
});
return data;
}
/**
* 搜索聊天记录
* @param converseId 会话id
* @param messageText 聊天文本
*/
export async function searchMessage(
text: string,
converseId: string,
groupId?: string
): Promise<ChatMessage[]> {
const { data } = await request.post('/api/chat/message/searchMessage', {
text,
converseId,
groupId,
});
return data;
}
interface LastMessageInfo {
converseId: string;
lastMessageId: string;
}
/**
* 基于会话id获取会话最后一条消息的id
*/
async function fetchConverseLastMessages(
converseIds: string[]
): Promise<{ converseId: string; lastMessageId: string }[]> {
const { data } = await request.post(
'/api/chat/message/fetchConverseLastMessages',
{
converseIds,
}
);
return data;
}
export const _fetchConverseLastMessageInfo = createAutoMergedRequest<
string[],
(LastMessageInfo | null)[]
>(
createAutoSplitRequest(
async (converseIdsList) => {
const uniqList = _uniq(_flatten(converseIdsList));
const infoList = await fetchConverseLastMessages(uniqList);
const map = _zipObject<LastMessageInfo | null>(uniqList, infoList);
// 将请求结果根据传输来源重新分组
return converseIdsList.map((converseIds) =>
converseIds.map((converseId) => map[converseId] ?? null)
);
},
'serial',
100
)
);
export function getConverseLastMessageInfo(converseIds: string[]) {
return _fetchConverseLastMessageInfo(converseIds);
}
/**
* @param converseId 会话ID
* @param messageId 消息ID
* @returns 消息附近的信息
*/
export async function fetchNearbyMessage(params: {
groupId?: string;
converseId: string;
messageId: string;
}): Promise<ChatMessage[]> {
const { data } = await request.post(
'/api/chat/message/fetchNearbyMessage',
params
);
return data;
}
/**
* 增加表情行为
*/
export async function addReaction(
messageId: string,
emoji: string
): Promise<boolean> {
const { data } = await request.post('/api/chat/message/addReaction', {
messageId,
emoji,
});
return data;
}
/**
* 移除表情行为
*/
export async function removeReaction(
messageId: string,
emoji: string
): Promise<boolean> {
const { data } = await request.post('/api/chat/message/removeReaction', {
messageId,
emoji,
});
return data;
}

View File

@@ -0,0 +1,93 @@
import { request } from '../api/request';
export interface PluginManifest {
/**
* 插件用于显示的名称
* @example 网页面板插件
*/
label: string;
'label.zh-CN'?: string;
/**
* 插件名, 插件唯一标识
* @example com.msgbyte.webview
*/
name: string;
/**
* 插件地址
*/
url: string;
/**
* 插件图标
* 推荐大小: 128x128
*/
icon?: string;
/**
* 插件版本号
* 遵循 semver 规则
*
* major.minor.patch
* @example 1.0.0
*/
version: string;
/**
* 插件维护者
*/
author: string;
/**
* 插件描述
*/
description: string;
'description.zh-CN'?: string;
/**
* 是否需要重启才能应用插件
*/
requireRestart: boolean;
/**
* 文档的链接
* 如果是markdown则解析, 如果是html则使用iframe
*/
documentUrl?: string;
}
/**
* 获取服务端插件中心的插件列表
*
* 后端动态
*/
export async function fetchRegistryPlugins(): Promise<PluginManifest[]> {
const { data } = await request.get('/api/plugin/registry/list');
return data;
}
/**
* 获取服务器安装的插件列表
*
* 后端固定
*/
export async function fetchServiceRegistryPlugins(): Promise<PluginManifest[]> {
const { data } = await request.get('/registry-be.json');
return data;
}
/**
* 获取本地固定的registry
*
* 前端固定
*/
export async function fetchLocalStaticRegistryPlugins(): Promise<
PluginManifest[]
> {
const { data } = await request.get('/registry.json', { baseURL: '' });
return data;
}

435
client/shared/model/user.ts Normal file
View File

@@ -0,0 +1,435 @@
import { request } from '../api/request';
import { buildCachedRequest } from '../cache/utils';
import { sharedEvent } from '../event';
import { SYSTEM_USERID } from '../utils/consts';
import {
createAutoMergedRequest,
createAutoSplitRequest,
} from '../utils/request';
import _pick from 'lodash/pick';
import _uniq from 'lodash/uniq';
import _flatten from 'lodash/flatten';
import _zipObject from 'lodash/zipObject';
import { t } from '../i18n';
import type { UserBaseInfo } from 'tailchat-types';
import { isObjectId } from '../utils/string-helper';
export type { UserBaseInfo };
export interface UserLoginInfo extends UserBaseInfo {
token: string;
createdAt: string;
}
export interface UserSettings {
/**
* 消息列表虚拟化
*/
messageListVirtualization?: boolean;
/**
* 消息通知免打扰(静音)
*/
messageNotificationMuteList?: string[];
/**
* 群组排序, 内容为群组id
*/
groupOrderList?: string[];
/**
* 是否关闭消息右键菜单
*/
disableMessageContextMenu?: boolean;
/**
* 其他的设置项
*/
[key: string]: any;
}
export function pickUserBaseInfo(userInfo: UserLoginInfo): UserBaseInfo {
return _pick(userInfo, [
'_id',
'email',
'nickname',
'discriminator',
'avatar',
'temporary',
'type',
'emailVerified',
'banned',
]);
}
// 内置用户信息
const builtinUserInfo: Record<string, () => UserBaseInfo> = {
[SYSTEM_USERID]: () => ({
_id: SYSTEM_USERID,
email: 'admin@msgbyte.com',
nickname: t('系统'),
discriminator: '0000',
avatar: null,
temporary: false,
type: 'normalUser',
emailVerified: false,
banned: false,
}),
'': () => ({
// dummy
_id: '',
email: '',
nickname: '',
discriminator: '0000',
avatar: null,
temporary: false,
type: 'normalUser',
emailVerified: false,
banned: false,
}),
};
/**
* 用户私信列表
*/
export interface UserDMList {
userId: string;
converseIds: string[];
}
/**
* 邮箱登录
* @param email 邮箱
* @param password 密码
*/
export async function loginWithEmail(
email: string,
password: string
): Promise<UserLoginInfo> {
const { data } = await request.post('/api/user/login', {
email,
password,
});
sharedEvent.emit('loginSuccess', pickUserBaseInfo(data));
return data;
}
/**
* 使用 Token 登录
* @param token JWT令牌
*/
export async function loginWithToken(token: string): Promise<UserLoginInfo> {
const { data } = await request.post('/api/user/resolveToken', {
token,
});
sharedEvent.emit('loginSuccess', pickUserBaseInfo(data));
return data;
}
/**
* 发送邮箱校验码
* @param email 邮箱
*/
export async function verifyEmail(email: string): Promise<UserLoginInfo> {
const { data } = await request.post('/api/user/verifyEmail', {
email,
});
return data;
}
/**
* 检查邮箱校验码并更新用户字段
* @param email 邮箱
*/
export async function verifyEmailWithOTP(
emailOTP: string
): Promise<UserLoginInfo> {
const { data } = await request.post('/api/user/verifyEmailWithOTP', {
emailOTP,
});
return data;
}
/**
* 邮箱注册账号
* @param email 邮箱
* @param password 密码
*/
export async function registerWithEmail({
email,
password,
nickname,
emailOTP,
}: {
email: string;
password: string;
nickname?: string;
emailOTP?: string;
}): Promise<UserLoginInfo> {
const { data } = await request.post('/api/user/register', {
email,
nickname,
password,
emailOTP,
});
return data;
}
/**
* 修改密码
*/
export async function modifyUserPassword(
oldPassword: string,
newPassword: string
): Promise<void> {
await request.post('/api/user/modifyPassword', {
oldPassword,
newPassword,
});
}
/**
* 忘记密码
* @param email 邮箱
*/
export async function forgetPassword(email: string) {
await request.post('/api/user/forgetPassword', {
email,
});
}
/**
* 忘记密码
* @param email 邮箱
*/
export async function resetPassword(
email: string,
password: string,
otp: string
) {
await request.post('/api/user/resetPassword', {
email,
password,
otp,
});
}
/**
* 创建访客账号
* @param nickname 访客昵称
*/
export async function createTemporaryUser(
nickname: string
): Promise<UserLoginInfo> {
const { data } = await request.post('/api/user/createTemporaryUser', {
nickname,
});
return data;
}
/**
* 认领访客账号
*/
export async function claimTemporaryUser(
userId: string,
email: string,
password: string,
emailOTP?: string
): Promise<UserLoginInfo> {
const { data } = await request.post('/api/user/claimTemporaryUser', {
userId,
email,
password,
emailOTP,
});
return data;
}
/**
* 使用唯一标识名搜索用户
* @param uniqueName 唯一标识用户昵称: 用户昵称#0000
*/
export async function searchUserWithUniqueName(
uniqueName: string
): Promise<UserBaseInfo> {
const { data } = await request.post('/api/user/searchUserWithUniqueName', {
uniqueName,
});
return data;
}
const _fetchUserInfo = createAutoMergedRequest<string, UserBaseInfo>(
createAutoSplitRequest(
async (userIds) => {
// 这里用post是为了防止一次性获取的userId过多超过url限制
const { data } = await request.post('/api/user/getUserInfoList', {
userIds,
});
return data;
},
'serial',
1000
)
);
/**
* 获取用户基本信息
* @param userId 用户ID
*/
export async function fetchUserInfo(userId: string): Promise<UserBaseInfo> {
if (
builtinUserInfo[userId] &&
typeof builtinUserInfo[userId] === 'function'
) {
return builtinUserInfo[userId]();
}
if (!isObjectId(userId)) {
throw new Error(`Invalid userId: ${userId}`);
}
const userInfo = await _fetchUserInfo(userId);
return userInfo;
}
const _fetchUserOnlineStatus = createAutoMergedRequest<string[], boolean[]>(
createAutoSplitRequest(
async (userIdsList) => {
const uniqList = _uniq(_flatten(userIdsList));
// 这里用post是为了防止一次性获取的userId过多超过url限制
const { data } = await request.post('/api/gateway/checkUserOnline', {
userIds: uniqList,
});
const map = _zipObject<boolean>(uniqList, data);
// 将请求结果根据传输来源重新分组
return userIdsList.map((userIds) =>
userIds.map((userId) => map[userId] ?? false)
);
},
'serial',
1000
)
);
/**
* 获取用户在线状态
*/
export async function getUserOnlineStatus(
userIds: string[]
): Promise<boolean[]> {
return _fetchUserOnlineStatus(userIds);
}
/**
* 将会话添加到用户私信列表
* 如果已添加则后端忽略
*/
export async function appendUserDMConverse(
converseId: string
): Promise<UserDMList> {
const { data } = await request.post<UserDMList>(
'/api/user/dmlist/addConverse',
{
converseId,
}
);
return data;
}
/**
* 移除会话列表
*/
export async function removeUserDMConverse(
converseId: string
): Promise<UserDMList> {
const { data } = await request.post<UserDMList>(
'/api/user/dmlist/removeConverse',
{
converseId,
}
);
return data;
}
/**
* 修改用户属性
* @param fieldName 要修改的属性名
* @param fieldValue 要修改的属性的值
*/
type AllowedModifyField = 'nickname' | 'avatar';
export async function modifyUserField(
fieldName: AllowedModifyField,
fieldValue: unknown
): Promise<UserBaseInfo> {
const { data } = await request.post('/api/user/updateUserField', {
fieldName,
fieldValue,
});
return data;
}
export async function modifyUserExtra(
fieldName: string,
fieldValue: unknown
): Promise<UserBaseInfo> {
const { data } = await request.post('/api/user/updateUserExtra', {
fieldName,
fieldValue,
});
return data;
}
/**
* 获取用户设置
*/
export async function getUserSettings(): Promise<UserSettings> {
const { data } = await request.get('/api/user/getUserSettings');
sharedEvent.emit('userSettingsUpdate', data);
return data;
}
/**
* 设置用户设置
*/
export async function setUserSettings(
settings: UserSettings
): Promise<UserSettings> {
const { data } = await request.post('/api/user/setUserSettings', {
settings,
});
return data;
}
/**
* 检查Token是否可用
*/
export const checkTokenValid = buildCachedRequest(
'tokenValid',
async (token: string): Promise<boolean> => {
const { data } = await request.post<boolean>('/api/user/checkTokenValid', {
token,
});
return data;
}
);