commit a376ead1953f89ea2c9bf1a92318c6a9fd882a45 Author: MoonLeeeaf <150461955+MoonLeeeaf@users.noreply.github.com> Date: Fri May 10 21:14:50 2024 +0800 chore: init diff --git a/.github/一些思想/聊天数据储存.md b/.github/一些思想/聊天数据储存.md new file mode 100644 index 0000000..f25dcdb --- /dev/null +++ b/.github/一些思想/聊天数据储存.md @@ -0,0 +1,39 @@ +### 关于聊天记录储存的方案概述 + +自 3月10日 开始,这个项目遇到了一些瓶颈,停止开发 + +我在这段时间的精力也被耗尽 + +故此,在今天(3月26日)重新思考,理清思路 + +预计3月28日左右实现此功能 + +#### 基本想法 + +##### 写入 + +对于储存来说,从 0 开始计次,一条消息对应一个 ID + +第一条消息的 ID 为 1, 计次文件储存为 1 + +为了性能,每个消息使用一个 JSON 文件储存 + +##### 读取 + +当读取时,从计次文件获取计次数,并从此数字递减,以此读取 + +然后储存到数组并直接返回给用户 + +客户端消息的 ID 必须要同步,这是为了范围读取以前的消息 + +#### 和以前的区别 + +在我的聊天软件第一版、第二版中,均由 PHP 作为后端 + +写入则直接追加,读取整个返回 + +这种做法的最大缺陷是浪费性能 + +我曾想过使用 SQL,不过潜在的风险以及我的技术都能符合我的要求 + +故此,便有了这个方法 diff --git a/.github/测试样例/test.html b/.github/测试样例/test.html new file mode 100644 index 0000000..5114813 --- /dev/null +++ b/.github/测试样例/test.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + 铃之椅 API测试 + + + + + +
+ +
+ +
+ +
+ + + + + + + + \ No newline at end of file diff --git a/.github/测试样例/聊天气泡实验.html b/.github/测试样例/聊天气泡实验.html new file mode 100644 index 0000000..3e3e73e --- /dev/null +++ b/.github/测试样例/聊天气泡实验.html @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + 聊天气泡测试 + + + + + +
+
+ 小沫 +
+ 你好!这是一条较长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长的消息内容,测试消息长度自动填充效果。 +
+
+ Avatar +
+ + +
+ Avatar +
+ 小沫 +
+ 你好!这是一条较长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长的消息内容,测试消息长度自动填充效果。 +
+
+
+ + + + + + + \ No newline at end of file diff --git a/.github/项目创建时间.txt b/.github/项目创建时间.txt new file mode 100644 index 0000000..5b7f768 --- /dev/null +++ b/.github/项目创建时间.txt @@ -0,0 +1,2 @@ +Web客户端: 2024‎年‎1‎月‎23‎日,‏‎13:15:09 +Node.js服务端: ‎2024‎年‎1‎月‎24‎日,‏‎14:10:47 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..991afb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +ling_chair_data/ +ling_chair_config/ diff --git a/final.md b/final.md new file mode 100644 index 0000000..4bda31b --- /dev/null +++ b/final.md @@ -0,0 +1,52 @@ +### 最终目标 + +铃之椅服务端的 Node 实现,目前最低目标如下 + +* 全 API 可用 +* 可配置 +* 开箱即用 + +如果有时间,可以完成下面的目标 + +* 尽可能使用异步 +* 最大利用 I/O 性能 + +#### API 实现 + +一般约定: ☘️=已完成 + +* 用户 + * 登录 ☘️ + * 注册 ☘️ + * 修改密码 + * 令牌机制 ☘️ + * 用户资料 + * 头像 ☘️ + * 昵称 ☘️ + * 安全 + * 账号冻结 + * 权限管理 + * 匿名 + +* 聊天 + * 双方私聊 ☘️ 完善中 + * 多人群聊 + * 临时对话 + * 多媒体 + * 文件 + * 群聊资料 + * 介绍 + * 头像 + * 群名称 + * 安全 + * 权限管理 + * 管理员 + * 入群权限 + * 可否被查找 + * 功能限制 + * 管理匿名 + +注: + 1. 为了保障管理员层安全,匿名的账号或管理员的信息仍然能被伺服器管理员所查询,不过请放心,一般来说不会有任何正常人能查询到你 + 2. 多媒体应该能够定时删除而非永久保存,否则伺服器会炸掉的 + 3. 每个功能都应该验证令牌 diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..5c226ee --- /dev/null +++ b/license.txt @@ -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. \ No newline at end of file diff --git a/ling_chair_http/chat-message.css b/ling_chair_http/chat-message.css new file mode 100644 index 0000000..46e0dcf --- /dev/null +++ b/ling_chair_http/chat-message.css @@ -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%; +} \ No newline at end of file diff --git a/ling_chair_http/config.json b/ling_chair_http/config.json new file mode 100644 index 0000000..3e24632 --- /dev/null +++ b/ling_chair_http/config.json @@ -0,0 +1,4 @@ +{ + "appTitle": "", + "canChangeServer": false +} \ No newline at end of file diff --git a/ling_chair_http/default_head.png b/ling_chair_http/default_head.png new file mode 100644 index 0000000..4f6f226 Binary files /dev/null and b/ling_chair_http/default_head.png differ diff --git a/ling_chair_http/icon.ico b/ling_chair_http/icon.ico new file mode 100644 index 0000000..224793f Binary files /dev/null and b/ling_chair_http/icon.ico differ diff --git a/ling_chair_http/index.css b/ling_chair_http/index.css new file mode 100644 index 0000000..5086f9d --- /dev/null +++ b/ling_chair_http/index.css @@ -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; + } +} diff --git a/ling_chair_http/index.html b/ling_chair_http/index.html new file mode 100644 index 0000000..08b1e72 --- /dev/null +++ b/ling_chair_http/index.html @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + 铃之椅 + + + + +
+ +
+ +
+ + +
+ + +
+ + +
+ + 常用 + 通讯录 + +
+
+
+ 欢迎回来! (^▽^。) +
+
+
+ +
+ +
+ +
+
点我继续加载前面的聊天记录, 或者回到底部
+
+
+ + + +
+ + +
+
+ 登录到 铃之椅 +
+
+
+ cloud_circle + + +
+
+ account_circle + + +
+
+ lock + + +
+ 注:使用非已知的服务提供商提供的服务器时, 请注意个人信息保护哦 o(。・ω・。)o +
+
+ + +
+
+
+ + +
+
+
+ +
+
+
+
+
+ +
+
+ + +
+
+ 资料 +
+
+ +
+
+ +
+
+ + +
+
+ 修改昵称 +
+
+
+ + +
+
+
+ + +
+
+ + +
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/ling_chair_http/index.js b/ling_chair_http/index.js new file mode 100644 index 0000000..ae3f0e7 --- /dev/null +++ b/ling_chair_http/index.js @@ -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 '<' + case '>': + return '>' + case '&': + return '&' + case '"': + return '"' + case "'": + return ''' + 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

欢迎各位大佬访问我们的项目主页', '关于 铃之椅', () => { }, { 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(`
  • ` + dick + `
  • `)).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 = `
    +
    + ` + nick + ` +
    + ` + msg + ` +
    +
    + +
    ` + else + temp = `
    + +
    + ` + nick + ` +
    + ` + msg + ` +
    +
    +
    ` + + let bn = new Date(t).getMinutes() + let e + if (re) { + this.addSystemMsg(temp, re) + if (this.bbn != bn) { + e = this.addSystemMsg(`
    ` + new Date().format(t == null ? Date.parse("1000-1-1 00:00:00") : t, "yyyy年MM月dd日 hh:mm:ss") + `
    `, re) + this.time = bn + } + } else { + if (this.bbn != bn) { + e = this.addSystemMsg(`
    ` + new Date().format(t == null ? Date.parse("1000-1-1 00:00:00") : t, "yyyy年MM月dd日 hh:mm:ss") + `
    `, 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')) diff --git a/ling_chair_http/license.txt b/ling_chair_http/license.txt new file mode 100644 index 0000000..5c226ee --- /dev/null +++ b/ling_chair_http/license.txt @@ -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. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9c64e96 --- /dev/null +++ b/package-lock.json @@ -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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..068d1a8 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "express": "^4.19.2", + "mime": "^4.0.1", + "socket.io": "^4.7.5" + }, + "type": "commonjs" +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..63711a5 --- /dev/null +++ b/readme.md @@ -0,0 +1,25 @@ +### 铃之椅 Node.js + + + +#### 使用 + +服务端: + + 1. 克隆本仓库源代码到本地 + + 2. ~~(仅需要集成时) 复制 client_src 中全部文件到 ling_chair_http~~ 默认就是集成的 + + 3. 执行 run.sh + + 4. Enjoy it :) + +网页端: + +三种方法任选一个 + + * 直接使用和服务端集成的网页 (推荐) + + * 克隆本仓库到本地并运行本地 HTTP 服务端 + + * 使用本仓库提供的网页 diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..96440bc --- /dev/null +++ b/run.bat @@ -0,0 +1,3 @@ +@echo off +node server_src/main.js +pause \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..0787755 --- /dev/null +++ b/run.sh @@ -0,0 +1 @@ +node server_src/main.js \ No newline at end of file diff --git a/server_src/api-msgs.js b/server_src/api-msgs.js new file mode 100644 index 0000000..b196304 --- /dev/null +++ b/server_src/api-msgs.js @@ -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 diff --git a/server_src/api-users.js b/server_src/api-users.js new file mode 100644 index 0000000..e053449 --- /dev/null +++ b/server_src/api-users.js @@ -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 diff --git a/server_src/color.js b/server_src/color.js new file mode 100644 index 0000000..f451b93 --- /dev/null +++ b/server_src/color.js @@ -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", +} diff --git a/server_src/hashlib.js b/server_src/hashlib.js new file mode 100644 index 0000000..83a6350 --- /dev/null +++ b/server_src/hashlib.js @@ -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 diff --git a/server_src/httpApi.js b/server_src/httpApi.js new file mode 100644 index 0000000..199008e --- /dev/null +++ b/server_src/httpApi.js @@ -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 diff --git a/server_src/iolib.js b/server_src/iolib.js new file mode 100644 index 0000000..be9bc27 --- /dev/null +++ b/server_src/iolib.js @@ -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 diff --git a/server_src/main.js b/server_src/main.js new file mode 100644 index 0000000..95abd27 --- /dev/null +++ b/server_src/main.js @@ -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) diff --git a/server_src/val.js b/server_src/val.js new file mode 100644 index 0000000..a75ff26 --- /dev/null +++ b/server_src/val.js @@ -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 diff --git a/server_src/wsApi.js b/server_src/wsApi.js new file mode 100644 index 0000000..cec1c23 --- /dev/null +++ b/server_src/wsApi.js @@ -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