chore: init

This commit is contained in:
MoonLeeeaf
2024-05-10 21:14:50 +08:00
commit a376ead195
29 changed files with 3024 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
### 关于聊天记录储存的方案概述
自 3月10日 开始,这个项目遇到了一些瓶颈,停止开发
我在这段时间的精力也被耗尽
故此,在今天(3月26日)重新思考,理清思路
预计3月28日左右实现此功能
#### 基本想法
##### 写入
对于储存来说,从 0 开始计次,一条消息对应一个 ID
第一条消息的 ID 为 1 计次文件储存为 1
为了性能,每个消息使用一个 JSON 文件储存
##### 读取
当读取时,从计次文件获取计次数,并从此数字递减,以此读取
然后储存到数组并直接返回给用户
客户端消息的 ID 必须要同步,这是为了范围读取以前的消息
#### 和以前的区别
在我的聊天软件第一版、第二版中,均由 PHP 作为后端
写入则直接追加,读取整个返回
这种做法的最大缺陷是浪费性能
我曾想过使用 SQL不过潜在的风险以及我的技术都能符合我的要求
故此,便有了这个方法

42
.github/测试样例/test.html vendored Normal file
View File

@@ -0,0 +1,42 @@
<!doctype html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no"/>
<meta name="renderer" content="webkit"/>
<meta name="force-rendering" content="webkit"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<!-- MDUI CSS -->
<link rel="stylesheet" href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css"/>
<title>铃之椅 API测试</title>
</head>
<body>
<button id="s" class="mdui-btn mdui-btn-raised mdui-ripple mdui-color-theme-accent">send</button>
<div class="mdui-textfield">
<input id="i" class="mdui-textfield-input" type="text" placeholder="API Name"/>
</div>
<div class="mdui-textfield">
<textarea id="sz" class="mdui-textfield-input" placeholder="Args"></textarea>
</div>
<!-- MDUI JavaScript -->
<script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.min.js"></script>
<!-- Socket.IO -->
<script src="https://unpkg.com/socket.io-client@4.7.4/dist/socket.io.min.js"></script>
<script>
const c = io("ws://localhost:3601")
let i = document.getElementById("i")
let sz = document.getElementById("sz")
document.getElementById("s").onclick = () => {
c.emit(i.value,JSON.parse(sz.value), (repo) => {
alert(JSON.stringify(repo))
})
}
// c.onAny((name,...args) => {alert(name + " -> called with -> " + JSON.stringify(args))})
</script>
</body>
</html>

View File

@@ -0,0 +1,68 @@
<!doctype html>
<html lang="zh-cmn-Hans">
<!--
* 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
* Copyright 2024 满月叶
* GitHub: https://github.com/MoonLeeeaf/LingChair-Web-Client
* 本项目使用 Apache 2.0 协议开源
*
* Copyright 2024 MoonLeeeaf
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
-->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no" />
<meta name="renderer" content="webkit" />
<meta name="force-rendering" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<!-- MDUI CSS -->
<link rel="stylesheet" href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css" />
<link rel="stylesheet" href="chat-message.css" />
<script src="https://unpkg.com/jquery@3.7.1/dist/jquery.min.js"></script>
<link rel="icon" href="icon.ico" />
<title>聊天气泡测试</title>
</head>
<body class="mdui-theme-teal mdui-theme-layout-auto mdui-theme-accent-teal">
<!-- 右侧样式 -->
<div class="chat-message">
<div class="message-content-with-nickname-right">
<span class="nickname">小沫</span>
<div class="message-content mdui-card">
<span>你好!这是一条较长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长的消息内容,测试消息长度自动填充效果。</span>
</div>
</div>
<img class="avatar" src="icon.ico" alt="Avatar">
</div>
<!-- 左侧样式 -->
<div class="chat-message">
<img class="avatar" src="icon.ico" alt="Avatar">
<div class="message-content-with-nickname-left">
<span class="nickname">小沫</span>
<div class="message-content mdui-card">
<span>你好!这是一条较长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长的消息内容,测试消息长度自动填充效果。</span>
</div>
</div>
</div>
<!-- MDUI JavaScript -->
<script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.min.js"></script>
<script src="index.js"></script>
</body>
</html>

2
.github/项目创建时间.txt vendored Normal file
View File

@@ -0,0 +1,2 @@
Web客户端: 202412313:15:09
Node.js服务端: 202412414:10:47

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
ling_chair_data/
ling_chair_config/

52
final.md Normal file
View File

@@ -0,0 +1,52 @@
### 最终目标
铃之椅服务端的 Node 实现,目前最低目标如下
* 全 API 可用
* 可配置
* 开箱即用
如果有时间,可以完成下面的目标
* 尽可能使用异步
* 最大利用 I/O 性能
#### API 实现
一般约定: ☘️=已完成
* 用户
* 登录 ☘️
* 注册 ☘️
* 修改密码
* 令牌机制 ☘️
* 用户资料
* 头像 ☘️
* 昵称 ☘️
* 安全
* 账号冻结
* 权限管理
* 匿名
* 聊天
* 双方私聊 ☘️ 完善中
* 多人群聊
* 临时对话
* 多媒体
* 文件
* 群聊资料
* 介绍
* 头像
* 群名称
* 安全
* 权限管理
* 管理员
* 入群权限
* 可否被查找
* 功能限制
* 管理匿名
注:
1. 为了保障管理员层安全,匿名的账号或管理员的信息仍然能被伺服器管理员所查询,不过请放心,一般来说不会有任何正常人能查询到你
2. 多媒体应该能够定时删除而非永久保存,否则伺服器会炸掉的
3. 每个功能都应该验证令牌

13
license.txt Normal file
View File

@@ -0,0 +1,13 @@
Copyright 2024 MoonLeeeaf
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,85 @@
/*
* 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
* Copyright 2024 满月叶
* GitHub: https://github.com/MoonLeeeaf/LingChair-Web-Client
* 本项目使用 Apache 2.0 协议开源
*
* Copyright 2024 MoonLeeeaf
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.chat-message-right {
display: flex;
justify-content: flex-end;
align-items: flex-start;
margin: 13px;
}
.chat-message-left {
display: flex;
justify-content: flex-start;
align-items: flex-start;
margin: 13px;
}
.message-content {
margin-top: 13px;
margin-bottom: 7px;
margin-left: 5px;
margin-right: 5px;
max-width: 100%;
white-space: normal;
word-break: break-all;
font-size: medium;
/* 使用了 CardView 就不需要边框了 */
/* border: 1.3px solid; */
padding: 15px;
border-radius: 15px;
/* 添加圆角样式 */
/* 设置外边距为 7px */
}
.message-content-with-nickname-right {
display: flex;
align-items: center;
margin: 7px;
flex-direction: column;
/* 垂直排列元素 */
align-items: flex-end;
/* 左对齐元素 */
}
.message-content-with-nickname-left {
display: flex;
align-items: center;
margin: 7px;
flex-direction: column;
/* 垂直排列元素 */
align-items: flex-start;
/* 左对齐元素 */
}
.chat-message-left .message-content-with-nickname-left .nickname,
.chat-message-right .message-content-with-nickname-right .nickname {
margin-right: 5px;
font-size: medium;
margin-top: 3px;
}
.chat-message-left > .avatar,
.chat-message-right > .avatar {
width: 45px;
height: 45px;
border-radius: 50%;
}

View File

@@ -0,0 +1,4 @@
{
"appTitle": "",
"canChangeServer": false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
ling_chair_http/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

83
ling_chair_http/index.css Normal file
View File

@@ -0,0 +1,83 @@
/*
* 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
* Copyright 2024 满月叶
* GitHub: https://github.com/MoonLeeeaf/LingChair-Web-Client
* 本项目使用 Apache 2.0 协议开源
*
* Copyright 2024 MoonLeeeaf
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
html, body {
max-height: 100%;
margin: 0;
padding: 0;
/* overflow: hidden; */
/*font: initial;*/
}
body {
margin: 0;
padding: 0;
}
.container {
display: flex;
flex-direction: column;
overflow: auto;
}
.content {
flex: 1;
}
/* 美化 MDUI */
body {
font-family: -apple-system, system-ui, -webkit-system-font;
}
.round {
border-radius: 20px;
}
.mdui-dialog {
border-radius: 23px;
}
.mdui-snackbar {
border-radius: 10px;
}
.mdui-menu {
border-radius: 10px;
}
.mdui-menu-item > a {
padding-right: 3px;
}
.mdui-dialog-actions a,
.mdui-dialog-actions button {
border-radius: 30px;
padding: 15px;
text-align: center;
line-height: 8px;
}
.mdui-dialog-actions {
margin-right: 7px;
margin-top: -7px;
}
/* 配色方案 */
.mdui-theme-color-auto {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
.mdui-theme-color-auto {
background-color: #303030;
}
}

