Compare commits

...

12 Commits

Author SHA1 Message Date
CrescentLeaf
d10b3cde72 chore: 添加 CryptoJS 的類型提示 2025-08-31 13:20:07 +08:00
CrescentLeaf
a316eef807 feat(wip): 前后端通訊基礎類 2025-08-31 13:19:49 +08:00
CrescentLeaf
f48b6567cd feat: 前端加密的 kv 存儲 2025-08-31 13:19:30 +08:00
CrescentLeaf
3c7d7e6b29 feat: 控制臺快捷指令 2025-08-31 13:19:15 +08:00
CrescentLeaf
7bbdc25ac0 chore: 使 js 編譯組件綫性工作, 並改爲 async 方法 2025-08-31 13:18:51 +08:00
CrescentLeaf
7300c091fd ui: 修正消息發送框的位置, 修正側邊列表的顯示, 修正 MesageContainer 的 marginBottom 為 20px 2025-08-31 13:17:45 +08:00
CrescentLeaf
f7bd8bdd36 chore: remove useless code 2025-08-31 10:37:35 +08:00
CrescentLeaf
ffb8c555c5 Merge branch 'main' of codeberg.org:CrescentLeaf/TheWhiteSilk 2025-08-31 10:35:43 +08:00
CrescentLeaf
3e1f9c055d ui: 聯絡人列表可摺叠, 添加 "加載更多" 按鈕 2025-08-31 10:35:29 +08:00
CrescentLeaf
7a9a9d628a ui: MessageContainer 默認 marginBottom 2025-08-31 10:34:59 +08:00
CrescentLeaf
70af48db03 chore: 移除 node_modules 的 ignore
(因為壓根不會被創建了)
2025-08-31 08:46:00 +08:00
CrescentLeaf
ac040f20c6 fix: add $ to regexp 2025-08-31 08:31:12 +08:00
8 changed files with 227 additions and 141 deletions

1
.gitignore vendored
View File

@@ -4,4 +4,3 @@ thewhitesilk_config.json
thewhitesilk_data/
deno.lock
node_modules/

43
client/Data.ts Normal file
View File

@@ -0,0 +1,43 @@
import CryptoJS from "./types/CryptoJS.d.ts"
const dataIsEmpty = !localStorage.tws_data || localStorage.tws_data == ''
const aes = {
enc: (m: string, k: string) => CryptoJS.AES.encrypt(m, k).toString(),
dec: (m: string, k: string) => CryptoJS.AES.decrypt(m, k).toString(CryptoJS.enc.Utf8),
}
const key = location.host + '_TWS_姐姐'
if (dataIsEmpty) localStorage.tws_data = aes.enc('{}', key)
let _dec = aes.dec(localStorage.tws_data, key)
if (_dec == '') _dec = '{}'
const _data_cached = JSON.parse(_dec)
// 類型定義
declare global {
interface Window {
data: {
apply: () => void
}
}
}
// deno-lint-ignore no-window
(window.data == null) && (window.data = new Proxy({
apply() {
localStorage.tws_data = aes.enc(JSON.stringify(_data_cached), key)
}
}, {
get(_obj, k) {
return _data_cached[k]
},
set(_obj, k, v) {
_data_cached[k] = v
return true
},
}))
// deno-lint-ignore no-window
export default window.data

11
client/api/Client.ts Normal file
View File

@@ -0,0 +1,11 @@
import { io, Socket } from 'https://unpkg.com/socket.io-client@4.8.1/dist/socket.io.esm.min.js'
class Client {
static socket: Socket
static connect() {
this.socket && this.socket.disconnect()
this.socket = io()
}
}
export default Client

113
client/types/CryptoJS.d.ts vendored Normal file
View File

