Compare commits

...

10 Commits

Author SHA1 Message Date
CrescentLeaf
fb48c44655 ui: 移除 添加对话 输入框边距 2025-10-06 02:13:25 +08:00
CrescentLeaf
7378024235 feat: 添加任意对话, chore: 使用 User.create (createWithUserNameChecked 已移除) 2025-10-06 02:11:41 +08:00
CrescentLeaf
1c985f28a2 feat: 自动检验用户名和对话 ID 是否已经存在 2025-10-06 02:10:51 +08:00
CrescentLeaf
449c0a8806 feat: 创建群组, impl - 获取群组信息 2025-10-06 02:09:30 +08:00
CrescentLeaf
e1e42ea188 feat: 添加任意对话, 不局限于用户 2025-10-06 02:09:03 +08:00
CrescentLeaf
823eef76b0 feat: 所有对话列表中, 创建群组 2025-10-06 02:08:14 +08:00
CrescentLeaf
3b0b5ff032 feat: 创建群组对话框 2025-10-06 02:07:25 +08:00
CrescentLeaf
6112b4b207 fix: https cannot load pem 2025-10-06 00:56:42 +08:00
CrescentLeaf
9e8e967eb9 chore: remove useless code 2025-10-04 22:18:58 +08:00
CrescentLeaf
697082193f fix: missing contact type of contactsList 2025-10-04 22:18:58 +08:00
12 changed files with 164 additions and 49 deletions

View File

@@ -17,12 +17,6 @@
<body>
<div id="app"></div>
<mdui-snackbar close-on-outside-click id="public_snackbar"></mdui-snackbar>
<mdui-dialog close-on-overlay-click id="ErrorDialog">
<span slot="headline">错误</span>
<span slot="description" id="ErrorDialog_Message"></span>
</mdui-dialog>
<script nomodule>
alert('很抱歉, 此应用无法在较旧的浏览器运行, 请使用基于 Chromium 89+ 的浏览器(内核)使用 :(')

View File

@@ -10,17 +10,6 @@ import './ui/custom-elements/chat-image.ts'
import './ui/custom-elements/chat-video.ts'
import './ui/custom-elements/chat-file.ts'
const urlParams = new URL(location.href).searchParams
// deno-lint-ignore no-window no-window-prefix
urlParams.get('debug') == 'true' && window.addEventListener('error', ({ message, filename, lineno, colno, error }) => {
const m = $("#ErrorDialog_Message")
const d = $("#ErrorDialog").get(0) as Dialog
const s = d.open
d.open = true
m.html((s ? `${m.html()}<br/><br/>` : '') + `${message} (${filename || 'unknown'}:${lineno}:${colno})`)
})
import App from './ui/App.tsx'
import AppMobile from './ui/AppMobile.tsx'
import isMobileUI from "./ui/isMobileUI.ts"

View File