269
ling_chair_http/index.html Normal file
View File

@@ -0,0 +1,269 @@
<!doctype html>
<html lang="zh-cmn-Hans">
<!--
* 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
* Copyright 2024 满月叶
* GitHub: https://github.com/MoonLeeeaf/LingChair-Web-Client
* 本项目使用 Apache 2.0 协议开源
*
* Copyright 2024 MoonLeeeaf
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
-->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no" />
<meta name="renderer" content="webkit" />
<meta name="force-rendering" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<!-- 给老旧的设备提供支持 支持不了, 照样没法运行 -->
<!-- <script src='https://polyfill.io/v3/polyfill.min.js?features=default%2Cdom4%2Ces2015%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2020%2Ces2021%2Ces2022%2Ces5%2Ces6%2Ces7'></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> -->
<!-- 代替私人 fixed 并提供更好的兼容性 -->
<script src="https://cdn.jsdelivr.net/gh/wilddeer/stickyfill@2.1.0/dist/stickyfill.min.js"></script>
<!-- Styles -->
<link rel="stylesheet" href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css" />
<link rel="stylesheet" href="index.css" />
<link rel="stylesheet" href="chat-message.css" />
<script src="https://unpkg.com/jquery@3.7.1/dist/jquery.min.js"></script>
<link rel="icon" href="icon.ico" />
<title>铃之椅</title>
</head>
<body
class="mdui-theme-primary-teal mdui-theme-accent-teal mdui-drawer-body-left mdui-appbar-with-toolbar mdui-theme-layout-auto"
id="app">
<div id="lingchair-app" style="height: 100%;">
<!-- 侧滑栏 -->
<div class="mdui-drawer" id="main-drawer">
<ul class="mdui-list">
<li class="mdui-list-item mdui-ripple">
<div class="mdui-list-item-avatar">
<img src="default_head.png" n-id="userHead" onerror="this.src='default_head.png'" />
</div>
<div class="mdui-list-item-content"><a n-id="helloText">早安</a>, <a n-id="userNick">Unknown</a></div>
</li>
<li class="mdui-subheader">个人</li>
<li class="mdui-list-item mdui-ripple" onclick="new mdui.Dialog(viewBinding.dialogMyProfile.get(0)).open()">
<i class="mdui-list-item-icon mdui-icon material-icons">account_circle</i>
<div class="mdui-list-item-content">资料</div>
</li>
<li class="mdui-subheader">客户端</li>
<li class="mdui-list-item mdui-ripple">
<i class="mdui-list-item-icon mdui-icon material-icons">settings</i>
<div class="mdui-list-item-content">设置</div>
</li>
<li class="mdui-list-item mdui-ripple" n-id="drawerChangeServer">
<i class="mdui-list-item-icon mdui-icon material-icons">cloud_circle</i>
<div class="mdui-list-item-content">更换服务器</div>
</li>
<li class="mdui-list-item mdui-ripple" n-id="drawerSignOut">
<i class="mdui-list-item-icon mdui-icon material-icons">exit_to_app</i>
<div class="mdui-list-item-content">退出登录</div>
</li>
</ul>
</div>
<!-- 应用栏 -->
<header class="mdui-appbar mdui-appbar-fixed">
<!-- Toolbar -->
<div class="mdui-toolbar mdui-color-theme">
<a mdui-drawer="{target: '#main-drawer'}" class="mdui-btn mdui-btn-icon mdui-ripple">
<i class="mdui-icon material-icons">menu</i>
</a>
<a class="mdui-typo-title" n-id="appTitle"></a>
<div class="mdui-toolbar-spacer"></div>
<a onclick="ContactsList.reloadList()" n-id="contactsRefresh" class="mdui-btn mdui-btn-icon mdui-ripple">
<i class="mdui-icon material-icons">refresh</i>
</a>
<a class="mdui-btn mdui-btn-icon mdui-ripple" n-id="switchNotifications" mdui-tooltip="{content:'开/关通知'}">
<i class="mdui-icon material-icons" n-id="switchNotificationsIcon">notifications_off</i>
</a>
<a mdui-menu="{target: '#appbar-menu'}" class="mdui-btn mdui-btn-icon mdui-ripple">
<i class="mdui-icon material-icons">more_vert</i>
</a>
<ul class="mdui-menu" id="appbar-menu">
<li class="mdui-menu-item">
<a class="mdui-ripple" n-id="menuAbout">关于</a>
</li>
</ul>
</div>
</header>
<!-- Tab 栏 -->
<div class="mdui-tab mdui-accent-theme mdui-theme-color-auto" style="position: fixed; z-index: 114;width: 100%;"
mdui-tab>
<!-- 侧滑栏的 z-index 是2000, 在移动端会直接覆盖 -->
<a href="#page-chat-list" n-id="tabChatList" class="mdui-ripple">常用</a>
<a href="#page-contacts" n-id="tabContacts" class="mdui-ripple">通讯录</a>
<a href="#page-chat-seesion" n-id="tabChatSeesion" class="mdui-ripple" style="text-transform: none;"></a>
</div>
<div id="page-chat-list" class="mdui-p-a-2 container">
<div class="mdui-valign content" style="margin-top: 40px;">
<span class="mdui-center">欢迎回来! (^▽^。)</span>
</div>
</div>
<div id="page-contacts" class="mdui-p-a-2">
<ul class="mdui-list" style="margin-top: 30px;">
<li class="mdui-subheader">好友</li>
<div n-id="contactsList">
</div>
</ul>
</div>
<!-- 滚动到底部咋这么难写... -->
<div id="page-chat-seesion" class="mdui-p-a-2" style="display: flex;flex-direction: column;">
<!-- 写时间居中写到吐了 这样式表不能要了 -->
<div
style="margin-top: 30px;overflow: auto;width: 100%;max-width: 100%;height: 100%;max-height: 100%;min-height: 0;margin-bottom: 40px;flex: 1 1 auto;display: flex;flex-direction: column;">
<div class="mdui-center" style="margin: 15px;"><a href="javascript:;" onclick="ChatMsgAdapter.loadMore()"
class="mdui-text-color-theme">点我</a>继续加载前面的聊天记录, 或者<a href="javascript:;"
onclick="ChatMsgAdapter.scrollToBottom()" class="mdui-text-color-theme">回到底部</a></div>
<div n-id="pageChatSeesion" style="flex: 1 1 auto;display: flex;flex-direction: column;"></div>
</div>
<!-- 妈的黑化了 私人玩意这么难整 早知道 z-index 弄死它得了 浪费我时间 我就没试过这么离谱的样式表 第三方库真难写CSS 就应该先写后端的 啊啊啊啊啊啊 -->
<!-- 不黑化了 因为 stickyfill -->
<div class="mdui-toolbar mdui-theme-color-auto"
style="position: sticky;max-width: 100%;margin-bottom: -30px;bottom: 0;">
<ul class="mdui-menu" id="msg-input-more">
<li class="mdui-menu-item">
<a class="mdui-ripple">插入图片</a>
</li>
</ul>
<div class="mdui-textfield" style="width: 100%;max-width: 100%;">
<textarea class="mdui-textfield-input" type="text" placeholder="(。・ω・。)" n-id="inputMsg"></textarea>
</div>
<div class="mdui-toolbar-spacer"></div>
<a n-id="sendMsg" class="mdui-btn mdui-btn-icon mdui-ripple">
<i class="mdui-icon material-icons">send</i>
</a>
<a mdui-menu="{target: '#msg-input-more', position: 'top'}" class="mdui-btn mdui-btn-icon mdui-ripple">
<i class="mdui-icon material-icons">more_vert</i>
</a>
</div>
</div>
<!-- 登录对话框 -->
<div class="mdui-dialog" n-id="dialogSignIn">
<div class="mdui-dialog-title">
登录到 铃之椅
</div>
<div class="mdui-dialog-content" style="margin-left:15px;margin-right:15px;">
<div class="mdui-textfield" n-id="dialogSignInServerLabel">
<i class="mdui-icon material-icons">cloud_circle</i>
<label class="mdui-textfield-label">服务器地址</label>
<input n-id="dialogSignInServer" class="mdui-textfield-input" type="text" placeholder="留空以使用网页所在地址"
n-input-ls="server" />
</div>
<div class="mdui-textfield">
<i class="mdui-icon material-icons">account_circle</i>
<label class="mdui-textfield-label">账号</label>
<input n-id="dialogSignInName" class="mdui-textfield-input" maxlength="25" type="text"
n-input-ls="userName" />
</div>
<div class="mdui-textfield">
<i class="mdui-icon material-icons">lock</i>
<label class="mdui-textfield-label">密码</label>
<input n-id="dialogSignInPasswd" class="mdui-textfield-input" maxlength="30" type="password" />
</div>
<span>注:使用非已知的服务提供商提供的服务器时, 请注意个人信息保护哦 o(。・ω・。)o</span>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple"
onclick="User.signUp(viewBinding.dialogSignInName.val(), viewBinding.dialogSignInPasswd.val(), () => mdui.snackbar('注册成功, 请直接点击登录即可~'))">注册</button>
<button class="mdui-btn mdui-ripple" n-id="dialogSignInEnter"
onclick="User.signInWithDialog(viewBinding.dialogSignInName.val(), viewBinding.dialogSignInPasswd.val())">登录</button>
</div>
</div>
</div>
<!-- 资料卡对话框 -->
<div class="mdui-dialog" n-id="dialogProfile">
<div class="mdui-dialog-content" style="margin-left: 15px; margin-right: 15px; height: 101px;">
<div style="display: flex;justify-content: flex-start">
<img style="width: 60px; height: 60px;" class="mdui-img-circle" n-id="dialogProfileHead">
<div n-id="dialogProfileNick" style="font-size: 22px;align-self: center;margin-left: 20px;"
class="mdui-text-color-white"></div>
</div>
<div style="margin-left: 80px;"></div>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" mdui-dialog-close>关闭</button>
</div>
</div>
<!-- 资料编辑对话框 -->
<div class="mdui-dialog" n-id="dialogMyProfile">
<div class="mdui-dialog-title">
资料
</div>
<div class="mdui-dialog-content" style="margin-left:15px;margin-right:15px;">
<ul class="mdui-list">
<li class="mdui-list-item mdui-ripple" mdui-dialog-close
onclick="(async () => {viewBinding.dialogEditNickNick.val(await NickCache.getNick(localStorage.userName));new mdui.Dialog(viewBinding.dialogEditNick.get(0)).open()})()">
<i class="mdui-list-item-icon mdui-icon material-icons">edit</i>
<div class="mdui-list-item-content">修改昵称</div>
</li>
<li class="mdui-list-item mdui-ripple" onclick="User.uploadHeadImage()">
<i class="mdui-list-item-icon mdui-icon material-icons">account_circle</i>
<div class="mdui-list-item-content">上传头像</div>
</li>
</ul>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" mdui-dialog-close>关闭</button>
</div>
</div>
<!-- 编辑昵称对话框 -->
<div class="mdui-dialog" n-id="dialogEditNick">
<div class="mdui-dialog-title">
修改昵称
</div>
<div class="mdui-dialog-content" style="margin-left:15px;margin-right:15px;">
<div class="mdui-textfield">
<label class="mdui-textfield-label">昵称</label>
<input n-id="dialogEditNickNick" class="mdui-textfield-input" maxlength="30" type="text" />
</div>
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple" n-id="dialogEditNickClose" mdui-dialog-close
onclick="new mdui.Dialog(viewBinding.dialogMyProfile.get(0)).open()">关闭</button>
<button class="mdui-btn mdui-ripple"
onclick="User.setNick(viewBinding.dialogEditNickNick.val(), () => {mdui.snackbar('已保存, 刷新页面生效');viewBinding.dialogEditNickClose.click()})">保存</button>
</div>
</div>
</div>
<div class="mdui-hidden">
<input type="file" n-id="uploadHeadImage" name="选择头像" onchange="User.uploadHeadImageCallback(this)"
accept="image/png, image/jpeg" />
</div>
<!-- Scripts -->
<script src="https://unpkg.com/crypto-js@4.2.0/crypto-js.js"></script>
<script src="https://unpkg.com/socket.io-client@4.7.4/dist/socket.io.min.js"></script>
<script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.min.js"></script>
<!-- 加了babel也许能解决下浏览器兼容问题 -->
<!-- <script type="module" src="index.js"></script>
<script nomodule type="text/babel" src="index.js"></script> -->
<!-- 就算弄语法兼容其他老旧设备照样用不了, 比如我的 iPad4 -->
<script src="index.js"></script>
</body>
</html>