@@ -0,0 +1,113 @@
// https://github.com/nozzlegear/crypto-js.d.ts/blob/master/crypto-js.d.ts
// Forked from https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/9143f1233f13692c14ae2afe7aee2d5014cba916/crypto-js/crypto-js.d.ts
declare namespace CryptoJS {
type Hash = (message: string, key?: string, ...options: any[]) => string;
interface Cipher {
encrypt(message: string, secretPassphrase: string, option?: CipherOption): WordArray;
decrypt(encryptedMessage: string | WordArray, secretPassphrase: string, option?: CipherOption): DecryptedMessage;
}
interface CipherAlgorythm {
createEncryptor(secretPassphrase: string, option?: CipherOption): Encriptor;
createDecryptor(secretPassphrase: string, option?: CipherOption): Decryptor;
}
interface Encriptor {
process(messagePart: string): string;
finalize(): string;
}
interface Decryptor {
process(messagePart: string): string;
finalize(): string;
}
interface WordArray {
iv: string;
salt: string;
ciphertext: string;
key?: string;
}
type DecryptedMessage = {
toString(encoder?: Encoder): string;
};
interface CipherOption {
iv?: string;
mode?: Mode;
padding?: Padding;
[option: string]: any;
}
interface Encoder {
parse(encodedMessage: string): any;
stringify(words: any): string;
}
interface Mode {}
interface Padding {}
interface Hashes {
MD5: Hash;
SHA1: Hash;
SHA256: Hash;
SHA224: Hash;
SHA512: Hash;
SHA384: Hash;
SHA3: Hash;
RIPEMD160: Hash;
HmacMD5: Hash;
HmacSHA1: Hash;
HmacSHA256: Hash;
HmacSHA224: Hash;
HmacSHA512: Hash;
HmacSHA384: Hash;
HmacSHA3: Hash;
HmacRIPEMD160: Hash;
PBKDF2: Hash;
AES: Cipher;
DES: Cipher;
TripleDES: Cipher;
RC4: Cipher;
RC4Drop: Cipher;
Rabbit: Cipher;
RabbitLegacy: Cipher;
EvpKDF: Cipher;
algo: {
AES: CipherAlgorythm;
DES: CipherAlgorythm;
TrippleDES: CipherAlgorythm;
RC4: CipherAlgorythm;
RC4Drop: CipherAlgorythm;
Rabbit: CipherAlgorythm;
RabbitLegacy: CipherAlgorythm;
EvpKDF: CipherAlgorythm;
};
format: {
OpenSSL: any;
Hex: any;
};
enc: {
Latin1: Encoder;
Utf8: Encoder;
Hex: Encoder;
Utf16: Encoder;
Utf16LE: Encoder;
Base64: Encoder;
};
mode: {
CFB: Mode;
CTR: Mode;
CTRGladman: Mode;
OFB: Mode;
ECB: Mode;
};
pad: {
Pkcs7: Padding;
AnsiX923: Padding;
Iso10126: Padding;
Iso97971: Padding;
ZeroPadding: Padding;
NoPadding: Padding;
};
}
}
declare let CryptoJS: CryptoJS.Hashes;
export default CryptoJS

View File