@@ -21,6 +21,7 @@ import useAsyncEffect from "./useAsyncEffect.ts"
import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx"
import Chat from "../api/client_data/Chat.ts"
import AddContactDialog from './dialog/AddContactDialog.tsx'
import CreateGroupDialog from './dialog/CreateGroupDialog.tsx'
import UserProfileDialog from "./dialog/UserProfileDialog.tsx"
import DataCaches from "../api/DataCaches.ts"
@@ -62,6 +63,7 @@ export default function App() {
const [userInfo, setUserInfo] = React.useState(null as unknown as User)
const addContactDialogRef = React.useRef<Dialog>(null)
const createGroupDialogRef = React.useRef<Dialog>(null)
const chatInfoDialogRef = React.useRef<Dialog>(null)
const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat)
@@ -153,6 +155,9 @@ export default function App() {
<AddContactDialog
addContactDialogRef={addContactDialogRef} />
<CreateGroupDialog
createGroupDialogRef={createGroupDialogRef} />
<mdui-navigation-rail contained value="Recents" ref={navigationRailRef}>
<mdui-button-icon slot="top">
<Avatar src={myUserProfileCache?.avatar} text={myUserProfileCache?.nickname} avatarRef={openMyProfileDialogButtonRef} />
@@ -179,6 +184,7 @@ export default function App() {
<ContactsList
openChatInfoDialog={openChatInfoDialog}
addContactDialogRef={addContactDialogRef as any}
createGroupDialogRef={createGroupDialogRef}
display={navigationItemSelected == "Contacts"} />
}
</div>

View File

@@ -20,6 +20,7 @@ import useAsyncEffect from "./useAsyncEffect.ts"
import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx"
import Chat from "../api/client_data/Chat.ts"
import AddContactDialog from './dialog/AddContactDialog.tsx'
import CreateGroupDialog from './dialog/CreateGroupDialog.tsx'
import UserProfileDialog from "./dialog/UserProfileDialog.tsx"
import DataCaches from "../api/DataCaches.ts"
@@ -58,6 +59,7 @@ export default function AppMobile() {
})
const addContactDialogRef = React.useRef<Dialog>(null)
const createGroupDialogRef = React.useRef<Dialog>(null)
const chatInfoDialogRef = React.useRef<Dialog>(null)
const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat)
@@ -177,6 +179,9 @@ export default function AppMobile() {
<AddContactDialog
addContactDialogRef={addContactDialogRef} />
<CreateGroupDialog
createGroupDialogRef={createGroupDialogRef} />
<mdui-top-app-bar style={{
position: 'sticky',
marginTop: '3px',
@@ -221,6 +226,7 @@ export default function AppMobile() {
<ContactsList
openChatInfoDialog={openChatInfoDialog}
addContactDialogRef={addContactDialogRef as any}
createGroupDialogRef={createGroupDialogRef}
display={navigationItemSelected == "Contacts"} />
}
</div>

View File

@@ -15,11 +15,11 @@ interface Refs {
export default function AddContactDialog({
addContactDialogRef,
}: Refs) {
const inputUserAccountRef = React.useRef<TextField>(null)
const inputTargetRef = React.useRef<TextField>(null)
async function addContact() {
const re = await Client.invoke("User.addContact", {
account: inputUserAccountRef.current!.value,
target: inputTargetRef.current!.value,
token: data.access_token,
})
@@ -30,14 +30,13 @@ export default function AddContactDialog({
})
EventBus.emit('ContactsList.updateContacts')
inputUserAccountRef.current!.value = ''
inputTargetRef.current!.value = ''
addContactDialogRef.current!.open = false
}
return (
<mdui-dialog close-on-overlay-click close-on-esc headline="添加對話" ref={addContactDialogRef}>
, ...
<mdui-text-field style={{ marginTop: "10px", }} clearable label="對方的 用戶 ID / 用戶名" ref={inputUserAccountRef as any} onKeyDown={(event) => {
<mdui-text-field clearable label="对话 ID / 用戶 ID / 用戶名" ref={inputTargetRef as any} onKeyDown={(event) => {
if (event.key == 'Enter')
addContact()
}}></mdui-text-field>

View File

@@ -0,0 +1,54 @@
import * as React from 'react'
import { Button, Dialog, TextField } from "mdui"
import useEventListener from "../useEventListener.ts"
import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts"
import Client from "../../api/Client.ts"
import * as CryptoJS from 'crypto-js'
import data from "../../Data.ts"
import EventBus from "../../EventBus.ts"
interface Refs {
createGroupDialogRef: React.MutableRefObject<Dialog | null>
}
export default function CreateGroupDialog({
createGroupDialogRef,
}: Refs) {
const inputGroupNameRef = React.useRef<TextField>(null)
const inputGroupIdRef = React.useRef<TextField>(null)
async function createGroup() {
const re = await Client.invoke("Chat.createGroup", {
title: inputGroupNameRef.current!.value,
id: inputGroupIdRef.current!.value,
token: data.access_token,
})
if (checkApiSuccessOrSncakbar(re, "添加失敗")) return
snackbar({
message: "创建成功!",
placement: "top",
})
EventBus.emit('ContactsList.updateContacts')
inputGroupNameRef.current!.value = ''
inputGroupIdRef.current!.value = ''
createGroupDialogRef.current!.open = false
}
return (
<mdui-dialog close-on-overlay-click close-on-esc headline="创建群组" ref={createGroupDialogRef}>
<mdui-text-field clearable label="群组名称" ref={inputGroupNameRef as any} onKeyDown={(event) => {
if (event.key == 'Enter')
inputGroupIdRef.current!.click()
}}></mdui-text-field>
<mdui-text-field style={{ marginTop: "10px", }} clearable label="群组 ID (可选)" ref={inputGroupIdRef as any} onKeyDown={(event) => {
if (event.key == 'Enter')
createGroup()
}}></mdui-text-field>
<mdui-button slot="action" variant="text" onClick={() => createGroupDialogRef.current!.open = false}></mdui-button>
<mdui-button slot="action" variant="text" onClick={() => createGroup()}></mdui-button>
</mdui-dialog>
)
}

View File

@@ -13,12 +13,14 @@ interface Args extends React.HTMLAttributes<HTMLElement> {
display: boolean
openChatInfoDialog: (chat: Chat) => void
addContactDialogRef: React.MutableRefObject<Dialog>
createGroupDialogRef: React.MutableRefObject<Dialog>
}
export default function ContactsList({
display,
openChatInfoDialog,
addContactDialogRef,
createGroupDialogRef,
...props
}: Args) {
const searchRef = React.useRef<HTMLElement>(null)
@@ -62,6 +64,10 @@ export default function ContactsList({
}} icon="person_add" onClick={() => addContactDialogRef.current!.open = true}></mdui-list-item>
<mdui-list-item rounded style={{
width: '100%',
}} icon="group_add" onClick={() => createGroupDialogRef.current!.open = true}></mdui-list-item>
<mdui-list-item rounded style={{
width: '100%',
marginTop: '13px',
marginBottom: '15px',
}} icon="refresh" onClick={() => EventBus.emit('ContactsList.updateContacts')}></mdui-list-item>
{/* <mdui-list-item rounded style={{

View File

@@ -7,7 +7,8 @@ import UserChatLinker from "../data/UserChatLinker.ts"
import ApiManager from "./ApiManager.ts"
import BaseApi from "./BaseApi.ts"
import TokenManager from "./TokenManager.ts"
import ChatPrivate from "../data/ChatPrivate.ts";
import ChatPrivate from "../data/ChatPrivate.ts"
import ChatGroup from "../data/ChatGroup.ts"
export default class ChatApi extends BaseApi {
override getName(): string {
@@ -56,6 +57,18 @@ export default class ChatApi extends BaseApi {
}
}
}
if (chat!.bean.type == 'group') {
return {
code: 200,
msg: "成功",
data: {
id: args.target as string,
type: chat.bean.type,
title: chat.getTitle(),
avatar: chat.getAvatarFileHash() ? "uploaded_files/" + chat.getAvatarFileHash() : undefined
}
}
}
return {
code: 501,
@@ -230,6 +243,47 @@ export default class ChatApi extends BaseApi {
}
}
})
/**
* 创建群组
* @param token 令牌
* @param title 名称
* @param [id] 群组 ID
*/
this.registerEvent("Chat.createGroup", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'title'])) return {
msg: "參數缺失",
code: 400,
}
if (this.checkArgsEmpty(args, ['title'])) 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(token.author) as User
const haveId = args.id && (args.id as string) != ''
if (haveId && Chat.findById(args.id as string) != null) return
const chat = ChatGroup.createGroup(haveId ? undefined : args.id as string)
chat.setTitle(args.title as string)
chat.addMembers([
user.bean.id,
])
user.addContact(chat.bean.id)
return {
code: 200,
msg: '成功',
data: {
chat_id: chat.bean.id,
}
}
})
/**
* 从私聊获取对方的 UserId
* @param token 令牌

View File

@@ -102,7 +102,7 @@ export default class UserApi extends BaseApi {
const nickname: string = args.nickname as string
const password: string = args.password as string
const user = User.createWithUserNameChecked(username, password, nickname, null)
const user = User.create(username, password, nickname, null)
return {
msg: "成功",
@@ -251,6 +251,7 @@ export default class UserApi extends BaseApi {
const chat = Chat.findById(id)
return {
id,
type: chat.bean.type,
title: chat?.getTitle(user) || "未知",
avatar: chat?.getAvatarFileHash(user) ? "uploaded_files/" + chat?.getAvatarFileHash(user) : undefined
}
@@ -260,7 +261,7 @@ export default class UserApi extends BaseApi {
})
// 添加聯絡人
this.registerEvent("User.addContact", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token']) || (args.chat_id == null && args.account == null)) return {
if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失",
code: 400,
}
@@ -272,18 +273,18 @@ export default class UserApi extends BaseApi {
}
const user = User.findById(token.author) as User
if (args.chat_id)
user!.addContact(args.chat_id as string)
else if (args.account) {
const targetUser = User.findByAccount(args.account as string) as User
if (targetUser == null) {
return {
msg: "找不到用戶",
code: 404,
}
}
const chat = ChatPrivate.findOrCreateForPrivate(user, targetUser)
const chat = Chat.findById(args.target as string)
const targetUser = User.findByAccount(args.target as string) as User
if (chat)
user!.addContact(chat.bean.id)
else if (targetUser) {
const privChat = ChatPrivate.findOrCreateForPrivate(user, targetUser)
user!.addContact(privChat.bean.id)
} else {
return {
msg: "找不到目标",
code: 404,
}
}
return {

View File

@@ -9,6 +9,7 @@ import chalk from "chalk"
import User from "./User.ts"
import ChatType from "./ChatType.ts"
import UserChatLinker from "./UserChatLinker.ts"
import DataWrongError from '../api/DataWrongError.ts'
/**
* Chat.ts - Wrapper and manager
@@ -48,6 +49,8 @@ export default class Chat {
}
static create(chatId: string, type: ChatType) {
if (this.findAllBeansByCondition('id = ?', chatId).length > 0)
throw new DataWrongError(`对话ID ${chatId} 已被使用`)
const chat = new Chat(
Chat.findAllBeansByCondition(
'count = ?',
@@ -101,6 +104,11 @@ export default class Chat {
return null
}
setTitle(title: string) {
if (this.bean.type == 'private')
throw new Error('不允许对私聊进行命名')
this.setAttr('title', title)
}
getTitle(userMySelf?: User) {
if (this.bean.type == 'group') return this.bean.title
if (this.bean.type == 'private') return this.getAnotherUserForPrivate(userMySelf as User)?.getNickName()

View File

@@ -10,11 +10,11 @@ import UserBean from './UserBean.ts'
import FileManager from './FileManager.ts'
import { SQLInputValue } from "node:sqlite"
import DataWrongError from "../api/DataWrongError.ts"
import ChatPrivate from "./ChatPrivate.ts"
import Chat from "./Chat.ts"
import ChatBean from "./ChatBean.ts"
import MapJson from "../MapJson.ts";
import MapJson from "../MapJson.ts"
import DataWrongError from '../api/DataWrongError.ts'
type UserBeanKey = keyof UserBean
@@ -45,18 +45,9 @@ export default class User {
return db
}
static createWithUserNameChecked(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
static create(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
if (userName && User.findAllBeansByCondition('username = ?', userName).length > 0)
throw new DataWrongError(`用户名 ${userName} 已存在`)
return User.create(
userName,
password,
nickName,
avatar
)
}
static create(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
const user = new User(
User.findAllBeansByCondition(
'count = ?',
@@ -124,6 +115,8 @@ export default class User {
return this.bean.username
}
setUserName(userName: string) {
if (User.findAllBeansByCondition('username = ?', userName).length > 0)
throw new DataWrongError(`用户名 ${userName} 已存在`)
this.setAttr("username", userName)
}
updateRecentChat(chatId: string, content: string) {

View File

@@ -15,6 +15,7 @@ import TokenManager from "./api/TokenManager.ts"
import UserChatLinker from "./data/UserChatLinker.ts"
import path from "node:path"
import cookieParser from 'cookie-parser'
import fs from 'node:fs/promises'
const app = express()
app.use('/', express.static(config.data_path + '/page_compiled'))
@@ -53,7 +54,11 @@ app.get('/uploaded_files/:hash', (req, res) => {
const httpServer: HttpServerLike = (
((config.server.use == 'http') && http.createServer(app)) ||
((config.server.use == 'https') && https.createServer(config.server.https, app)) ||
((config.server.use == 'https') && https.createServer({
...config.server.https,
key: await fs.readFile(config.server.https.key),
cert: await fs.readFile(config.server.https.cert),
}, app)) ||
http.createServer(app)
)
const io = new SocketIo.Server(httpServer, {