644
ling_chair_http/index.js Normal file
View File

@@ -0,0 +1,644 @@
/*
* 铃之椅 - 把选择权还给用户, 让聊天权掌握在用户手中
* Copyright 2024 满月叶
* GitHub: https://github.com/MoonLeeeaf/LingChair-Web-Client
* 本项目使用 Apache 2.0 协议开源
*
* Copyright 2024 MoonLeeeaf
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const UrlArgs = new URL(location.href).searchParams
if (UrlArgs.get("debug")) {
let script = document.createElement('script')
script.src = "//cdn.jsdelivr.net/npm/eruda"
document.body.appendChild(script)
script.onload = () => eruda.init()
}
// 经常会因为这个指定ID为位置导致一些莫名BUG
if (location.href.includes("#")) location.replace(location.href.substring(0, location.href.indexOf("#")))
const mdui_snackbar = mdui.snackbar
mdui.snackbar = (m) => {
let t = m
if (m instanceof Object)
t = JSON.stringify(m)
mdui_snackbar(t)
}
const checkEmpty = (i) => {
if (i instanceof Array) {
for (let k of i) {
if (checkEmpty(k)) return true
}
}
return (i == null) || ("" === i) || (0 === i)
}
function escapeHTML(str) {
return str.replace(/[<>&"']/g, function (match) {
switch (match) {
case '<':
return '&lt;'
case '>':
return '&gt;'
case '&':
return '&amp;'
case '"':
return '&quot;'
case "'":
return '&#39;'
default:
return match
}
})
}
class NData {
static mount(node) {
// 便捷获得指定组件
let es = node.querySelectorAll("[n-id]")
let ls = {}
es.forEach((i) => ls[$(i).attr("n-id")] = $(i))
// input 组件与 localStorage 绑定
es = node.querySelectorAll("[n-input-ls]")
es.forEach((e) => {
let j = $(e)
j.val(localStorage.getItem(j.attr("n-input-ls")))
j.blur(() => localStorage.setItem(j.attr("n-input-ls"), j.val()))
})
return ls
}
}
// 快捷获取指定视图
let viewBinding = NData.mount($("#app").get(0))
$.ajax({
url: "config.json",
dataType: "json",
success: (c) => {
viewBinding.appTitle.text(c.appTitle)
if (!c.canChangeServer) {
viewBinding.dialogSignInServerLabel.hide()
viewBinding.drawerChangeServer.hide()
}
},
})
// Toolbar 快捷按钮绑定
viewBinding.contactsRefresh.hide()
viewBinding.tabChatList.on("show.mdui.tab", () => {
viewBinding.contactsRefresh.hide()
})
viewBinding.tabContacts.on("show.mdui.tab", () => {
viewBinding.contactsRefresh.show()
})
viewBinding.tabChatSeesion.on("show.mdui.tab", () => {
viewBinding.contactsRefresh.hide()
})
viewBinding.tabChatSeesion.hide()
// 关于页面
viewBinding.menuAbout.click(() => mdui.alert('GitHub: MoonLeeeaf<br/><br/>欢迎各位大佬访问我们的<a class="mdui-text-color-theme-accent" href="https://github.com/LingChair/LingChair">项目主页</a>', '关于 铃之椅', () => { }, { confirmText: "关闭" }))
viewBinding.drawerChangeServer.click(() => {
mdui.prompt('输入服务器地址...(为空则使用当前页面地址)', (value) => {
localStorage.server = value
mdui.snackbar("更新成功, 刷新页面生效")
}, () => { }, {
confirmText: "确定",
cancelText: "取消"
})
})
viewBinding.drawerSignOut.click(() => {
mdui.confirm('确定要登出账号吗', () => {
localStorage.refreshToken = ""
localStorage.isSignIn = false
setTimeout(() => location.reload(), 300)
}, () => { }, {
confirmText: "确定",
cancelText: "取消"
})
})
viewBinding.sendMsg.click((a) => {
let text = viewBinding.inputMsg.val()
if (text.trim() !== "")
ChatMsgAdapter.send(text)
})
viewBinding.inputMsg.keydown((e) => {
if (e.ctrlKey && e.keyCode === 13)
viewBinding.sendMsg.click()
})
viewBinding.dialogSignInPasswd.keydown((e) => {
if (e.keyCode === 13)
viewBinding.dialogSignInEnter.click()
})
viewBinding.switchNotifications.click((a) => {
if (localStorage.useNotifications === "true" || localStorage.useNotifications != null) {
localStorage.useNotifications = false
viewBinding.switchNotificationsIcon.text("notifications_off")
} else {
localStorage.useNotifications = true
viewBinding.switchNotificationsIcon.text("notifications")
}
})
if (localStorage.useNotifications === "true")
viewBinding.switchNotificationsIcon.text("notifications")
// https://zhuanlan.zhihu.com/p/162910462
Date.prototype.format = function (tms, format) {
let tmd = new Date(tms)
/*
* 例子: format="YYYY-MM-dd hh:mm:ss";
*/
var o = {
"M+": tmd.getMonth() + 1, // month
"d+": tmd.getDate(), // day
"h+": tmd.getHours(), // hour
"m+": tmd.getMinutes(), // minute
"s+": tmd.getSeconds(), // second
"q+": Math.floor((tmd.getMonth() + 3) / 3), // quarter
"S": tmd.getMilliseconds()
// millisecond
}
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (tmd.getFullYear() + "")
.substr(4 - RegExp.$1.length));
}
for (var k in o) {
if (new RegExp("(" + k + ")").test(format)) {
format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k]
: ("00" + o[k]).substr(("" + o[k]).length));
}
}
return format;
}
// new mdui.Drawer('#main-drawer').close()
class NickCache {
static data = {}
static async getNick(name) {
return await new Promise((res, rej) => {
// 这个this别摆着不放啊 不然两下就会去世
let nick = this.data[name]
if (nick == null)
client.emit("user.getNick", { name: localStorage.userName }, (re) => {
let nk = re.data != null ? re.data.nick : name
if (nk == null) nk = name
this.data[name] = nk
res(nk)
})
else
res(nick)
})
}
}
// 既然已经有 Notification 了, 那用回中文也不过分吧 :)
class 通知 {
constructor() {
this.args = {}
this.title = ""
}
static checkAvailable() {
return ("Notification" in window)
}
static async request() {
if (!this.checkAvailable()) return false
return (await Notification.requestPermission())
}
setId(id) {
this.args.tag = id
return this
}
setTitle(t) {
this.title = t
return this
}
setMessage(m) {
this.args.body = m
return this
}
setIcon(i) {
this.args.icon = i
return this
}
setImage(i) {
this.args.image = i
return this
}
setData(data) {
this.args.data = data
}
show(onclick/*, onclose*/) {
if (!通知.checkAvailable()) return
if (localStorage.useNotifications !== "true") return
let n = new Notification(this.title, this.args)
n.onclick = onclick == null ? () => n.close() : (n) => onclick(n)
// n.onclose = onclose
// n.close()
return n
}
}
class ContactsList {
static async reloadList() {
client.emit("user.getFriends", {
name: localStorage.userName,
accessToken: await User.getAccessToken(),
}, async (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
viewBinding.contactsList.empty()
let ls = re.data.friends
for (let index in ls) {
let name = ls[index]
let dick = await NickCache.getNick(name)
/*client.emit("user.getNick", { name: localStorage.userName }, (re) => {
let nick = re.data == null ? re.data.nick : null
let name = ls[index]*/
$($.parseHTML(`<li class="mdui-list-item mdui-ripple"><div class="mdui-list-item-avatar"><img src="` + User.getUserHeadUrl(name) + `" onerror="this.src='default_head.png'" /></div><div class="mdui-list-item-content">` + dick + `</div></li>`)).appendTo(viewBinding.contactsList).click(() => {
ChatMsgAdapter.switchTo(name, "single")
})
//})
}
})
}
}
// 第一次写前端的消息加载, 代码很乱, 还请原谅~
class ChatMsgAdapter {
static type
static target
// static msgList
static minMsgId
static time
static bbn
// 切换聊天对象
static async switchTo(name, type) {
viewBinding.tabChatSeesion.show()
viewBinding.tabChatSeesion.text(await NickCache.getNick(name))
viewBinding.tabChatSeesion.get(0).click()
this.type = type
this.target = name
// this.msgList = []
this.minMsgId = null
viewBinding.pageChatSeesion.empty()
await this.loadMore()
this.scrollToBottom()
}
// 发送消息
static async send(msg) {
client.emit("user.sendSingleMsg", {
name: localStorage.userName,
target: this.target,
msg: msg,
accessToken: await User.getAccessToken(),
}, async (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
viewBinding.inputMsg.val("")
// 微机课闲的没事干玩玩 发现私聊会多发一个(一个是本地的, 另一个是发送成功的) 选择一个关掉就好了
// 这里我选择服务端不发送回调, 不然多设备同步会吵死
// 错了 应该是客户端少发条才对 不然不能多设备同步
if (ChatMsgAdapter.target !== localStorage.userName && ChatMsgAdapter.type === "single") {
let i = ChatMsgAdapter.isAtBottom()
await ChatMsgAdapter.addMsg(localStorage.userName, msg, re.data.time)
if (i) ChatMsgAdapter.scrollToBottom()
}
})
}
static async getHistroy(start, limit) {
return new Promise(async (res, rej) => {
client.emit("user.getSingleChatHistroy", {
name: localStorage.userName,
target: this.target,
limit: limit,
accessToken: await User.getAccessToken(),
startId: start,
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
res(re.data.histroy)
})
})
}
static async loadMore(limit) {
let histroy = await this.getHistroy(this.minMsgId, limit == null ? 13 : limit)
if (histroy.length == 0)
return mdui.snackbar("已经加载完了~")
let re = this.minMsgId != null
this.minMsgId = histroy[0].msgid - 1
let sc = 0
if (re) histroy = histroy.reverse()
for (let index in histroy) {
let i = histroy[index]
let e = await this.addMsg(i.name, i.msg, i.time, re, true)
// 因为某些因素直接DEBUG到吐血 断点继续都不报错 原因不明
sc = sc + (e == null ? 20 : e.get(0).offsetTop)
}
window.scrollBy({
top: sc,
behavior: 'smooth'
})
}
static addSystemMsg(m, re) {
let e
if (re)
e = $($.parseHTML(m)).prependTo(viewBinding.pageChatSeesion)
else
e = $($.parseHTML(m)).appendTo(viewBinding.pageChatSeesion)
return e
}
static isAtBottom() {
let elementRect = viewBinding.pageChatSeesion.get(0).getBoundingClientRect();
return (elementRect.bottom <= window.innerHeight);
}
// 不会压栈 只添加消息 返回消息的JQ对象
static async addMsg(name, m, t, re) {
let nick = await NickCache.getNick(name) // re.data == null ? name : re.data.nick
let msg = escapeHTML(m)
let temp
if (name === localStorage.userName)
temp = `<div class="chat-message-right">
<div class="message-content-with-nickname-right">
<span class="nickname">` + nick + `</span>
<div class="message-content mdui-card">
<span>` + msg + `</span>
</div>
</div>
<img class="avatar" src="` + User.getUserHeadUrl(name) + `" onerror="this.src='default_head.png'" />
</div>`
else
temp = `<div class="chat-message-left">
<img class="avatar" src="` + User.getUserHeadUrl(name) + `" onerror="this.src='default_head.png'" />
<div class="message-content-with-nickname-left">
<span class="nickname">` + nick + `</span>
<div class="message-content mdui-card">
<span>` + msg + `</span>
</div>
</div>
</div>`
let bn = new Date(t).getMinutes()
let e
if (re) {
this.addSystemMsg(temp, re)
if (this.bbn != bn) {
e = this.addSystemMsg(`<div class="mdui-center">` + new Date().format(t == null ? Date.parse("1000-1-1 00:00:00") : t, "yyyy年MM月dd日 hh:mm:ss") + `</div>`, re)
this.time = bn
}
} else {
if (this.bbn != bn) {
e = this.addSystemMsg(`<div class="mdui-center">` + new Date().format(t == null ? Date.parse("1000-1-1 00:00:00") : t, "yyyy年MM月dd日 hh:mm:ss") + `</div>`, re)
this.time = bn
}
this.addSystemMsg(temp, re)
}
this.bbn = new Date(t).getMinutes()
return e
}
// 添加消息记录 作用在 UI 和 msgList
/* static async addMsgLocal(name, m, t, msgid) {
this.msgList.push({
name: name,
msg: m,
msgid: msgid,
})
this.addMsg(name, m, t)
} */
// 从服务器加载一些聊天记录, limit默认=13
static async loadMsgs(limit) {
let histroy = await this.getHistroy(this.msgList[0] == null ? null : this.msgList[0].msgid - 1, limit == null ? 13 : limit)
this.msgList = histroy
}
/* static async loadMsgsFromList(lst) {
for (let index in lst) {
let i = lst[index]
await this.addMsg(i.name, i.msg, i.time)
}
} */
static scrollToBottom() {
// 吐了啊 原来这样就行了 我何必在子element去整啊
window.scrollBy({
top: 1145141919810,
behavior: 'smooth'
});
}
// 从本地加载
/*static loadMsgsFromLocal(target) {
let data = localStorage["chat_msg_" + target]
if (data == null || data === "[]")
return []
return JSON.parse(data)
}
// 把当前聊天记录储存到本地
static saveToLocal() {
localStorage["chat_msg_" + this.target] = JSON.stringify(this.msgList)
}*/
}
class Hash {
static md5(data) {
return CryptoJS.MD5(data).toString(CryptoJS.enc.Base64)
}
static sha256(data) {
return CryptoJS.SHA256(data).toString(CryptoJS.enc.Base64)
}
}
class User {
static myAccessToken
// 登录账号 通过回调函数返回刷新令牌
static signIn(name, passwd, cb) {
client.emit("user.signIn", {
name: name,
passwd: Hash.sha256(passwd) + Hash.md5(passwd),
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
cb(re)
})
}
static signUp(name, passwd, cb) {
client.emit("user.signUp", {
name: name,
passwd: Hash.sha256(passwd) + Hash.md5(passwd),
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
cb(re)
})
}
// 为登录对话框编写的
static signInWithDialog(name, passwd) {
this.signIn(name, passwd, (re) => {
localStorage.refreshToken = re.data.refreshToken
localStorage.isSignIn = true
location.reload()
})
}
static async setNick(nick, cb) {
client.emit("user.setNick", {
name: localStorage.userName,
accessToken: await this.getAccessToken(),
nick: nick,
}, (re) => {
if (re.code !== 0)
return mdui.snackbar(re.msg)
if (cb) cb()
})
}
// 获取头像链接
static getUserHeadUrl(name) {
return client.io.uri + "/users_head/" + name + ".png"
}
static async getAccessToken(er) {
if (this.myAccessToken == null)
this.myAccessToken = await new Promise((res) => {
client.emit("user.getAccessToken", { name: localStorage.userName, refreshToken: localStorage.refreshToken }, (r) => {
if (r.data != null) res(r.data.accessToken)
if (er != null) er(r.msg)
})
})
return this.myAccessToken
}
static uploadHeadImage() {
viewBinding.uploadHeadImage.click()
}
static async uploadHeadImageCallback(self) {
let img = self.files[0]
client.emit("user.setHeadImage", {
name: localStorage.userName,
accessToken: await User.getAccessToken(),
headImage: img,
}, (re) => mdui.snackbar(re.msg))
}
static auth() {
client.emit("user.auth", { name: localStorage.userName, refreshToken: localStorage.refreshToken }, (re) => {
if (re.code !== 0) {
console.error(re)
return mdui.snackbar("验证用户失败!")
}
})
}
static registerCallback() {
client.on("msg.receive", async (a) => {
if (checkEmpty([a.target, a.msg, a.type]))
return
if ((ChatMsgAdapter.target === a.target) && (ChatMsgAdapter.type === a.type)) {
let i = ChatMsgAdapter.isAtBottom()
await ChatMsgAdapter.addMsg(a.target, a.msg.msg, a.msg.time)
if (i) ChatMsgAdapter.scrollToBottom()
}
let n = new 通知().setTitle("新消息 - " + await NickCache.getNick(a.target)).setMessage(a.msg.msg).setIcon(User.getUserHeadUrl(a.target)).show(async () => {
await ChatMsgAdapter.switchTo(a.target, a.type)
ChatMsgAdapter.scrollToBottom()
n.close()
})
})
}
static async openProfileDialog(name) {
viewBinding.dialogProfileHead.attr("src", User.getUserHeadUrl(name))
viewBinding.dialogProfileNick.text(await NickCache.getNick(name))
new mdui.Dialog(viewBinding.dialogProfile).open()
}
}
// 没有刷新令牌需要重新登录 或者初始化
if (!localStorage.refreshToken || localStorage.refreshToken === "")
localStorage.isSignIn = false
let client
if (!localStorage.server || localStorage.server === "")
client = new io({
auth: {
name: localStorage.isSignIn === "false" ? null : localStorage.userName
}
})
else
client = new io(localStorage.server, {
auth: {
name: localStorage.isSignIn === "false" ? null : localStorage.userName
}
})
// 登录到账号
let dialogSignIn
// 谨防 localStorage 字符串数据大坑
if (localStorage.isSignIn === "false")
dialogSignIn = new mdui.Dialog(viewBinding.dialogSignIn.get(0), {
modal: true,
closeOnEsc: false,
history: false,
}).open()
else {
(async () => viewBinding.userNick.text(await NickCache.getNick(localStorage.userName)))()
let hello
let nowHour = new Date().getHours()
if (nowHour >= 6 && nowHour <= 11) hello = "早安"
else if (nowHour == 12) hello = "中午好"
else if (nowHour >= 13 && nowHour <= 18) hello = "下午好"
else if (nowHour >= 19 && nowHour < 22) hello = "晚上好"
else hello = "晚安"
viewBinding.helloText.text(hello)
viewBinding.userHead.attr("src", User.getUserHeadUrl(localStorage.userName))
ContactsList.reloadList()
client.on("connect", () => {
User.auth()
})
User.registerCallback()
}
// 感谢AI的力量
Stickyfill.add($("*").filter((a, b) => $(b).css('position') === 'sticky'))

