Compare commits
8 Commits
fabdd192dd
...
38c28c3fb6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38c28c3fb6 | ||
|
|
0df1149618 | ||
|
|
aeafcb5b97 | ||
|
|
324962b0fc | ||
|
|
f5f3774daf | ||
|
|
e666dc573a | ||
|
|
11362a5689 | ||
|
|
7c7e641d1f |
@@ -21,6 +21,7 @@ const _data_cached = JSON.parse(_dec)
|
||||
declare global {
|
||||
interface Window {
|
||||
data: {
|
||||
refresh_token?: string
|
||||
split_sizes: number[]
|
||||
apply(): void
|
||||
access_token?: string
|
||||
@@ -29,6 +30,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore: 忽略...
|
||||
// deno-lint-ignore no-window
|
||||
(window.data == null) && (window.data = new Proxy({
|
||||
apply() {}
|
||||
|
||||
@@ -2,6 +2,7 @@ export type CallMethod =
|
||||
"User.auth" |
|
||||
"User.register" |
|
||||
"User.login" |
|
||||
"User.refreshAccessToken" |
|
||||
|
||||
"User.setAvatar" |
|
||||
"User.updateProfile" |
|
||||
@@ -17,6 +18,10 @@ export type CallMethod =
|
||||
|
||||
"Chat.getInfo" |
|
||||
|
||||
"Chat.updateSettings" |
|
||||
|
||||
"Chat.createGroup" |
|
||||
|
||||
"Chat.getIdForPrivate" |
|
||||
"Chat.getAnotherUserIdFromPrivate" |
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class Client {
|
||||
})
|
||||
this.socket!.on("connect", async () => {
|
||||
this.connected = true
|
||||
const re = await this.auth(data.access_token)
|
||||
const re = await this.auth(data.access_token as string)
|
||||
if (re.code != 200)
|
||||
checkApiSuccessOrSncakbar(re, "重连失败")
|
||||
})
|
||||
@@ -42,7 +42,7 @@ class Client {
|
||||
}
|
||||
})
|
||||
}
|
||||
static invoke(method: CallMethod, args: unknown = {}, timeout: number = 5000, refreshAndRetryLimit: number = 3, forceRefreshAndRetry: boolean = false): Promise<ApiCallbackMessage> {
|
||||
static invoke(method: CallMethod, args: object = {}, timeout: number = 5000, refreshAndRetryLimit: number = 3, forceRefreshAndRetry: boolean = false): Promise<ApiCallbackMessage> {
|
||||
// 在 未初始化 / 未建立连接且调用非可调用接口 的时候进行延迟
|
||||
if (this.socket == null || (!this.connected && !CallableMethodBeforeAuth.includes(method))) {
|
||||
return new Promise((reslove) => {
|
||||
@@ -87,7 +87,7 @@ class Client {
|
||||
const re = await this.invoke("User.refreshAccessToken", {
|
||||
refresh_token: data.refresh_token
|
||||
})
|
||||
return re.data?.access_token
|
||||
return re.data?.access_token as string
|
||||
}
|
||||
static async auth(token: string, timeout: number = 5000) {
|
||||
const re = await this.invoke("User.auth", {
|
||||
|
||||
@@ -5,6 +5,7 @@ export default class Chat {
|
||||
declare id: string
|
||||
declare title: string
|
||||
declare avatar?: string
|
||||
declare settings?: { [key: string]: unknown }
|
||||
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
10
client/api/client_data/GroupSettings.ts
Normal file
10
client/api/client_data/GroupSettings.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
interface GroupSettings {
|
||||
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
|
||||
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export default GroupSettings
|
||||
@@ -19,8 +19,6 @@
|
||||
"@types/react": "npm:@types/react@18.3.1",
|
||||
"@types/react-dom": "npm:@types/react-dom@18.3.1",
|
||||
"@vitejs/plugin-react": "npm:@vitejs/plugin-react@4.7.0",
|
||||
"vite-plugin-babel": "npm:vite-plugin-babel@1.3.2",
|
||||
"@babel/preset-env": "npm:@babel/preset-env@7.28.3",
|
||||
"react": "npm:react@18.3.1",
|
||||
"react-dom": "npm:react-dom@18.3.1",
|
||||
"vite": "npm:vite@7.0.6",
|
||||
|
||||
@@ -24,6 +24,7 @@ import SwitchPreference from '../preference/SwitchPreference.tsx'
|
||||
import SelectPreference from '../preference/SelectPreference.tsx'
|
||||
import TextFieldPreference from '../preference/TextFieldPreference.tsx'
|
||||
import Preference from '../preference/Preference.tsx'
|
||||
import GroupSettings from "../../api/client_data/GroupSettings.ts"
|
||||
|
||||
interface Args extends React.HTMLAttributes<HTMLElement> {
|
||||
target: string
|
||||
@@ -64,6 +65,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
} as Chat)
|
||||
|
||||
const [tabItemSelected, setTabItemSelected] = React.useState('None')
|
||||
const [groupPreferenceDefaultValue, setGroupPreferenceDefaultValue] = React.useState<GroupSettings>({})
|
||||
const tabRef = React.useRef<Tab>(null)
|
||||
const chatPanelRef = React.useRef<HTMLElement>(null)
|
||||
useEventListener(tabRef, 'change', () => {
|
||||
@@ -82,6 +84,11 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
await loadMore()
|
||||
|
||||
setTabItemSelected("Chat")
|
||||
if (re.data!.type == 'group') {
|
||||
setGroupPreferenceDefaultValue(re.data!.settings as GroupSettings)
|
||||
groupPreferenceStore.setter(re.data!.settings as GroupSettings)
|
||||
console.log(re.data!.settings as GroupSettings)
|
||||
}
|
||||
setTimeout(() => {
|
||||
chatPanelRef.current!.scrollTo({
|
||||
top: 10000000000,
|
||||
@@ -211,7 +218,20 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
}
|
||||
})
|
||||
|
||||
const groupPreferenceStore = new PreferenceStore()
|
||||
const groupPreferenceStore = new PreferenceStore<GroupSettings>()
|
||||
groupPreferenceStore.setOnUpdate(async (value) => {
|
||||
const re = await Client.invoke("Chat.updateSettings", {
|
||||
token: data.access_token,
|
||||
target,
|
||||
settings: value,
|
||||
})
|
||||
if (checkApiSuccessOrSncakbar(re, "更新设定失败")) return
|
||||
|
||||
setChatInfo(JSON.parse(JSON.stringify({
|
||||
...chatInfo,
|
||||
settings: value as object,
|
||||
})))
|
||||
})
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
@@ -448,14 +468,14 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
<SwitchPreference
|
||||
title="允许入群"
|
||||
icon="person_add"
|
||||
defaultState={false}
|
||||
defaultState={groupPreferenceDefaultValue.allow_new_member_join || false}
|
||||
updater={groupPreferenceStore.updater('allow_new_member_join')} />
|
||||
<SwitchPreference
|
||||
title="允许成员邀请"
|
||||
description="目前压根没有这项功能, 甚至还不能查看成员列表, 以后再说吧"
|
||||
icon="_"
|
||||
disabled={true}
|
||||
defaultState={false}
|
||||
defaultState={groupPreferenceDefaultValue.allow_new_member_from_invitation || false}
|
||||
updater={groupPreferenceStore.updater('allow_new_member_from_invitation')} />
|
||||
<SelectPreference
|
||||
title="入群验证方式"
|
||||
@@ -467,14 +487,14 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
||||
}}
|
||||
disabled={!groupPreferenceStore.value.allow_new_member_join}
|
||||
updater={groupPreferenceStore.updater('new_member_join_method')}
|
||||
defaultCheckedId="disabled" />
|
||||
defaultCheckedId={groupPreferenceDefaultValue.new_member_join_method || 'disabled'} />
|
||||
{
|
||||
groupPreferenceStore.value.new_member_join_method == 'answered_and_allowed_by_admin'
|
||||
&& <TextFieldPreference
|
||||
title="设置问题"
|
||||
icon="_"
|
||||
description="WIP"
|
||||
defaultState=""
|
||||
defaultState={groupPreferenceDefaultValue.answered_and_allowed_by_admin_question || ''}
|
||||
disabled={true}
|
||||
updater={groupPreferenceStore.updater('answered_and_allowed_by_admin_question')} />
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
export default class PreferenceStore {
|
||||
declare value: { [key: string]: unknown }
|
||||
declare setter: React.Dispatch<React.SetStateAction<{ [key: string]: unknown }>>
|
||||
export default class PreferenceStore<T extends object> {
|
||||
declare value: T
|
||||
declare setter: React.Dispatch<React.SetStateAction<T>>
|
||||
declare onUpdate: (value: unknown) => void
|
||||
constructor() {
|
||||
const _ = React.useState<{ [key: string]: unknown }>({})
|
||||
const _ = React.useState<T>({} as T)
|
||||
this.value = _[0]
|
||||
this.setter = _[1]
|
||||
}
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import deno from '@deno/vite-plugin'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import pluginBabel from 'vite-plugin-babel'
|
||||
import config from '../server/config.ts'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), pluginBabel({
|
||||
babelConfig: {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
android: '70'
|
||||
},
|
||||
}
|
||||
],
|
||||
],
|
||||
}
|
||||
}), deno()],
|
||||
plugins: [react(), deno()],
|
||||
build: {
|
||||
cssTarget: 'chrome70',
|
||||
sourcemap: true,
|
||||
outDir: "." + config.data_path + '/page_compiled',
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ export type CallMethod =
|
||||
"User.auth" |
|
||||
"User.register" |
|
||||
"User.login" |
|
||||
"User.refreshAccessToken" |
|
||||
|
||||
"User.setAvatar" |
|
||||
"User.updateProfile" |
|
||||
@@ -17,6 +18,10 @@ export type CallMethod =
|
||||
|
||||
"Chat.getInfo" |
|
||||
|
||||
"Chat.updateSettings" |
|
||||
|
||||
"Chat.createGroup" |
|
||||
|
||||
"Chat.getIdForPrivate" |
|
||||
"Chat.getAnotherUserIdFromPrivate" |
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import BaseApi from "./BaseApi.ts"
|
||||
import TokenManager from "./TokenManager.ts"
|
||||
import ChatPrivate from "../data/ChatPrivate.ts"
|
||||
import ChatGroup from "../data/ChatGroup.ts"
|
||||
import GroupSettingsBean from "../data/GroupSettingsBean.ts"
|
||||
|
||||
export default class ChatApi extends BaseApi {
|
||||
override getName(): string {
|
||||
@@ -53,7 +54,8 @@ export default class ChatApi extends BaseApi {
|
||||
id: args.target as string,
|
||||
type: chat.bean.type,
|
||||
title: chat.getTitle(mine),
|
||||
avatar: chat.getAvatarFileHash(mine) ? "uploaded_files/" + chat.getAvatarFileHash(mine) : undefined
|
||||
avatar: chat.getAvatarFileHash(mine) ? "uploaded_files/" + chat.getAvatarFileHash(mine) : undefined,
|
||||
settings: JSON.parse(chat.bean.settings),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +67,8 @@ export default class ChatApi extends BaseApi {
|
||||
id: args.target as string,
|
||||
type: chat.bean.type,
|
||||
title: chat.getTitle(),
|
||||
avatar: chat.getAvatarFileHash() ? "uploaded_files/" + chat.getAvatarFileHash() : undefined
|
||||
avatar: chat.getAvatarFileHash() ? "uploaded_files/" + chat.getAvatarFileHash() : undefined,
|
||||
settings: JSON.parse(chat.bean.settings),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,7 +270,10 @@ export default class ChatApi extends BaseApi {
|
||||
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
|
||||
if (haveId && Chat.findById(args.id as string) != null) return {
|
||||
msg: "对话 ID 已被占用",
|
||||
code: 403,
|
||||
}
|
||||
|
||||
const chat = ChatGroup.createGroup(haveId ? undefined : args.id as string)
|
||||
chat.setTitle(args.title as string)
|
||||
@@ -284,6 +290,39 @@ export default class ChatApi extends BaseApi {
|
||||
}
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 更新设定
|
||||
* @param token 令牌
|
||||
* @param title 名称
|
||||
* @param [id] 群组 ID
|
||||
*/
|
||||
this.registerEvent("Chat.updateSettings", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['token', 'target', 'settings'])) 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 chat = Chat.findById(args.target as string)
|
||||
if (chat == null) return {
|
||||
code: 404,
|
||||
msg: "对话不存在",
|
||||
}
|
||||
|
||||
if (chat.bean.type == 'group')
|
||||
ChatGroup.fromChat(chat).getSettings().update(args.settings as GroupSettingsBean)
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
msg: '成功',
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 从私聊获取对方的 UserId
|
||||
* @param token 令牌
|
||||
|
||||
@@ -32,15 +32,15 @@ export default class TokenManager {
|
||||
}
|
||||
}
|
||||
|
||||
static make(user: User, time_: number | null | undefined, device_id: string, type: TokenType = "access_token") {
|
||||
static make(user: User, time_: number | null | undefined, device_id: string, tokenType: TokenType = "access_token") {
|
||||
const time = (time_ || Date.now())
|
||||
return this.encode({
|
||||
author: user.bean.id,
|
||||
auth: this.makeAuth(user),
|
||||
made_time: time,
|
||||
expired_time: time + (type == 'access_token' ? (1000 * 60 * 60 * 2) : (40 * 1000 * 60 * 60 * 24)),
|
||||
expired_time: time + (tokenType == 'access_token' ? (1000 * 60 * 60 * 2) : (40 * 1000 * 60 * 60 * 24)),
|
||||
device_id: device_id,
|
||||
type
|
||||
type: tokenType
|
||||
})
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -295,7 +295,7 @@ export default class UserApi extends BaseApi {
|
||||
const chat = Chat.findById(id)
|
||||
return {
|
||||
id,
|
||||
type: chat.bean.type,
|
||||
type: chat?.bean.type,
|
||||
title: chat?.getTitle(user) || "未知",
|
||||
avatar: chat?.getAvatarFileHash(user) ? "uploaded_files/" + chat?.getAvatarFileHash(user) : undefined
|
||||
}
|
||||
|
||||
6
server/data/AdminPermissions.ts
Normal file
6
server/data/AdminPermissions.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default class AdminPermissions {
|
||||
static readonly OWNER = "OWNER"
|
||||
static readonly MANAGE_ADMIN = "MANAGE_ADMIN"
|
||||
static readonly MUTE_MEMBERS = "MUTE_MEMBERS"
|
||||
static readonly REMOVE_MEMBERS = "REMOVE_MEMBERS"
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import User from "./User.ts"
|
||||
import ChatType from "./ChatType.ts"
|
||||
import UserChatLinker from "./UserChatLinker.ts"
|
||||
import DataWrongError from '../api/DataWrongError.ts'
|
||||
import ChatAdminLinker from "./ChatAdminLinker.ts"
|
||||
|
||||
/**
|
||||
* Chat.ts - Wrapper and manager
|
||||
@@ -80,6 +81,10 @@ export default class Chat {
|
||||
this.bean[key] = value
|
||||
}
|
||||
|
||||
addAdmin(userId: string, permission: string[] | string) {
|
||||
ChatAdminLinker.linkAdminAndChat(userId, this.bean.id)
|
||||
ChatAdminLinker.updatePermissions(userId, this.bean.id, permission instanceof Array ? JSON.stringify(permission) : permission)
|
||||
}
|
||||
getMembersList() {
|
||||
return UserChatLinker.getChatMembers(this.bean.id)
|
||||
}
|
||||
|
||||
49
server/data/ChatAdminLinker.ts
Normal file
49
server/data/ChatAdminLinker.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { DatabaseSync } from "node:sqlite"
|
||||
import path from 'node:path'
|
||||
|
||||
import config from "../config.ts"
|
||||
import { SQLInputValue } from "node:sqlite"
|
||||
export default class ChatAdminLinker {
|
||||
static database: DatabaseSync = this.init()
|
||||
|
||||
private static init(): DatabaseSync {
|
||||
const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, 'ChatAdminLinker.db'))
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ChatAdminLinker (
|
||||
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/* 用戶 ID */ user_id TEXT NOT NULL,
|
||||
/* Chat ID */ chat_id TEXT NOT NULL,
|
||||
/* 管理权限 */ permissions TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
return db
|
||||
}
|
||||
|
||||
static linkAdminAndChat(userId: string, chatId: string) {
|
||||
if (!this.checkAdminIsLinkedToChat(userId, chatId))
|
||||
this.database.prepare(`INSERT INTO ChatAdminLinker (
|
||||
user_id,
|
||||
chat_id,
|
||||
permissions
|
||||
) VALUES (?, ?, ?);`).run(
|
||||
userId,
|
||||
chatId,
|
||||
'[]'
|
||||
)
|
||||
}
|
||||
static updatePermissions(userId: string, chatId: string, permissions: string) {
|
||||
this.database.prepare(`UPDATE ChatAdminLinker SET permissions = ? WHERE user_id = ? AND chat_id = ?`).run(permissions, userId, chatId)
|
||||
}
|
||||
static unlinkAdminAndChat(userId: string, chatId: string) {
|
||||
this.database.prepare(`DELETE FROM ChatAdminLinker WHERE user_id = ? AND chat_id = ?`).run(userId, chatId)
|
||||
}
|
||||
static checkAdminIsLinkedToChat(userId: string, chatId: string) {
|
||||
return this.findAllByCondition('user_id = ? AND chat_id = ?', userId, chatId).length != 0
|
||||
}
|
||||
static getChatAdmins(chatId: string) {
|
||||
return this.findAllByCondition('chat_id = ?', chatId).map((v) => v.user_id) as string[]
|
||||
}
|
||||
protected static findAllByCondition(condition: string, ...args: SQLInputValue[]) {
|
||||
return this.database.prepare(`SELECT * FROM ChatAdminLinker WHERE ${condition}`).all(...args)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,41 @@
|
||||
import chalk from "chalk"
|
||||
import Chat from "./Chat.ts"
|
||||
import User from "./User.ts"
|
||||
import GroupSettingsBean from "./GroupSettingsBean.ts"
|
||||
|
||||
class GroupSettings {
|
||||
declare chat: ChatGroup
|
||||
declare settings: GroupSettingsBean
|
||||
constructor(chat: ChatGroup) {
|
||||
this.chat = chat
|
||||
this.settings = JSON.parse(chat.bean.settings)
|
||||
}
|
||||
|
||||
update(bean: GroupSettingsBean) {
|
||||
const updateValue = (key: string) => {
|
||||
if (key in bean)
|
||||
this.settings[key] = bean[key]
|
||||
}
|
||||
for (const k of [
|
||||
'allow_new_member_join',
|
||||
'allow_new_member_from_invitation',
|
||||
'new_member_join_method',
|
||||
'answered_and_allowed_by_admin_question',
|
||||
])
|
||||
updateValue(k)
|
||||
|
||||
this.apply()
|
||||
}
|
||||
apply() {
|
||||
this.chat.setAttr('settings', JSON.stringify(this.settings))
|
||||
}
|
||||
}
|
||||
|
||||
export default class ChatGroup extends Chat {
|
||||
getSettings() {
|
||||
return new GroupSettings(this)
|
||||
}
|
||||
|
||||
static fromChat(chat: Chat) {
|
||||
return new ChatGroup(chat.bean)
|
||||
}
|
||||
|
||||
10
server/data/GroupSettingsBean.ts
Normal file
10
server/data/GroupSettingsBean.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
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
|
||||
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export default GroupSettingsBean
|
||||
@@ -2,7 +2,7 @@ import { DatabaseSync } from "node:sqlite"
|
||||
import path from 'node:path'
|
||||
|
||||
import config from "../config.ts"
|
||||
import { SQLInputValue } from "node:sqlite";
|
||||
import { SQLInputValue } from "node:sqlite"
|
||||
export default class UserChatLinker {
|
||||
static database: DatabaseSync = this.init()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user