@@ -18,57 +18,9 @@ export default function App() {
nickName: "Maya Fey",
content: "我是绫里真宵, 是一名灵媒师~"
},
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "麻油衣酱",
content: "成步堂君, 我又坐牢了("
},
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "Maya Fey",
content: "我是绫里真宵, 是一名灵媒师~"
},
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "麻油衣酱",
content: "成步堂君, 我又坐牢了("
},
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "Maya Fey",
content: "我是绫里真宵, 是一名灵媒师~"
},
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "麻油衣酱",
content: "成步堂君, 我又坐牢了("
},
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "Maya Fey",
content: "我是绫里真宵, 是一名灵媒师~"
},
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "麻油衣酱",
content: "成步堂君, 我又坐牢了("
},
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "Maya Fey",
content: "我是绫里真宵, 是一名灵媒师~"
},
])
const [contactsMap, setContactsMap] = React.useState({
默认分组: [
所有: [
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
@@ -78,19 +30,7 @@ export default function App() {
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "Maya Fey",
}
],
测试分组114514: [
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "麻油衣酱",
},
{
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "Maya Fey",
}
],
})
const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents')
@@ -125,15 +65,16 @@ export default function App() {
}
{
// 最近聊天
(navigationItemSelected == "Recents") &&
<mdui-list style={{
width: "35%",
overflowY: 'auto',
paddingRight: '10px'
paddingRight: '10px',
display: navigationItemSelected == "Recents" ? null : 'none'
}}>
{
recentsList.map((v) =>
<RecentsListItem
key={v.userId}
nickName={v.nickName}
avatar={v.avatar}
content={v.content} />
@@ -143,26 +84,29 @@ export default function App() {
}
{
// 联系人列表
(navigationItemSelected == "Contacts") &&
<mdui-list style={{
width: "35%",
overflowY: 'auto',
paddingRight: '10px'
paddingRight: '10px',
display: navigationItemSelected == "Contacts" ? null : 'none'
}}>
{
Object.keys(contactsMap).map((v) =>
<>
<mdui-list-subheader>{v}</mdui-list-subheader>
{
contactsMap[v].map((v2) =>
<ContactsListItem
nickName={v2.nickName}
avatar={v2.avatar} />
)
}
</>
)
}
<mdui-collapse accordion value={Object.keys(contactsMap)[0]}>
{
Object.keys(contactsMap).map((v) =>
<mdui-collapse-item key={v} value={v}>
<mdui-list-subheader slot="header">{v}</mdui-list-subheader>
{
contactsMap[v].map((v2) =>
<ContactsListItem
key={v2.userId}
nickName={v2.nickName}
avatar={v2.avatar} />
)
}
</mdui-collapse-item>
)
}
</mdui-collapse>
</mdui-list>
}
{
@@ -192,55 +136,23 @@ export default function App() {
<mdui-top-app-bar-title>Title</mdui-top-app-bar-title>
<mdui-button-icon icon="more_vert"></mdui-button-icon>
</mdui-top-app-bar>
<div>
<MessageContainer style={{
height: '100%',
paddingBottom: '20px',
<div style={{
display: "flex",
flexDirection: "column",
height: "100%",
}}>
<div style={{
display: "flex",
justifyContent: "center",
}}>
<mdui-button variant="text">加載更多</mdui-button>
</div>
<MessageContainer>
<Message
nickName="Fey"
avatar="https://www.court-records.net/mugshot/aa6-004-maya.png">
Test
</Message>
<Message
direction="right"
nickName="Fey"
avatar="https://www.court-records.net/mugshot/aa6-004-maya.png">
Test
</Message>
<Message
nickName="Fey"
avatar="https://www.court-records.net/mugshot/aa6-004-maya.png">
Test
</Message>
<Message
direction="right"
nickName="Fey"
avatar="https://www.court-records.net/mugshot/aa6-004-maya.png">
Test
</Message>
<Message
nickName="Fey"
avatar="https://www.court-records.net/mugshot/aa6-004-maya.png">
Test
</Message>
<Message
direction="right"
nickName="Fey"
avatar="https://www.court-records.net/mugshot/aa6-004-maya.png">
Test
</Message>
<Message
nickName="Fey"
avatar="https://www.court-records.net/mugshot/aa6-004-maya.png">
Test
</Message>
<Message
direction="right"
nickName="Fey"
avatar="https://www.court-records.net/mugshot/aa6-004-maya.png">
Test
</Message>
</MessageContainer>
{
// 输入框
@@ -265,17 +177,6 @@ export default function App() {
marginRight: '7px',
}}></mdui-button-icon>
</div>
{/* <mdui-top-app-bar style={{
position: 'sticky',
bottom: '0',
}}>
<mdui-button-icon icon="menu"></mdui-button-icon>
<mdui-top-app-bar-title>Title</mdui-top-app-bar-title>
<div style={{
flexGrow: 1,
}}></div>
<mdui-button-icon icon="more_vert"></mdui-button-icon>
</mdui-top-app-bar> */}
</div>
</div>
</div>

View File

@@ -9,6 +9,8 @@ export default function MessageContainer({ children, style, ...props } = {}) {
flexDirection: 'column',
justifyContent: 'flex-end',
alignItems: 'center',
marginBottom: '20px',
height: "100%",
...style,
}}
{...props}>

View File

@@ -24,13 +24,14 @@ async function compileJs(path: string) {
console.log(`编译: ${path}`)
}
export default function(source: string, output: string) {
export default async function(source: string, output: string) {
const t = Date.now()
io.copyDir(source, output)
io.listFiles(output, {
for (const v of io.listFiles(output, {
recursive: true,
fullPath: true,
}).forEach(function (v) {
}))
if (/\.(t|j)sx?$/.test(v))
compileJs(v)
})
await compileJs(v)
return (Date.now() - t) / 1000
}

View File

@@ -6,12 +6,15 @@ import HttpServerLike from "./types/HttpServerLike.ts"
import config from './config.ts'
import http from 'node:http'
import https from 'node:https'
import readline from 'node:readline'
import process from "node:process"
import transform from './compiler/transform.ts'
import chalk from "chalk"
const app = express()
app.use((req, res, next) => {
const url = req.originalUrl || req.url
if (/\.(j|t)sx?/.test(url))
if (/\.(j|t)sx?$/.test(url))
res.setHeader('Content-Type', 'application/javascript')
next()
})
@@ -31,5 +34,18 @@ ApiManager.initEvents()
ApiManager.initAllApis()
httpServer.listen(config.server.listen)
console.log(chalk.green("API & Web 服務已經開始運作"))
transform('./client', config.data_path + '/page_compiled')
console.log(chalk.green("Web 頁面已編譯完成, 用時 " + await transform('./client', config.data_path + '/page_compiled') + "s"))
console.log(chalk.yellow("===== TheWhiteSilk Server ====="))
console.log(chalk.yellow("b - 重新編譯 Web 頁面"))
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.on('line', async (text) => {
if (text == "b")
console.log(chalk.green("Web 頁面已編譯完成, 用時 " + await transform('./client', config.data_path + '/page_compiled') + "s"))
})