View File

@@ -0,0 +1,13 @@
Copyright 2024 MoonLeeeaf
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

897
package-lock.json generated Normal file
View File

@@ -0,0 +1,897 @@
{
"name": "LingChair-Node.js",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"express": "^4.19.2",
"mime": "^4.0.1",
"socket.io": "^4.7.5"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"license": "MIT"
},
"node_modules/@types/cookie": {
"version": "0.4.1",
"license": "MIT"
},
"node_modules/@types/cors": {
"version": "2.8.17",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "20.12.5",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/base64id": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/body-parser/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.4.2",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/cors": {
"version": "2.8.5",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "4.3.4",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/engine.io": {
"version": "6.5.4",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.2",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/express/node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/express/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/finalhandler/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz",
"integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==",
"funding": [
"https://github.com/sponsors/broofa"
],
"bin": {
"mime": "bin/cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.2",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/send/node_modules/debug/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/send/node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"dependencies": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/socket.io": {
"version": "4.7.5",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.5.2",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.4",
"license": "MIT",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.11.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "8.11.0",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

8
package.json Normal file
View File

@@ -0,0 +1,8 @@
{
"dependencies": {
"express": "^4.19.2",
"mime": "^4.0.1",
"socket.io": "^4.7.5"
},
"type": "commonjs"
}

25
readme.md Normal file
View File

@@ -0,0 +1,25 @@
### 铃之椅 Node.js
#### 使用
服务端:
1. 克隆本仓库源代码到本地
2. ~~(仅需要集成时) 复制 client_src 中全部文件到 ling_chair_http~~ 默认就是集成的
3. 执行 run.sh
4. Enjoy it :)
网页端:
三种方法任选一个
* 直接使用和服务端集成的网页 (推荐)
* 克隆本仓库到本地并运行本地 HTTP 服务端
* 使用本仓库提供的网页

3
run.bat Normal file
View File

@@ -0,0 +1,3 @@
@echo off
node server_src/main.js
pause

1
run.sh Normal file
View File

@@ -0,0 +1 @@
node server_src/main.js

121
server_src/api-msgs.js Normal file
View File

@@ -0,0 +1,121 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 通讯辅助类
*/
const io = require("./iolib")
const hash = require("./hashlib")
const vals = require("./val")
const users = require("./api-users")
let getSameHashedValue = (a, b) => {
let _a = [hash.md5(a) + hash.sha256(a), hash.md5(b) + hash.sha256(b)].sort()
let [_1, _2] = _a
return hash.sha256hex(hash.sha256hex(_1) + hash.sha256hex(_2))
}
let getSingleChatDir = (a, b) => {
return vals.LINGCHAIR_SINGLE_MESSAGE_DIR + "/" + getSameHashedValue(a, b)
}
let apis = {
// 储存单聊消息: 操作者, 访问密钥, 发送至, 消息内容
// 消息存储方式为计次直接储存, 每一个消息都有对应的 ID
// 读取某一段落时使用遍历方式
// @API
sendSingleMsg: (name, accessToken, target, msg) => {
if (!users.checkAccessToken(name, accessToken))
return { code: -1, msg: "访问令牌错误" }
if (!users.isUserExists(target))
return { code: -1, msg: "目标用户不存在" }
if (msg.trim() == "")
return { code: -1, msg: "不是有内容的消息我不要" }
let fileDir = getSingleChatDir(name, target)
io.mkdirs(fileDir)
let countFile = io.open(fileDir + "/count.txt", "rw")
if (!io.exists(fileDir + "/count.txt"))
countFile.write("0")
let count = parseInt(countFile.read())
count += 1
let time = Date.now()
io.open(fileDir + "/msg_" + count + ".json", "w").writeJson({
name: name,
msg: msg,
msgid: count,
time: time,
}).close()
countFile.write(count + "")
return { code: 0, msg: "成功", msgid: count, time: time }
},
// 读取消息记录
// 从起始点到结束点读取,由最新到最老(计次越大越新)
// 不提供 startId 则默认从最新计次往前数
// 若超过 limit 计次范围, 直接终止遍历
// @API
getSingleMsgHistroy: (name, accessToken, target, sid, limit) => {
if (!users.checkAccessToken(name, accessToken))
return { code: -1, msg: "访问令牌错误" }
if (!users.isUserExists(target))
return { code: -1, msg: "目标用户不存在" }
let fileDir = getSingleChatDir(name, target)
io.mkdirs(fileDir)
let countFile = io.open(fileDir + "/count.txt", "rw")
if (!io.exists(fileDir + "/count.txt"))
countFile.write("0")
let startId = sid
if (startId == null)
startId = parseInt(countFile.read().toString())
let list = []
let i = startId
let i2 = 0
let cfn
while(true) {
cfn = fileDir + "/msg_" + i + ".json"
// 1. 超过界限
// 2. 超过计次
// 3. 超过最大限度
if ((!io.exists(cfn)) || i2 > limit || i2 > 100) break
try {
let data = io.open(cfn, "r").readJson()
list.unshift(data)
} catch (e) {
return { code: -2, msg: e }
}
i--
i2++
}
return { code: 0, msg: "成功", histroy: list }
},
// 上传图片: 操作者, 访问密钥, 发送至, 图片
// 未来需要一些操作来删除未使用的图片文件
// @API
uploadImage: (name, accessToken, target, msg) => {
if (!users.checkAccessToken(name, accessToken))
return { code: -1, msg: "访问令牌错误" }
if (!users.isUserExists(target))
return { code: -1, msg: "目标用户不存在" }
let fileDir = getSingleChatDir(name, target) + "/images/"
io.mkdirs(fileDir)
},
}
module.exports = apis

195
server_src/api-users.js Normal file
View File

@@ -0,0 +1,195 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 用户辅助类
*/
const io = require("./iolib")
const hash = require("./hashlib")
const vals = require("./val")
// 获取用户资料所在的路径
let getUserPath = (name) => {
return vals.LINGCHAIR_DATA_DIR + "/users/" + name
}
// 用户是否存在
let isUserExists = (name) => {
return io.exists(getUserPath(name))
}
let apis = {
isUserExists: isUserExists,
// ================================
// 无需令牌的 API
// ================================
// 创建账号: 账号, 密码 返回账号唯一 ID 和成功信息 失败返回 null 和原因
// 账号文件结构: {uid: 10000, name: "GenShin", nick: "Impact", passwd: "SHA-256 + MD5"}
// 注意: 密码在客户端也应该经过哈希处理(SHA256 + MD5)
// @APi
signUp: (name, passwd) => {
if (passwd == null || name == null)
return { msg: "必须输入 账号和密码", code: -1 }
let path = getUserPath(name)
if (isUserExists(name))
return { msg: "用户账号名重复", code: -1 }
io.mkdirs(path)
let idCount = io.open(vals.LINGCHAIR_USERS_COUNT_FILE)
let uid = parseInt(idCount.read("*a"))
idCount.write((uid + 1) + "").close()
io.open(path + "/user.json").writeJson({
uid: uid,
name: name,
nick: null,
passwd: hash.sha256(passwd) + hash.md5(passwd),
}).close()
return { uid: uid, msg: "成功", code: 0 }
},
// 登录账号: 账号, 密码 返回刷新令牌 失败返回 null 和原因
// 注意: 密码在客户端应该经过哈希处理(SHA256 + MD5)
// @API
signIn: (name, passwd) => {
if (passwd == null || name == null)
return { msg: "必须输入 账号和密码", code: -1 }
if (!isUserExists(name))
return { msg: "用户不存在", code: -1 }
if (apis.getPassWordHashedRaw(name) !== (hash.sha256(passwd) + hash.md5(passwd)))
return { msg: "账号所对应的密码错误", code: -1 }
return { msg: "成功", code: 0, refreshToken: apis.getRefreshToken(name, apis.getPassWordHashed(name)) }
},
// 获取刷新令牌: 账号,密码 返回刷新令牌
// 注意: 密码在客户端也应该经过哈希处理(SHA256 + MD5)
// 刷新令牌算法: 哈希(用户ID + 当前年 + 当前月 + 密码 + 盐)
// 有效期: 一个月
getRefreshToken: (name, passwd) => {
let d = new Date()
let raw = name + d.getFullYear() + d.getMonth() + passwd + "LINGCHAIR-TEST-DEMO"
return hash.sha256(raw) + hash.md5(raw)
},
// 获取访问令牌: 账号,刷新令牌 返回访问令牌
// 注意: 密码在客户端也应该经过哈希处理(SHA256 + MD5)
// 刷新令牌算法: 哈希(用户ID + 当前年 + 当前月 + 密码 + 盐)
// 有效期: 一天
getAccessTokenNonApi: (name, rt) => {
if (!apis.checkRefreshToken(name, rt))
return null
let date = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'numeric', day: 'numeric' })
return hash.sha256(name + date) + hash.md5(rt + date + "LINGCHAIR-ACCESS-TEST-DEMO")
},
// 获取密码(已被哈希处理) 返回密码
// 在密码被设置前已经被哈希过,不需要重复
// 算法: (SHA256 + MD5)
// 警告: 这是经过二次哈希的
getPassWordHashed: (name) => {
return hash.sha256(apis.getPassWordHashedRaw(name)) + hash.md5(apis.getPassWordHashedRaw(name))
},
// 请勿与上面的混淆
// 上面的是经过第二次哈希的
getPassWordHashedRaw: (name) => {
return io.open(getUserPath(name) + "/user.json").readJson().passwd
},
// 检测刷新令牌是否正确: 账号, 刷新令牌 返回布尔值
// 密码在服务端经过哈希保存 不需要重复输入密码
checkRefreshToken: (name, rt) => {
return apis.getRefreshToken(name, apis.getPassWordHashed(name)) === rt
},
// 检测访问令牌是否正确: 账号, 访问令牌 返回布尔值
// 密码在服务端经过哈希保存 不需要重复输入密码
checkAccessToken: (name, at) => {
return apis.getAccessTokenNonApi(name, apis.getRefreshToken(name, apis.getPassWordHashed(name /* 就是你这个傻逼害得我找两年BUG */))) === at
},
// ================================
// 需要令牌的 API
// ================================
// 获取访问令牌: 账号, 刷新令牌 返回访问令牌 失败返回 -1 和原因
// 有效期: 一天
// 算法: SHA256(name) + MD5(rt + 盐)
// @Api
getAccessToken: (name, rt) => {
if (!apis.checkRefreshToken(name, rt))
return { msg: "刷新令牌不正确!", code: -1 }
return { msg: "成功", code: 0, accessToken: apis.getAccessTokenNonApi(name, rt) }
},
// 设置头像: 账号, 访问令牌, 头像数据 返回结果
// @API
setHeadImage: (name, at, head) => {
if (!apis.checkAccessToken(name, at))
return { msg: "访问令牌不正确!", code: -1 }
io.open(vals.LINGCHAIR_USERS_HEAD_DIR + "/" + name + ".png", "w").write(head).close()
return { msg: "成功", code: 0 }
},
// 修改昵称
// @APi
setNick: (name, at, nick) => {
if (!apis.checkAccessToken(name, at))
return { msg: "访问令牌不正确!", code: -1 }
let path = getUserPath(name)
let configIo = io.open(path + "/user.json", "rw")
let config = configIo.readJson()
config.nick = nick
configIo.writeJson(config)
configIo.close()
return { msg: "成功", code: 0 }
},
// 取联系人列表(好友): 账号, 访问令牌 返回好友列表
getFriendsNonApi: (name, at) => {
let file = getUserPath(name) + "/friends.json"
if (!io.exists(file))
io.open(file, "w").writeJson({list: [name]}).close()
return io.open(file, "r").readJson().list
},
// 取用户昵称: 账号 返回昵称
getNickNonApi: (name) => {
let file = getUserPath(name) + "/user.json"
return io.open(file, "r").readJson().nick
},
// 取昵称: 账号 返回昵称
// @API
getNick: (name, at) => {
return { msg: "成功", code: 0, nick: apis.getNickNonApi(name)}
},
// 取联系人列表(好友): 账号, 访问令牌 返回好友列表
// @API
getFriends: (name, at) => {
if (!apis.checkAccessToken(name, at))
return { msg: "访问令牌不正确!", code: -1 }
return { msg: "成功", code: 0, friends: apis.getFriendsNonApi(name, at)}
},
}
module.exports = apis

13
server_src/color.js Normal file
View File

@@ -0,0 +1,13 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 控制台颜色辅助类
*/
module.exports = {
none: "\033[0m",
red: "\033[1;31m",
green: "\033[1;32m",
yellow: "\033[1;33m",
blue: "\033[1;34m",
}

16
server_src/hashlib.js Normal file
View File

@@ -0,0 +1,16 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 哈希辅助类
*/
const crypto = require("crypto")
let apis = {
sha256: (data) => crypto.createHash("sha256").update(data).digest("base64"),
md5: (data) => crypto.createHash("md5").update(data).digest("base64"),
sha256hex: (data) => crypto.createHash("sha256").update(data).digest("hex"),
md5hex: (data) => crypto.createHash("md5").update(data).digest("hex"),
}
module.exports = apis

17
server_src/httpApi.js Normal file
View File

@@ -0,0 +1,17 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 铃之椅 Node 服务端
*/
// 不得不说 express 太强了
const vals = require("./val")
const express = require("express")
let api = express()
api.use("/", express.static("ling_chair_http"))
api.use("/users_head/", express.static(vals.LINGCHAIR_DATA_DIR + "/users_head"))
module.exports = api

48
server_src/iolib.js Normal file
View File

@@ -0,0 +1,48 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 更简单地使用 FileSystem 库
*/
const fs = require("fs")
class IoImpl {
constructor(p, m) {
this.path = p
this.mode = m
}
write(byteOrString) {
fs.writeFileSync(this.path, byteOrString)
return this
}
read(type) {
// TODO: impentments this method
return fs.readFileSync(this.path)
}
close() {
delete this.path
delete this.mode
delete this.read
delete this.write
}
readJson() {
return JSON.parse(this.read("*a"))
}
writeJson(data) {
return this.write(JSON .stringify(data))
}
}
let apis = {
open: (path, mode) => {
return new IoImpl(path, mode)
},
mkdirs: (path) => {
try {fs.mkdirSync(path, { recursive: true })}catch(e){}
},
exists: (path) => {
return fs.existsSync(path)
},
}
module.exports = apis

93
server_src/main.js Normal file
View File

@@ -0,0 +1,93 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 铃之椅 Node 服务端
*/
console.log("正在初始化...")
const log = (t) => {
console.log("[" + new Date().toLocaleTimeString('en-US', { hour12: false }) + "] " + t)
}
const sio = require("socket.io")
const http = require("http")
const https = require("https")
const fs = require("fs")
const process = require("process")
const vals = require("./val")
const color = require("./color")
//定义 Http 服务器回调
let httpServerCallback = require("./httpApi")
// 定义 Socket.io 服务器回调
let wsServerCallback = require("./wsApi")
let httpServer
if (vals.LINGCHAIR_SERVER_CONFIG.useHttps)
httpServer = https.createServer({
key: fs.readFileSync(vals.LINGCHAIR_SERVER_CONFIG.https.key),
cert: fs.readFileSync(vals.LINGCHAIR_SERVER_CONFIG.https.cert),
}, httpServerCallback)
else
httpServer = http.createServer(httpServerCallback)
let wsServer = new sio.Server(httpServer)
const cachedClients = {}
let checkEmpty = (i) => {
if (i instanceof Array) {
for (k in i) {
if (checkEmpty(i[k])) return true
}
}
return (i == null) || ("" === i) || (0 === i)
}
wsServer.on("connect", (client) => {
log("客户端 " + client.handshake.address + " 已连接, 用户名(未经验证): " + client.handshake.auth.name)
for (const cb in wsServerCallback) {
client.on(cb, (...args) => {
log("客户端 " + client.handshake.address + " 对接口 [" + cb + "] 发起请求,参数为 " + JSON.stringify(args[0]))
let callback = args[args.length - 1]
try {
wsServerCallback[cb](args[0], (reArgs) => {
callback(reArgs)
log("返回接口 [" + cb + "] 到 " + client.handshake.address + ",参数为 " + JSON.stringify(reArgs))
}, client, cachedClients)
} catch (e) {
log(color.yellow + "调用接口或返回数据时出错: " + e + color.none)
callback({ code: -1, msg: e })
}
})
}
client.on("disconnect", () => {
if (!client.handshake.auth.passCheck)
return log("未验证的客户端 " + client.handshake.address + " 已断开, 未验证的用户名: " + client.handshake.auth.name)
// 为了支持多客户端登录 我豁出去了
if (cachedClients[client.handshake.auth.name].length === 1)
cachedClients[client.handshake.auth.name] = null
else
cachedClients[client.handshake.auth.name].forEach((item, index, arr) => {
if (item == client) {
arr.splice(index, 1)
}
})
log("客户端 " + client.handshake.address + " 已断开, 用户名: " + client.handshake.auth.name)
})
})
httpServer.listen(vals.LINGCHAIR_SERVER_CONFIG.port)
console.log(color.red + "=== 铃之椅 - Server ===" + color.none + "\n\r")
console.log(color.yellow + "Github: MoonLeeeaf" + color.none)
log(color.green + "运行服务于端口 " + vals.LINGCHAIR_SERVER_CONFIG.port + " 上," + (vals.LINGCHAIR_SERVER_CONFIG.useHttps == true ? "已" : "未") + "使用 HTTPS" + color.none)
log(color.green + "服务已启动..." + color.none)

56
server_src/val.js Normal file
View File

@@ -0,0 +1,56 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 铃之椅 Node 服务端
*/
const io = require("./iolib")
let vals = {}
// 配置目录
vals.LINGCHAIR_CONFIG_DIR = "ling_chair_config"
// HTTP 服务器资源目录
vals.LINGCHAIR_HTTP_DIR = "ling_chair_http"
// 服务端配置
vals.LINGCHAIR_SERVER_CONFIG_FILE = vals.LINGCHAIR_CONFIG_DIR + "/server.json"
// 主要数据目录
vals.LINGCHAIR_DATA_DIR = "ling_chair_data"
// 用户数据
vals.LINGCHAIR_USERS_DATA_DIR = vals.LINGCHAIR_DATA_DIR + "/users"
// 用户头像
vals.LINGCHAIR_USERS_HEAD_DIR = vals.LINGCHAIR_DATA_DIR + "/users_head"
// 群聊消息
vals.LINGCHAIR_GROUP_MESSAGE_DIR = vals.LINGCHAIR_DATA_DIR + "/messages/group"
// 单聊消息
vals.LINGCHAIR_SINGLE_MESSAGE_DIR = vals.LINGCHAIR_DATA_DIR + "/messages/single"
// 用户 ID 计次
vals.LINGCHAIR_USERS_COUNT_FILE = vals.LINGCHAIR_USERS_DATA_DIR + "/count.txt"
// 创建必备目录
io.mkdirs(vals.LINGCHAIR_CONFIG_DIR)
io.mkdirs(vals.LINGCHAIR_USERS_DATA_DIR)
io.mkdirs(vals.LINGCHAIR_USERS_HEAD_DIR)
io.mkdirs(vals.LINGCHAIR_GROUP_MESSAGE_DIR)
io.mkdirs(vals.LINGCHAIR_SINGLE_MESSAGE_DIR)
// 生成服务端配置文件
if (!io.exists(vals.LINGCHAIR_SERVER_CONFIG_FILE)) io.open(vals.LINGCHAIR_SERVER_CONFIG_FILE, "w").write(JSON.stringify({
useHttps: false,
port: 3601,
bindAddress: "",
https: {
key: "",
cert: "",
},
})).close()
if (!io.exists(vals.LINGCHAIR_USERS_COUNT_FILE)) io.open(vals.LINGCHAIR_USERS_COUNT_FILE, "w").write("10000").close()
// 加载服务端配置文件
vals.LINGCHAIR_SERVER_CONFIG = JSON.parse(io.open(vals.LINGCHAIR_SERVER_CONFIG_FILE, "r").read("*a"))
module.exports = vals

214
server_src/wsApi.js Normal file
View File

@@ -0,0 +1,214 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 铃之椅 Node 服务端
*/
const log = (t) => {
console.log("[" + new Date().toLocaleTimeString('en-US', { hour12: false }) + "] " + t)
}
const msgs = require("./api-msgs")
const users = require("./api-users")
const color = require("./color")
let checkEmpty = (i) => {
if (i instanceof Array) {
for (k in i) {
if (checkEmpty(i[k])) return true
}
}
return (i == null) || ("" === i) || (0 === i)
}
/*
* Api 规范:
* 1. 禁止中文 拼音
* 2. 一个 Api 做一件事 同一组 Api 用注释行分隔
* 3. 尽可能简单易懂 或者打注释
* 4. 保证客户端可用
*/
// Api 调用:
// 一般规定, code=0 正常, code=-1 异常, code=-2 运行时错误 另外还需要 msg="any"
// 可以随便 return 进行函数中断 因为这里的调用不会取返回值
let api = {
// ---------- 用户 API ----------
// 验证
// 调用方法自己看
"user.auth": (a, cb, client, cachedClients) => {
if (checkEmpty([a.name, a.refreshToken]))
return cb({ msg: "参数缺失", code: -1 })
if (!users.checkRefreshToken(a.name, a.refreshToken))
return cb({ code: -1, msg: "刷新令牌错误" })
log(color.yellow + "客户端 " + client.handshake.address + " 完成了用户 " + a.name + " 的验证" + color.none)
// 更新映射
client.handshake.auth.passCheck = true
if (cachedClients[a.name] == null)
cachedClients[a.name] = []
cachedClients[a.name].push(client)
cb({ code: 0, msg: "成功" })
},
// 注册
// {name: 账号, nick: 昵称, passwd: 密码} 返回 {data: {uid: 账号ID}}
// 密码在客户端应该经过哈希处理 算法为 SHA256+MD5
// 客户端在注册成功之后应该引导用户登录
"user.signUp": (a, cb) => {
if (checkEmpty([a.name, a.passwd]))
return cb({ msg: "参数缺失", code: -1 })
let { uid, msg, code } = users.signUp(a.name, a.passwd)
if (code !== 0)
return cb({ msg: msg, code: code })
cb({ msg: msg, code: 0, data: { uid: uid } })
},
// 登录
// {name: 账号, passwd: 密码} 返回 {data: {refreshToken: 刷新令牌}}
// 密码在客户端应该经过哈希处理 算法为 SHA256+MD5
"user.signIn": (a, cb) => {
if (checkEmpty([a.name, a.passwd]))
return cb({ msg: "参数缺失", code: -1 })
let { refreshToken, msg, code } = users.signIn(a.name, a.passwd)
if (code !== 0)
return cb({ msg: msg, code: code })
cb({ msg: msg, code: 0, data: { refreshToken: refreshToken } })
},
// 获取访问令牌
// {name: 账号, refreshToken: 刷新令牌} 返回 {data: {accessToken: 访问令牌}}
"user.getAccessToken": (a, cb) => {
if (checkEmpty([a.name, a.refreshToken]))
return cb({ msg: "参数缺失", code: -1 })
let { accessToken, msg, code } = users.getAccessToken(a.name, a.refreshToken)
if (code !== 0)
return cb({ msg: msg, code: code })
cb({ msg: msg, code: 0, data: { accessToken: accessToken } })
},
// 上传头像
// {name: 账号, accessToken: 访问令牌, headImage: 头像数据} 返回 {}
"user.setHeadImage": (a, cb) => {
if (checkEmpty([a.name, a.accessToken, a.headImage]))
return cb({ msg: "参数缺失", code: -1 })
let { msg, code } = users.setHeadImage(a.name, a.accessToken, a.headImage)
if (code !== 0)
return cb({ msg: msg, code: code })
cb({ msg: msg, code: 0 })
},
// 修改昵称
"user.setNick": (a, cb) => {
if (checkEmpty([a.name, a.accessToken, a.nick]))
return cb({ msg: "参数缺失", code: -1 })
let { msg, code } = users.setNick(a.name, a.accessToken, a.nick)
if (code !== 0)
return cb({ msg: msg, code: code })
cb({ msg: msg, code: 0 })
},
// ---------- 联系人 API --------
// 获取好友列表
// {name: 账号, accessToken: 访问令牌} 返回 {friends: []}
"user.getFriends": (a, cb) => {
if (checkEmpty([a.name, a.accessToken]))
return cb({ msg: "参数缺失", code: -1 })
let { msg, code, friends } = users.getFriends(a.name, a.accessToken)
if (code !== 0)
return cb({ msg: msg, code: code })
cb({ msg: msg, code: 0, data: { friends: friends } })
},
"user.getNick": (a, cb) => {
if (checkEmpty([a.name]))
return cb({ msg: "参数缺失", code: -1 })
let { msg, code, nick } = users.getNick(a.name)
if (code !== 0)
return cb({ msg: msg, code: code })
cb({ msg: msg, code: 0, data: { nick: nick } })
},
// ---------- 通讯 API ----------
// 单聊发送消息
// {name: 当前用户, target: 发送到, accessToken: 访问密钥, msg: 消息内容}
// 2024.3.30: 支持对方收到消息
"user.sendSingleMsg": (a, cb, c, cache) => {
if (checkEmpty([a.name, a.target, a.accessToken, a.msg]))
return cb({ msg: "参数缺失", code: -1 })
let { msg, code, msgid, time } = msgs.sendSingleMsg(a.name, a.accessToken, a.target, a.msg)
if (code !== 0)
return cb({ msg: msg, code: code })
// 微机课闲的没事干玩玩 发现私聊会多发一个(一个是本地的, 另一个是发送成功的) 选择一个关掉就好了
// 这里我选择客户端, 否则没法多设备同步
let args = {
target: a.name,
msg: {
msgid: msgid,
time: time,
msg: a.msg,
name: a.name,
},
type: "single",
}
if (cache[a.target] != null)
cache[a.target].forEach((v) => {
v.emit("msg.receive", args, () => { })
log("尝试向客户端 " + v.handshake.address + " 发送事件 [msg.receive], 参数为 " + JSON.stringify(args))
})
cb({ msg: msg, code: 0, data: { time: time } })
},
// 单聊获取历史记录
// {name: 当前用户, target: 聊天目标, accessToken: 访问密钥, startId: 计次开始的msgid, limit: 最大返回数(最大100)}
"user.getSingleChatHistroy": (a, cb) => {
if (checkEmpty([a.name, a.target, a.accessToken, a.limit]))
return cb({ msg: "参数缺失", code: -1 })
let { msg, code, histroy } = msgs.getSingleMsgHistroy(a.name, a.accessToken, a.target, a.startId, a.limit)
if (code !== 0)
return cb({ msg: msg, code: code })
cb({ msg: msg, code: 0, data: { histroy: histroy } })
},
}
module.exports = api