From f370f28da62a221ddc819ef332d5272a987a3398 Mon Sep 17 00:00:00 2001 From: tursom Date: Sat, 21 Mar 2026 18:52:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BC=9A=E8=AF=9D=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E6=98=BE=E7=A4=BA=E8=87=AA=E5=B7=B1=E5=90=8D=E5=AD=97?= =?UTF-8?q?=E8=80=8C=E9=9D=9E=E5=AF=B9=E6=96=B9=E5=90=8D=E5=AD=97=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit encodeSteamMessage 中 echo 消息始终使用 friendId 获取对方信息, 避免会话名称被自己的名字覆盖。同时包含 Web 聊天界面及相关重构。 Co-Authored-By: Claude Opus 4.6 (1M context) --- API.md | 641 ++++++++++++ chat.js | 1899 ++++++++++++++++++++++++++++++---- client.js | 19 +- config.example.js | 13 +- logger.js | 2 +- package-lock.json | 1144 ++++++++++---------- package.json | 2 +- public/app.js | 2523 +++++++++++++++++++++++++++++++++++++++++++++ public/index.html | 144 +++ public/style.css | 1335 ++++++++++++++++++++++++ test/chat.test.js | 1063 +++++++++++++++++++ 11 files changed, 8027 insertions(+), 758 deletions(-) create mode 100644 API.md create mode 100644 public/app.js create mode 100644 public/index.html create mode 100644 public/style.css create mode 100644 test/chat.test.js diff --git a/API.md b/API.md new file mode 100644 index 0000000..9aeb9e0 --- /dev/null +++ b/API.md @@ -0,0 +1,641 @@ +# Steam Chat 服务 API 文档 + +本文档基于当前仓库中的 `chat.js` 实现整理,说明该服务对外提供的 HTTP 与 WebSocket API。 + +## 1. 启用服务 + +在 `config.js` 中启用 `chat` 配置即可。 + +### 最简写法 + +```js +module.exports = { + // ... + chat: true, +}; +``` + +等价于: + +```js +chat: { + enabled: true, + host: '0.0.0.0', + port: 3000, + wsPath: '/ws', + auth: { + username: 'admin', + password: 'change-me', + realm: 'Steam Chat', + trustProxy: false, + }, +} +``` + +### 完整写法 + +```js +chat: { + enabled: true, + host: '0.0.0.0', + port: 3000, + wsPath: '/ws', +} +``` + +## 2. 基本说明 + +- 默认监听地址:`0.0.0.0:3000` +- 默认 WebSocket 路径:`/ws` +- 可选 HTTP Basic Auth:当请求来源不是局域网/回环地址,且配置了 `chat.auth.username` 与 `chat.auth.password` 时,会要求输入用户名和密码 +- 反向代理支持:将 `chat.auth.trustProxy` 设为 `true` 后,会优先解析 `Forwarded`、`X-Forwarded-For`、`X-Real-IP` +- 根页面:`GET /` 会返回内置聊天页面 +- 历史记录来源:本地日志文件 `logs/chat.jsonl` +- 贴纸缓存目录:`logs/stickers` +- 图片缓存目录:`logs/images` +- 错误响应统一为: + +```json +{ "error": "错误信息" } +``` + +未认证时返回: + +```json +{ "error": "Authentication Required" } +``` + +## 3. 数据结构 + +### 3.1 历史消息项 `HistoryItem` + +```json +{ + "type": "message", + "date": "2026-03-20 10:00:00.000", + "echo": false, + "id": "7656119xxxxxxxxxx", + "name": "Friend", + "message": "hello", + "imageUrl": null, + "ordinal": 1, + "sentAt": null +} +``` + +字段说明: + +- `type`: `message` 或 `image` +- `date`: 格式通常为 `yyyy-mm-dd HH:MM:ss.l` +- `echo`: 是否为自己发送的消息 +- `id`: 会话对象 SteamID +- `name`: 显示名称 +- `message`: 文本消息内容;图片记录通常为空字符串 +- `imageUrl`: 图片消息的远程地址,没有则为 `null` +- `ordinal`: Steam 消息序号;图片记录通常为 `null` +- `sentAt`: 某些图片记录可能带 ISO 时间戳 + +### 3.2 会话摘要项 `ConversationSummary` + +```json +{ + "id": "7656119xxxxxxxxxx", + "name": "Friend", + "updatedAt": "2026-03-20 10:00:00.000", + "preview": "hello", + "lastType": "message", + "lastEcho": false, + "messageCount": 12 +} +``` + +字段说明: + +- `preview`: 最近一条消息的摘要,可能是普通文本,也可能是 `[图片]`、`[贴纸] xxx`、`[表情] xxx` +- `lastType`: `message` 或 `image` +- `lastEcho`: 最近一条是否为自己发送 +- `messageCount`: 当前日志窗口内该会话的消息数 + +## 4. HTTP API + +以下示例默认服务地址为 `http://127.0.0.1:3000`。 + +### 4.1 发送文本消息 + +**POST** `/message` + +兼容别名:**POST** `/` + +请求体: + +```json +{ + "id": "7656119xxxxxxxxxx", + "msg": "你好" +} +``` + +必填字段: + +- `id`: 对方 SteamID +- `msg`: 文本内容 + +成功响应:`200 OK` + +```json +{ + "type": "message", + "date": "2026-03-20 10:00:00.000", + "echo": true, + "id": "7656119xxxxxxxxxx", + "name": "MyName", + "message": "你好", + "imageUrl": null, + "ordinal": 42, + "sentAt": null +} +``` + +示例: + +```bash +curl -X POST http://127.0.0.1:3000/message \ + -H 'Content-Type: application/json' \ + -d '{"id":"7656119xxxxxxxxxx","msg":"hello"}' +``` + +### 4.2 发送图片 + +**POST** `/image` + +兼容别名:**POST** `/img` + +请求体支持两种方式: + +#### 方式 A:直接上传 base64 + +```json +{ + "id": "7656119xxxxxxxxxx", + "img": "iVBORw0KGgoAAAANSUhEUg..." +} +``` + +`img` 可以是: + +- 纯 base64 内容 +- `data:image/png;base64,...` 这种 Data URL + +#### 方式 B:让服务端下载远程图片后转发 + +```json +{ + "id": "7656119xxxxxxxxxx", + "url": "https://example.com/demo.png" +} +``` + +说明: + +- `id` 必填 +- `img` 与 `url` 至少提供一个 +- 如果两者同时提供,服务端优先使用 `url` + +成功响应:`200 OK` + +```json +{ + "type": "image", + "date": "2026-03-20 10:00:00.000", + "echo": true, + "id": "7656119xxxxxxxxxx", + "name": "MyName", + "message": "", + "imageUrl": "https://...", + "ordinal": null, + "sentAt": "2026-03-20T10:00:00.000Z" +} +``` + +示例: + +```bash +curl -X POST http://127.0.0.1:3000/image \ + -H 'Content-Type: application/json' \ + -d '{"id":"7656119xxxxxxxxxx","url":"https://example.com/demo.png"}' +``` + +### 4.3 获取历史记录 + +**GET** `/history` + +查询参数: + +- `id`:可选,仅返回指定 SteamID 的记录 +- `limit`:可选,返回条数上限,默认 `100`,最大 `500` + +示例: + +```bash +curl 'http://127.0.0.1:3000/history?id=7656119xxxxxxxxxx&limit=50' +``` + +成功响应: + +```json +{ + "items": [ + { + "type": "message", + "date": "2026-03-20 10:00:00.000", + "echo": false, + "id": "7656119xxxxxxxxxx", + "name": "Friend", + "message": "hello", + "imageUrl": null, + "ordinal": 1, + "sentAt": null + } + ] +} +``` + +说明: + +- 数据来自本地日志 `logs/chat.jsonl` +- 返回结果按时间升序排序;同一时间下按 `ordinal` 升序 + +### 4.4 获取最近会话摘要 + +**GET** `/conversations` + +查询参数: + +- `limit`:可选,默认 `500`,最大 `500` + +示例: + +```bash +curl 'http://127.0.0.1:3000/conversations?limit=200' +``` + +成功响应: + +```json +{ + "items": [ + { + "id": "7656119xxxxxxxxxx", + "name": "Friend", + "updatedAt": "2026-03-20 10:00:00.000", + "preview": "hello", + "lastType": "message", + "lastEcho": false, + "messageCount": 12 + } + ] +} +``` + +说明: + +- 这里的 `limit` 是“用于生成摘要的历史记录条数”,不是最终会话数上限 +- 返回结果按 `updatedAt` 倒序排列 + +### 4.5 代理贴纸图片 + +**GET** `/proxy/sticker/:type` + +示例: + +```bash +curl -o sticker.png 'http://127.0.0.1:3000/proxy/sticker/Sticker_MalteseCry' +``` + +说明: + +- 服务会尝试从 Steam 贴纸地址下载图片 +- 成功后缓存到 `logs/stickers` +- 成功响应内容类型固定为 `image/png` + +### 4.6 代理远程图片 + +**GET** `/proxy/image?url=...` + +示例: + +```bash +curl -o image.png 'http://127.0.0.1:3000/proxy/image?url=https%3A%2F%2Fexample.com%2Fa.png' +``` + +说明: + +- 服务会下载指定远程图片并缓存到 `logs/images` +- 响应 `Content-Type` 会尽量根据 URL 后缀或源响应头推断 +- `url` 必须是 `http://` 或 `https://` + +### 4.7 内置聊天页面 + +**GET** `/` + +返回一个内置 HTML 页面,页面内部通过 WebSocket 调用下文的实时接口。 + +## 5. WebSocket API + +连接地址: + +```text +ws://: +``` + +默认示例: + +```text +ws://127.0.0.1:3000/ws +``` + +### 5.1 连接建立后的消息 + +服务端在连接成功后会先主动发送: + +```json +{ + "type": "ready", + "data": { + "wsPath": "/ws" + } +} +``` + +### 5.2 客户端请求格式 + +所有请求均为 JSON。可选携带 `requestId`,服务端会原样带回,便于请求响应配对。 + +```json +{ + "type": "send_message", + "requestId": "req-1", + "id": "7656119xxxxxxxxxx", + "msg": "hello" +} +``` + +### 5.3 支持的请求类型 + +#### 发送文本消息 + +```json +{ + "type": "send_message", + "requestId": "req-1", + "id": "7656119xxxxxxxxxx", + "msg": "hello" +} +``` + +兼容别名:`type: "msg"` + +成功响应: + +```json +{ + "type": "message_sent", + "requestId": "req-1", + "data": { + "type": "message", + "date": "2026-03-20 10:00:00.000", + "echo": true, + "id": "7656119xxxxxxxxxx", + "name": "MyName", + "message": "hello", + "imageUrl": null, + "ordinal": 42, + "sentAt": null + } +} +``` + +#### 发送图片 + +```json +{ + "type": "send_image", + "requestId": "req-2", + "id": "7656119xxxxxxxxxx", + "url": "https://example.com/demo.png" +} +``` + +或: + +```json +{ + "type": "send_image", + "requestId": "req-2", + "id": "7656119xxxxxxxxxx", + "img": "iVBORw0KGgoAAAANSUhEUg..." +} +``` + +兼容别名:`type: "img"` + +成功响应: + +```json +{ + "type": "image_sent", + "requestId": "req-2", + "data": { + "type": "image", + "date": "2026-03-20 10:00:00.000", + "echo": true, + "id": "7656119xxxxxxxxxx", + "name": "MyName", + "message": "", + "imageUrl": "https://...", + "ordinal": null, + "sentAt": "2026-03-20T10:00:00.000Z" + } +} +``` + +#### 获取历史记录 + +```json +{ + "type": "get_history", + "requestId": "req-3", + "id": "7656119xxxxxxxxxx", + "limit": 50 +} +``` + +兼容别名:`type: "history"` + +成功响应: + +```json +{ + "type": "history", + "requestId": "req-3", + "data": { + "items": [] + } +} +``` + +#### 获取会话摘要 + +```json +{ + "type": "get_conversations", + "requestId": "req-4", + "limit": 200 +} +``` + +兼容别名:`type: "conversations"` + +成功响应: + +```json +{ + "type": "conversations", + "requestId": "req-4", + "data": { + "items": [] + } +} +``` + +#### 心跳 + +```json +{ + "type": "ping", + "requestId": "ping-1" +} +``` + +成功响应: + +```json +{ + "type": "pong", + "requestId": "ping-1", + "data": { + "now": "2026-03-20T10:00:00.000Z" + } +} +``` + +### 5.4 服务端主动推送事件 + +#### 文本消息广播 + +当服务收到 Steam 好友消息,或通过 HTTP / WebSocket 成功发送文本消息后,会广播: + +```json +{ + "type": "message", + "data": { + "type": "message", + "date": "2026-03-20 10:00:00.000", + "echo": false, + "id": "7656119xxxxxxxxxx", + "name": "Friend", + "message": "hello", + "imageUrl": null, + "ordinal": 1, + "sentAt": null + } +} +``` + +#### 图片发送广播 + +当通过服务成功发送图片后,会广播: + +```json +{ + "type": "image", + "data": { + "type": "image", + "date": "2026-03-20 10:00:00.000", + "echo": true, + "id": "7656119xxxxxxxxxx", + "name": "MyName", + "message": "", + "imageUrl": "https://...", + "ordinal": null, + "sentAt": "2026-03-20T10:00:00.000Z" + } +} +``` + +#### 错误消息 + +请求失败时,服务端会返回: + +```json +{ + "type": "error", + "requestId": "req-1", + "message": "错误信息" +} +``` + +若收到非法 JSON,则返回: + +```json +{ + "type": "error", + "message": "Invalid JSON" +} +``` + +## 6. 行为细节 + +### 6.1 去重策略 + +服务在本地发送文本消息后,会记录一个短期去重键;如果随后从 Steam 收到同一条 `friendMessageEcho`,15 秒内会避免重复广播。 + +### 6.2 图片发送 + +发送图片时依赖 Steam Web Session: + +- 服务会先等待 Steam 登录和 Web Session 就绪 +- 如果首次上传失败,会尝试刷新一次 Web Session 后重试 + +### 6.3 历史记录来源 + +`/history` 和 `/conversations` 都基于本地日志文件,不会主动向 Steam 拉取远端历史消息。 + +## 7. 快速示例 + +### HTTP 发送消息 + +```bash +curl -X POST http://127.0.0.1:3000/message \ + -H 'Content-Type: application/json' \ + -d '{"id":"7656119xxxxxxxxxx","msg":"hello"}' +``` + +### WebSocket 发送消息 + +```js +const ws = new WebSocket('ws://127.0.0.1:3000/ws'); + +ws.onmessage = (event) => { + console.log(JSON.parse(event.data)); +}; + +ws.onopen = () => { + ws.send(JSON.stringify({ + type: 'send_message', + requestId: 'req-1', + id: '7656119xxxxxxxxxx', + msg: 'hello', + })); +}; +``` diff --git a/chat.js b/chat.js index dacaebb..d915cc5 100644 --- a/chat.js +++ b/chat.js @@ -1,279 +1,1728 @@ -const client = require("./client") -const axios = require('axios'); const http = require('http'); const fs = require('fs'); -const { once } = require("node:events"); +const crypto = require('crypto'); +const path = require('path'); +const { once } = require('node:events'); + +const axios = require('axios'); const dateformat = require('@matteo.collina/dateformat'); const WebSocket = require('ws'); -const logger = client.logger -const steamUser = client.steamUser +const CHAT_LOG_FILE = './logs/chat.jsonl'; +const STICKER_CACHE_DIR = './logs/stickers'; +const IMAGE_CACHE_DIR = './logs/images'; -fs.mkdir("./logs", { recursive: true }, (err) => { - if (err) { - logger.error("an error occurred while creating the logs directory: " + err); +function normalizeAuthConfig(rawAuth) { + const defaultConfig = { + username: '', + password: '', + realm: 'Steam Chat', + trustProxy: false, + }; + + if (!rawAuth || typeof rawAuth !== 'object') { + return defaultConfig; } -}); -async function sendMsg(req, res) { - let body = ''; - req.on('data', chunk => { - body += chunk.toString(); // 将Buffer转换为字符串 - }); + return { + username: rawAuth.username || '', + password: rawAuth.password || '', + realm: rawAuth.realm || defaultConfig.realm, + trustProxy: rawAuth.trustProxy === true, + }; +} - await once(req, 'end'); +function normalizeChatConfig(rawConfig) { + const defaultConfig = { + enabled: Boolean(rawConfig), + host: '0.0.0.0', + port: 3000, + wsPath: '/ws', + auth: normalizeAuthConfig(null), + }; - let requests = JSON.parse(body); + if (!rawConfig || typeof rawConfig !== 'object') { + return defaultConfig; + } - console.log(requests) + return { + enabled: rawConfig.enabled !== false, + host: rawConfig.host || defaultConfig.host, + port: rawConfig.port || defaultConfig.port, + wsPath: rawConfig.wsPath || defaultConfig.wsPath, + auth: normalizeAuthConfig(rawConfig.auth), + }; +} + +function normalizeIpAddress(rawAddress) { + let address = String(rawAddress || '').trim(); + if (!address) { + return ''; + } + + const forwardedMatch = address.match(/^for=(.+)$/i); + if (forwardedMatch) { + address = forwardedMatch[1]; + } + + address = address.replace(/^"|"$/g, ''); + + if (address.startsWith('[')) { + const closingIndex = address.indexOf(']'); + if (closingIndex !== -1) { + address = address.slice(1, closingIndex); + } + } else if ((address.match(/:/g) || []).length === 1 && address.includes('.')) { + address = address.split(':')[0]; + } + + address = address.replace(/^\[|\]$/g, '').replace(/%[0-9a-z]+$/i, '').trim().toLowerCase(); + + if (address.startsWith('::ffff:')) { + return address.slice('::ffff:'.length); + } + + return address; +} + +function parseForwardedHeader(headerValue) { + if (!headerValue) { + return ''; + } + + for (const part of String(headerValue).split(',')) { + for (const segment of part.split(';')) { + const match = segment.trim().match(/^for=(.+)$/i); + if (match && match[1]) { + return normalizeIpAddress(match[1]); + } + } + } + + return ''; +} + +function getClientIp(req, trustProxy = false) { + const headers = req && req.headers ? req.headers : {}; + + if (trustProxy) { + const forwarded = parseForwardedHeader(headers.forwarded); + if (forwarded) { + return forwarded; + } + + const xForwardedFor = String(headers['x-forwarded-for'] || '') + .split(',') + .map((item) => normalizeIpAddress(item)) + .find(Boolean); + if (xForwardedFor) { + return xForwardedFor; + } + + const xRealIp = normalizeIpAddress(headers['x-real-ip']); + if (xRealIp) { + return xRealIp; + } + } + + return normalizeIpAddress( + (req && req.socket && req.socket.remoteAddress) + || (req && req.connection && req.connection.remoteAddress) + || '', + ); +} + +function isLanIp(rawAddress) { + const address = normalizeIpAddress(rawAddress); + if (!address) { + return false; + } + + if (address === '::1' || address === 'localhost') { + return true; + } + + if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(address)) { + if (address.startsWith('10.') || address.startsWith('127.') || address.startsWith('192.168.') || address.startsWith('169.254.')) { + return true; + } + + const octets = address.split('.').map((item) => Number.parseInt(item, 10)); + if (octets.length === 4 && octets.every((item) => Number.isInteger(item) && item >= 0 && item <= 255)) { + return octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31; + } + + return false; + } + + return address.startsWith('fc') + || address.startsWith('fd') + || address.startsWith('fe8') + || address.startsWith('fe9') + || address.startsWith('fea') + || address.startsWith('feb'); +} + +function isAuthEnabled(authConfig) { + return Boolean(authConfig && authConfig.username && authConfig.password); +} + +function hashSecret(value) { + return crypto.createHash('sha256').update(String(value || '')).digest(); +} + +function safeEqual(left, right) { + return crypto.timingSafeEqual(hashSecret(left), hashSecret(right)); +} + +function parseBasicAuthHeader(headerValue) { + const match = String(headerValue || '').match(/^Basic\s+(.+)$/i); + if (!match) { + return null; + } try { - let resp = await sendFriendMessage(requests.id, requests.msg); - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - let messageEncoded = await messageToJson(resp, true); - res.end(messageEncoded); + const decoded = Buffer.from(match[1], 'base64').toString('utf8'); + const separatorIndex = decoded.indexOf(':'); + if (separatorIndex === -1) { + return null; + } + + return { + username: decoded.slice(0, separatorIndex), + password: decoded.slice(separatorIndex + 1), + }; } catch (err) { - logger.error("failed to send friend message", err); - res.statusCode = 500; - res.setHeader('Content-Type', 'text/plain'); - res.end('Internal Server Error\n'); + return null; } } -function sendFriendMessage(uid, msg) { - return new Promise((resolve, reject) => { - client.steamUser.chat.sendFriendMessage(uid, msg, (err, response) => { - if (err) { - reject(err); - return - } +function isAuthorized(req, authConfig) { + if (!isAuthEnabled(authConfig)) { + return true; + } - client.getUserInfo(client.steamUser.steamID).then((sender) => { - fs.appendFile("./logs/chat.jsonl", JSON.stringify({ - date: dateToString(response.server_timestamp), - echo: true, - id: uid, - name: sender.player_name, - message: response.modified_message, - ordinal: response.ordinal, - }) + "\n", (e) => { - if (e) { - logger.error("an error occurred while writing chat log file: " + e); - } - }); - }) + const credentials = parseBasicAuthHeader(req && req.headers ? req.headers.authorization : ''); + if (!credentials) { + return false; + } - resp = { - server_timestamp: response.server_timestamp, - steamid_friend: uid, - message: response.modified_message, - ordinal: response.ordinal, - } - onSteamMessage(resp, true); - resolve(resp); - }) + return safeEqual(credentials.username, authConfig.username) + && safeEqual(credentials.password, authConfig.password); +} + +function requiresHttpAuth(req, chatConfig) { + if (!isAuthEnabled(chatConfig && chatConfig.auth)) { + return false; + } + + return !isLanIp(getClientIp(req, chatConfig.auth.trustProxy)); +} + +function normalizeWsRequest(payload) { + switch (payload.type) { + case 'msg': + case 'send_message': + return { + action: 'send_message', + requestId: payload.requestId, + id: payload.id, + msg: payload.msg, + }; + case 'img': + case 'send_image': + return { + action: 'send_image', + requestId: payload.requestId, + id: payload.id, + img: payload.img, + url: payload.url, + }; + case 'history': + case 'get_history': + return { + action: 'get_history', + requestId: payload.requestId, + id: payload.id, + limit: payload.limit, + }; + case 'conversations': + case 'get_conversations': + return { + action: 'get_conversations', + requestId: payload.requestId, + limit: payload.limit, + }; + case 'emoticons': + case 'get_emoticons': + return { + action: 'get_emoticons', + requestId: payload.requestId, + }; + case 'ping': + return { + action: 'ping', + requestId: payload.requestId, + }; + default: + return { + action: payload.type, + requestId: payload.requestId, + ...payload, + }; + } +} + +function buildMessageKey(message) { + return `${message.id}:${message.ordinal}:${message.message}`; +} + +function normalizeHistoryEntry(entry) { + if (!entry || typeof entry !== 'object') { + return null; + } + + return { + type: entry.type || (entry.imageUrl ? 'image' : 'message'), + date: entry.date || '', + echo: Boolean(entry.echo), + id: entry.id || '', + name: entry.name || '', + message: entry.message || '', + imageUrl: entry.imageUrl || null, + ordinal: typeof entry.ordinal === 'number' ? entry.ordinal : null, + sentAt: entry.sentAt || null, + }; +} + +function sanitizeLimit(limit, fallback = 100) { + const value = Number.parseInt(limit, 10); + if (!Number.isFinite(value) || value <= 0) { + return fallback; + } + return Math.min(value, 500); +} + +function extractStickerType(message) { + if (typeof message !== 'string') { + return null; + } + + const match = message.match(/\[sticker\s+type="([^"]+)"/i); + return match ? match[1] : null; +} + +function extractEmoticonNames(message) { + if (typeof message !== 'string') { + return []; + } + + const names = new Set(); + + for (const match of message.matchAll(/\[emoticon\s+name="([^"]+)"\](?:\[\/emoticon\])?/gi)) { + if (match[1]) { + names.add(match[1]); + } + } + + for (const match of message.matchAll(/\[emoticon\]([^\[]+)\[\/emoticon\]/gi)) { + if (match[1]) { + names.add(match[1].trim()); + } + } + + for (const match of message.matchAll(/(^|\s):([a-z0-9_][a-z0-9_\-]*):(?=\s|$|[!?,.])/gi)) { + if (match[2]) { + names.add(match[2]); + } + } + + return [...names]; +} + +function extractImageUrls(message) { + if (typeof message !== 'string') { + return []; + } + + const urls = new Set(); + + for (const match of message.matchAll(/\[img\](https?:\/\/[^\s[\]]+?)\[\/img\]/gi)) { + if (match[1]) { + urls.add(match[1]); + } + } + + for (const match of message.matchAll(/\[img\s+src=(https?:\/\/\S+?)[\s\]]/gi)) { + if (match[1]) { + urls.add(match[1]); + } + } + + for (const match of message.matchAll(/]*?\bsrc=["'](https?:\/\/[^"']+)["'][^>]*>/gi)) { + if (match[1]) { + urls.add(match[1]); + } + } + + for (const match of message.matchAll(/https?:\/\/\S+?(?:png|jpe?g|gif|webp|bmp)(?:\?\S*)?/gi)) { + if (match[0]) { + urls.add(match[0]); + } + } + + return [...urls]; +} + +function parseBbCodeAttributes(rawAttributes) { + const attrs = {}; + const content = String(rawAttributes || ''); + const attributeRegex = /([a-z][a-z0-9_-]*)=(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^\s"'=<>`]+))/gi; + let match; + + while ((match = attributeRegex.exec(content)) !== null) { + const key = match[1].toLowerCase(); + const value = match[2] ?? match[3] ?? match[4] ?? ''; + attrs[key] = value.replace(/\\(["'])/g, '$1'); + } + + return attrs; +} + +function extractOpenGraphEmbeds(message) { + if (typeof message !== 'string') { + return []; + } + + const embeds = []; + + for (const match of message.matchAll(/\[og\s+([^\]]+)\]([\s\S]*?)\[\/og\]/gi)) { + const attrs = parseBbCodeAttributes(match[1] || ''); + const fallbackUrl = (match[2] || '').trim(); + + embeds.push({ + url: attrs.url || fallbackUrl, + img: attrs.img || null, + title: attrs.title || '', + }); + } + + return embeds.filter((item) => item.url); +} + +function buildSteamEmoticonUrl(name, large = true) { + const normalized = String(name || '').trim().replace(/^:+|:+$/g, ''); + if (!normalized) { + return null; + } + + const sizePath = large ? 'emoticonlarge' : 'emoticon'; + return `https://steamcommunity-a.akamaihd.net/economy/${sizePath}/${encodeURIComponent(normalized)}`; +} + +function buildSteamStickerCandidateUrls(type) { + const normalized = String(type || '').trim(); + if (!normalized) { + return []; + } + + return [ + `https://steamcommunity-a.akamaihd.net/economy/sticker/${encodeURIComponent(normalized)}`, + `https://steamcommunity-a.akamaihd.net/economy/stickerlarge/${encodeURIComponent(normalized)}`, + `https://steamcommunity.com/economy/sticker/${encodeURIComponent(normalized)}`, + `https://steamcommunity.com/economy/stickerlarge/${encodeURIComponent(normalized)}`, + ]; +} + +function buildStickerCachePath(type) { + const normalized = String(type || '').trim(); + if (!normalized) { + return path.join(STICKER_CACHE_DIR, 'unknown.png'); + } + + return path.join(STICKER_CACHE_DIR, `${encodeURIComponent(normalized)}.bin`); +} + +function buildImageCachePaths(url) { + const normalized = String(url || '').trim(); + const hash = crypto.createHash('sha1').update(normalized).digest('hex'); + return { + dataPath: path.join(IMAGE_CACHE_DIR, `${hash}.bin`), + metaPath: path.join(IMAGE_CACHE_DIR, `${hash}.json`), + }; +} + +function guessImageContentType(url, fallback = 'image/png') { + const pathname = String(url || '').split('?')[0].toLowerCase(); + if (pathname.endsWith('.png')) { + return 'image/png'; + } + if (pathname.endsWith('.jpg') || pathname.endsWith('.jpeg')) { + return 'image/jpeg'; + } + if (pathname.endsWith('.gif')) { + return 'image/gif'; + } + if (pathname.endsWith('.webp')) { + return 'image/webp'; + } + if (pathname.endsWith('.bmp')) { + return 'image/bmp'; + } + if (pathname.endsWith('.svg')) { + return 'image/svg+xml'; + } + return fallback; +} + +function buildConversationPreview(entry) { + if (!entry) { + return ''; + } + + if (entry.type === 'image' || entry.imageUrl) { + return '[图片]'; + } + + const ogEmbeds = extractOpenGraphEmbeds(entry.message); + if (ogEmbeds.length > 0) { + return ogEmbeds[0].title || ogEmbeds[0].url || '[链接预览]'; + } + + if (extractImageUrls(entry.message).length > 0) { + return '[图片]'; + } + + const stickerType = extractStickerType(entry.message); + if (stickerType) { + return `[贴纸] ${stickerType.replace(/^Sticker_/, '')}`; + } + + const emoticonNames = extractEmoticonNames(entry.message); + const emoticonOnlyText = String(entry.message || '') + .replace(/\[emoticon\s+name="([^"]+)"\](?:\[\/emoticon\])?/gi, (_, name) => `:${name}:`) + .replace(/\[emoticon\]([^\[]+)\[\/emoticon\]/gi, (_, name) => `:${name.trim()}:`) + .trim() + .replace(/\s+/g, ''); + if (emoticonNames.length && emoticonOnlyText === emoticonNames.map((name) => `:${name}:`).join('')) { + return `[表情] ${emoticonNames.join(' ')}`; + } + + return String(entry.message || '').trim().replace(/\s+/g, ' ').slice(0, 60); +} + +function sortHistoryItems(items) { + return [...items].sort((left, right) => { + const leftDate = left.date || left.sentAt || ''; + const rightDate = right.date || right.sentAt || ''; + + if (leftDate !== rightDate) { + return leftDate.localeCompare(rightDate); + } + + const leftOrdinal = typeof left.ordinal === 'number' ? left.ordinal : Number.MAX_SAFE_INTEGER; + const rightOrdinal = typeof right.ordinal === 'number' ? right.ordinal : Number.MAX_SAFE_INTEGER; + + return leftOrdinal - rightOrdinal; }); } -function logSendMsg(uid, response) { - client.getUserInfo(client.steamUser.steamID).then((sender) => { - fs.appendFile("./logs/chat.jsonl", JSON.stringify({ - date: dateToString(response.server_timestamp), +function buildConversationSummaries(items) { + const conversations = new Map(); + + for (const entry of sortHistoryItems(items)) { + if (!entry.id) { + continue; + } + + const current = conversations.get(entry.id) || { + id: entry.id, + name: entry.name || '', + updatedAt: entry.date || entry.sentAt || '', + preview: buildConversationPreview(entry), + lastType: entry.type || (entry.imageUrl ? 'image' : 'message'), + lastEcho: Boolean(entry.echo), + messageCount: 0, + }; + + current.name = entry.name || current.name; + current.updatedAt = entry.date || entry.sentAt || current.updatedAt; + current.preview = buildConversationPreview(entry) || current.preview; + current.lastType = entry.type || (entry.imageUrl ? 'image' : 'message'); + current.lastEcho = Boolean(entry.echo); + current.messageCount += 1; + + conversations.set(entry.id, current); + } + + return [...conversations.values()].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)); +} + +const PUBLIC_DIR = path.join(__dirname, 'public'); + +const STATIC_CONTENT_TYPES = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', +}; + +function serveStaticFile(res, filePath) { + const ext = path.extname(filePath) || '.html'; + const contentType = STATIC_CONTENT_TYPES[ext]; + if (!contentType) { + res.writeHead(404); + res.end('Not Found'); + return; + } + + const fullPath = path.join(PUBLIC_DIR, filePath); + const normalized = path.normalize(fullPath); + if (!normalized.startsWith(PUBLIC_DIR)) { + res.writeHead(403); + res.end('Forbidden'); + return; + } + + fs.readFile(normalized, (err, data) => { + if (err) { + res.writeHead(404); + res.end('Not Found'); + return; + } + res.writeHead(200, { 'Content-Type': contentType }); + res.end(data); + }); +} + +function getDefaultDeps() { + const config = require('./config.js'); + const client = require('./client'); + + return { + rawChatConfig: config.chat, + client, + logger: client.logger, + steamUser: client.steamUser, + steamCommunity: client.steamCommunity, + fsModule: fs, + httpModule: http, + onceFn: once, + axiosInstance: axios, + WebSocketImpl: WebSocket, + dateToString: (date) => dateformat(date, 'yyyy-mm-dd HH:MM:ss.l'), + }; +} + +function createChatService(customDeps = {}) { + const baseDeps = customDeps.useDefaultDeps === false ? {} : getDefaultDeps(); + const deps = { + ...baseDeps, + ...customDeps, + }; + + delete deps.useDefaultDeps; + + const { + rawChatConfig, + client, + logger, + steamUser, + steamCommunity, + fsModule, + httpModule, + onceFn, + axiosInstance, + WebSocketImpl, + dateToString, + } = deps; + + const chatConfig = normalizeChatConfig(rawChatConfig); + const recentSelfMessages = new Map(); + const recentSelfImageUrls = new Map(); + const pendingStickerFetches = new Map(); + const pendingImageFetches = new Map(); + let started = false; + + fsModule.mkdir('./logs', { recursive: true }, (err) => { + if (err) { + logger.error('an error occurred while creating the logs directory: ' + err); + } + }); + fsModule.mkdir(STICKER_CACHE_DIR, { recursive: true }, (err) => { + if (err) { + logger.error('an error occurred while creating the sticker cache directory: ' + err); + } + }); + fsModule.mkdir(IMAGE_CACHE_DIR, { recursive: true }, (err) => { + if (err) { + logger.error('an error occurred while creating the image cache directory: ' + err); + } + }); + + const server = httpModule.createServer(handleHttp); + const wss = new WebSocketImpl.Server({ + server, + path: chatConfig.wsPath, + verifyClient: (info, done) => { + if (!requiresHttpAuth(info.req, chatConfig) || isAuthorized(info.req, chatConfig.auth)) { + done(true); + return; + } + + done(false, 401, 'Authentication Required', { + 'WWW-Authenticate': `Basic realm="${String(chatConfig.auth.realm || 'Steam Chat').replace(/"/g, '\\"')}"`, + }); + }, + }); + + async function readRequestBody(req) { + let body = ''; + req.on('data', (chunk) => { + body += chunk.toString(); + }); + await onceFn(req, 'end'); + return body; + } + + async function readJsonBody(req) { + const body = await readRequestBody(req); + if (!body) { + return {}; + } + + try { + return JSON.parse(body); + } catch (err) { + const error = new Error('Invalid JSON'); + error.code = 400; + throw error; + } + } + + function sendJson(res, statusCode, payload) { + res.statusCode = statusCode; + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.end(JSON.stringify(payload)); + } + + function sendAuthRequired(res) { + res.statusCode = 401; + res.setHeader('WWW-Authenticate', `Basic realm="${String(chatConfig.auth.realm || 'Steam Chat').replace(/"/g, '\\"')}"`); + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.end(JSON.stringify({ error: 'Authentication Required' })); + } + + function sendWs(ws, payload) { + if (ws.readyState === WebSocketImpl.OPEN) { + ws.send(JSON.stringify(payload)); + } + } + + function broadcastWs(payload) { + const encoded = JSON.stringify(payload); + wss.clients.forEach((ws) => { + if (ws.readyState === WebSocketImpl.OPEN) { + ws.send(encoded); + } + }); + } + + function appendLogEntry(entry) { + fsModule.appendFile(CHAT_LOG_FILE, JSON.stringify(entry) + '\n', (err) => { + if (err) { + logger.error('an error occurred while writing chat log file: ' + err); + } + }); + } + + async function readFileIfExists(filePath) { + return new Promise((resolve, reject) => { + fsModule.readFile(filePath, (err, content) => { + if (err) { + if (err.code === 'ENOENT') { + resolve(null); + return; + } + reject(err); + return; + } + + resolve(content); + }); + }); + } + + async function writeFileAsync(filePath, content) { + return new Promise((resolve, reject) => { + if (typeof fsModule.writeFile !== 'function') { + resolve(); + return; + } + + fsModule.writeFile(filePath, content, (err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); + } + + async function readJsonIfExists(filePath) { + const content = await readFileIfExists(filePath); + if (!content) { + return null; + } + + try { + return JSON.parse(Buffer.isBuffer(content) ? content.toString('utf8') : String(content)); + } catch (err) { + return null; + } + } + + function appendOutgoingLog(uid, response) { + client.getUserInfo(steamUser.steamID).then((sender) => { + appendLogEntry({ + type: 'message', + date: dateToString(response.server_timestamp), + echo: true, + id: uid, + name: sender.player_name, + message: response.modified_message, + ordinal: response.ordinal, + }); + }); + } + + async function appendOutgoingImageLog(uid, imageUrl) { + const sender = await client.getUserInfo(steamUser.steamID, () => {}); + const entry = { + type: 'image', + date: dateToString(new Date()), echo: true, id: uid, name: sender.player_name, - message: response.modified_message, - ordinal: response.ordinal, - }) + "\n", (e) => { - if (e) { - logger.error("an error occurred while writing chat log file: " + e); - } - }); - }) -} - -async function sendImg(req, res) { - let body = ''; - req.on('data', chunk => { - body += chunk.toString(); // 将Buffer转换为字符串 - }); - - await once(req, 'end'); - - let requests = JSON.parse(body); - - res.setHeader('Content-Type', 'text/plain'); - try { - let url = await sendImageToUser(requests.id, requests.img, requests.url); - res.statusCode = 200; - res.end(url); - } catch (err) { - const { code = 500, message = "Internal Server Error" } = err; - res.statusCode = code; - res.end(message); + imageUrl, + ordinal: null, + sentAt: new Date().toISOString(), + }; + appendLogEntry(entry); + return entry; } -} -function sendImageToUser(uid, img, url) { - return new Promise(async (resolve, reject) => { - if (img) { - console.log({ - uid: uid, - img: img.length, - }); - } else { - console.log({ - uid: uid, - url: url, - }); + async function encodeSteamMessage(message, echo) { + let friendId = message.steamid_friend; + if (typeof friendId !== 'string') { + friendId = friendId.getSteamID64(); } - if (url) { - img = await readUrlAsBuffer(url) - } else if (img) { - img = Buffer.from(img, 'base64'); - } else { - reject({ code: 400, message: "Bad Request" }); + const friend = await client.getUserInfo(friendId, () => {}); + + return { + type: 'message', + date: dateToString(message.server_timestamp), + echo, + id: friendId, + name: friend.player_name, + message: message.message, + ordinal: message.ordinal, + imageUrl: null, + sentAt: null, + }; + } + + function rememberSelfMessage(message) { + const key = buildMessageKey(message); + recentSelfMessages.set(key, Date.now() + 15000); + const timer = setTimeout(() => { + recentSelfMessages.delete(key); + }, 15000); + + if (typeof timer.unref === 'function') { + timer.unref(); + } + } + + function wasRecentlyBroadcasted(message) { + const key = buildMessageKey(message); + const expiresAt = recentSelfMessages.get(key); + if (!expiresAt) { + return false; + } + + if (expiresAt < Date.now()) { + recentSelfMessages.delete(key); + return false; + } + + return true; + } + + async function broadcastSteamMessage(message, echo, { dedupe = false } = {}) { + const data = await encodeSteamMessage(message, echo); + if (dedupe && wasRecentlyBroadcasted(data)) { + return; + } + if (dedupe && wasRecentImageEcho(data)) { return; } - client.steamCommunity.sendImageToUser(uid, img, function (err, imageUrl) { - if (err) { - logger.error("an error occurred while sending image: ", err); - client.steamUser.webLogOn(); - reject({}); - return - } - - resolve(imageUrl); + broadcastWs({ + type: 'message', + data, }); - }) -} - -async function readUrlAsBuffer(url) { - try { - const response = await axios.get(url, { responseType: 'arraybuffer' }); - return Buffer.from(response.data); - } catch (err) { - throw new Error(`Failed to fetch URL: ${err.message}`); } -} -function readFileAsBuffer(filePath) { - return new Promise((resolve, reject) => { - fs.readFile(filePath, (err, data) => { - if (err) { - return reject(err); + async function getEmoticonList() { + await ensureWebSession(); + + const EMsg = require('steam-user/enums/EMsg'); + const msgKey = EMsg.ClientEmoticonList; + + function removeHandler(handler) { + const handlers = steamUser._handlerManager._handlers[msgKey]; + if (handlers) { + const idx = handlers.indexOf(handler); + if (idx !== -1) { + handlers.splice(idx, 1); + } } - resolve(data); - }); - }); -} - -function dateToString(date) { - return dateformat(date, "yyyy-mm-dd HH:MM:ss.l"); -} - -async function handleHttp(req, res) { - try { - if (req.url == "/img") { - await sendImg(req, res); - } else { - await sendMsg(req, res); } - } catch (e) { - console.error("An error occurred while processing the request: ", e); - res.statusCode = 500; - res.setHeader('Content-Type', 'text/plain'); - res.end('Internal Server Error\n'); + + const body = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + removeHandler(handler); + reject(new Error('getEmoticonList timed out')); + }, 10000); + if (typeof timeout.unref === 'function') { + timeout.unref(); + } + function handler(body) { + clearTimeout(timeout); + removeHandler(handler); + resolve(body); + } + steamUser._handlerManager.add(msgKey, handler); + steamUser._send(EMsg.ClientGetEmoticonList, {}); + }); + + const emoticons = (body.emoticons || []).map((e) => ({ + name: String(e.name || '').replace(/^:+|:+$/g, ''), + count: e.count, + use_count: e.use_count || 0, + time_last_used: e.time_last_used, + appid: e.appid, + })); + + const stickers = (body.stickers || []).map((s) => ({ + name: s.name, + count: s.count, + use_count: s.use_count || 0, + time_last_used: s.time_last_used, + appid: s.appid, + })); + + return { emoticons, stickers }; } -} -function handleWs(ws) { - console.log('WebSocket connection established.'); + function sendFriendMessage(uid, msg) { + return new Promise((resolve, reject) => { + if (!uid || typeof msg !== 'string') { + reject(new Error('id and msg are required')); + return; + } + + steamUser.chat.sendFriendMessage(uid, msg, (err, response) => { + if (err) { + reject(err); + return; + } + + appendOutgoingLog(uid, response); + + resolve({ + server_timestamp: response.server_timestamp, + steamid_friend: uid, + message: response.modified_message, + ordinal: response.ordinal, + }); + }); + }); + } + + async function ensureWebSession() { + await client.steamLoginPromise; + await client.steamWebLoginPromise; + } + + function isTransientNetworkError(err) { + const code = String(err && err.code ? err.code : '').toUpperCase(); + const message = String(err && err.message ? err.message : '').toLowerCase(); + + if ([ + 'ECONNRESET', + 'ECONNABORTED', + 'ETIMEDOUT', + 'EPIPE', + 'EAI_AGAIN', + 'ENETUNREACH', + 'EHOSTUNREACH', + 'ECONNREFUSED', + ].includes(code)) { + return true; + } + + return message.includes('client network socket disconnected before secure tls connection was established') + || message.includes('socket disconnected before secure tls connection was established') + || message.includes('tls connection') + || message.includes('socket hang up'); + } + + function isLikelyExpiredWebSessionError(err) { + const code = String(err && err.code ? err.code : '').toUpperCase(); + const message = String(err && err.message ? err.message : '').toLowerCase(); + + if (code === 'ESESSIONEXPIRED' || code === 'EWEBSESSION') { + return true; + } + + return message.includes('session') + || message.includes('cookie') + || message.includes('not logged in') + || message.includes('access denied') + || message.includes('forbidden'); + } + + async function waitForFreshWebSession(timeoutMs = 15000) { + let timer = null; - ws.on('message', (message) => { - let data; try { - data = JSON.parse(message); - } catch { - ws.close(1003, 'Invalid JSON'); - return; + await Promise.race([ + onceFn(steamUser, 'webSession'), + new Promise((_, reject) => { + timer = setTimeout(() => { + const error = new Error(`Timed out after ${timeoutMs}ms while waiting for Steam web session`); + error.code = 'WEB_SESSION_TIMEOUT'; + reject(error); + }, timeoutMs); + }), + ]); + } finally { + if (timer) { + clearTimeout(timer); + } } - logger.info('Received message:', data); - - switch (data.type) { - case "msg": - sendFriendMessage(data.id, data.msg); - break; - case "img": - sendImageToUser(data.id, data.img, data.url); - break - } - }); - - ws.on('close', () => { - logger.info('WebSocket connection closed.'); - }); - - ws.on('error', (err) => { - logger.error('WebSocket error:', err); - }); -} - -async function messageToJson(message, echo) { - let friendId = message.steamid_friend - if (typeof friendId !== 'string') { - friendId = friendId.getSteamID64(); } - let sender = await client.getUserInfo(echo ? steamUser.steamID : friendId, (ignore) => { }); - - return JSON.stringify({ - date: dateToString(message.server_timestamp), - echo: echo, - id: friendId, - name: sender.player_name, - message: message.message, - ordinal: message.ordinal, - }) -} - -async function onSteamMessage(message, echo) { - let messageEncoded = await messageToJson(message, echo); - - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(messageEncoded); + async function readUrlAsBuffer(url) { + try { + const response = await axiosInstance.get(url, { responseType: 'arraybuffer' }); + return Buffer.from(response.data); + } catch (err) { + const error = new Error(`Failed to fetch URL: ${err.message}`); + error.code = 400; + throw error; } - }); -} + } -client.steamLoginPromise.then(() => { - steamUser.chat.on("friendMessage", (message) => { - // noinspection JSIgnoredPromiseFromCall - onSteamMessage(message, false); - }); + async function parseImageBuffer({ img, url }) { + if (url) { + return readUrlAsBuffer(url); + } - steamUser.chat.on("friendMessageEcho", (message) => { - // noinspection JSIgnoredPromiseFromCall - onSteamMessage(message, true); - }); + if (img) { + const normalized = String(img).includes(',') ? String(img).split(',').pop() : String(img); + return Buffer.from(normalized, 'base64'); + } - try { - const server = http.createServer(handleHttp); + const error = new Error('img or url is required'); + error.code = 400; + throw error; + } - // 创建 WebSocket 服务器,并限制路径为 /ws - globalThis.wss = new WebSocket.Server({ server, path: '/ws' }); + function uploadImageToUser(uid, imageBuffer) { + return new Promise((resolve, reject) => { + steamCommunity.sendImageToUser(uid, imageBuffer, (err, imageUrl) => { + if (err) { + reject(err); + return; + } + + resolve(imageUrl); + }); + }); + } + + async function sendImageToUser(uid, img, url) { + if (!uid) { + const error = new Error('id is required'); + error.code = 400; + throw error; + } + + await ensureWebSession(); + + const imageBuffer = await parseImageBuffer({ img, url }); + + try { + return await uploadImageToUser(uid, imageBuffer); + } catch (err) { + let uploadError = err; + + if (isTransientNetworkError(uploadError)) { + logger.warn('temporary network error while sending image, retrying once', { + id: uid, + error: uploadError.message, + code: uploadError.code || null, + }); + + try { + return await uploadImageToUser(uid, imageBuffer); + } catch (retryErr) { + uploadError = retryErr; + } + } + + if (!isLikelyExpiredWebSessionError(uploadError) && !isTransientNetworkError(uploadError)) { + logger.error('an error occurred while sending image', uploadError); + throw uploadError; + } + + logger.warn('failed to send image, trying to refresh web session', { + id: uid, + error: uploadError.message, + code: uploadError.code || null, + }); + + try { + steamUser.webLogOn(); + await waitForFreshWebSession(); + return await uploadImageToUser(uid, imageBuffer); + } catch (retryErr) { + logger.error('an error occurred while sending image', retryErr); + throw retryErr; + } + } + } + + function parseLogLines(content) { + return String(content || '') + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + try { + return normalizeHistoryEntry(JSON.parse(line)); + } catch (err) { + logger.warn('skip invalid chat log line', { line }); + return null; + } + }) + .filter(Boolean); + } + + async function readChatHistory({ id, limit } = {}) { + const lines = await new Promise((resolve, reject) => { + fsModule.readFile(CHAT_LOG_FILE, 'utf8', (err, content) => { + if (err) { + if (err.code === 'ENOENT') { + resolve(''); + return; + } + reject(err); + return; + } + + resolve(content); + }); + }); + + const maxItems = sanitizeLimit(limit, 100); + let items = parseLogLines(lines); + + if (id) { + items = items.filter((entry) => entry.id === id); + } + + if (items.length > maxItems) { + items = items.slice(-maxItems); + } + + return sortHistoryItems(items); + } + + async function readConversationSummaries({ limit } = {}) { + const items = await readChatHistory({ limit: sanitizeLimit(limit, 500) }); + return buildConversationSummaries(items); + } + + async function fetchStickerBuffer(type) { + const normalizedType = String(type || '').trim(); + const cachePath = buildStickerCachePath(normalizedType); + const cached = await readFileIfExists(cachePath); + if (cached) { + return cached; + } + + const pendingFetch = pendingStickerFetches.get(normalizedType); + if (pendingFetch) { + return pendingFetch; + } + + const urls = buildSteamStickerCandidateUrls(normalizedType); + let lastError = null; + const fetchPromise = (async () => { + for (const url of urls) { + try { + const response = await axiosInstance.get(url, { responseType: 'arraybuffer' }); + const buffer = Buffer.from(response.data); + if (!buffer.length) { + continue; + } + + try { + await writeFileAsync(cachePath, buffer); + } catch (writeErr) { + logger.warn('failed to write sticker cache', { type: normalizedType, error: writeErr.message }); + } + + return buffer; + } catch (err) { + lastError = err; + } + } + + const error = new Error(`Failed to fetch sticker: ${normalizedType}`); + error.code = 404; + error.cause = lastError; + throw error; + })(); + + pendingStickerFetches.set(normalizedType, fetchPromise); + + try { + return await fetchPromise; + } finally { + if (pendingStickerFetches.get(normalizedType) === fetchPromise) { + pendingStickerFetches.delete(normalizedType); + } + } + } + + async function fetchCachedImage(url) { + const normalizedUrl = String(url || '').trim(); + if (!/^https?:\/\//i.test(normalizedUrl)) { + const error = new Error('Invalid image URL'); + error.code = 400; + throw error; + } + + const { dataPath, metaPath } = buildImageCachePaths(normalizedUrl); + const cachedData = await readFileIfExists(dataPath); + if (cachedData) { + const cachedMeta = await readJsonIfExists(metaPath); + return { + buffer: cachedData, + contentType: (cachedMeta && cachedMeta.contentType) || guessImageContentType(normalizedUrl), + }; + } + + const pendingFetch = pendingImageFetches.get(normalizedUrl); + if (pendingFetch) { + return pendingFetch; + } + + const fetchPromise = (async () => { + const response = await axiosInstance.get(normalizedUrl, { responseType: 'arraybuffer' }); + const buffer = Buffer.from(response.data); + const contentType = guessImageContentType( + normalizedUrl, + response.headers && response.headers['content-type'] ? response.headers['content-type'] : 'image/png', + ); + + try { + await writeFileAsync(dataPath, buffer); + await writeFileAsync(metaPath, JSON.stringify({ contentType })); + } catch (err) { + logger.warn('failed to cache image', { url: normalizedUrl, error: err.message }); + } + + return { + buffer, + contentType, + }; + })(); + + pendingImageFetches.set(normalizedUrl, fetchPromise); + + try { + return await fetchPromise; + } finally { + if (pendingImageFetches.get(normalizedUrl) === fetchPromise) { + pendingImageFetches.delete(normalizedUrl); + } + } + } + + async function handleStickerProxy(req, res, type) { + try { + const buffer = await fetchStickerBuffer(type); + res.statusCode = 200; + res.setHeader('Content-Type', 'image/png'); + res.setHeader('Content-Length', buffer.length); + res.setHeader('Cache-Control', 'public, max-age=86400'); + res.end(buffer); + } catch (err) { + logger.warn('failed to proxy sticker', { type, error: err.message }); + sendJson(res, err.code || 404, { error: err.message || 'Sticker Not Found' }); + } + } + + async function handleImageProxy(req, res, url) { + try { + const image = await fetchCachedImage(url); + res.statusCode = 200; + res.setHeader('Content-Type', image.contentType); + res.setHeader('Content-Length', image.buffer.length); + res.setHeader('Cache-Control', 'public, max-age=86400'); + res.end(image.buffer); + } catch (err) { + logger.warn('failed to proxy image', { url, error: err.message }); + sendJson(res, err.code || 404, { error: err.message || 'Image Not Found' }); + } + } + + async function handleSendMessageRequest(payload) { + const message = await sendFriendMessage(payload.id, payload.msg); + const encoded = await encodeSteamMessage(message, true); + rememberSelfMessage(encoded); + + broadcastWs({ + type: 'message', + data: encoded, + }); + + return encoded; + } + + function rememberSelfImageUrl(uid, imageUrl) { + const urlKey = `${uid}:${imageUrl}`; + recentSelfImageUrls.set(urlKey, Date.now() + 15000); + + // Also remember that we sent *any* image to this uid recently, + // so we can suppress echo messages that contain image URLs even + // if Steam transforms the URL format. + const uidKey = `img:${uid}`; + recentSelfImageUrls.set(uidKey, Date.now() + 15000); + + const timer = setTimeout(() => { + recentSelfImageUrls.delete(urlKey); + recentSelfImageUrls.delete(uidKey); + }, 15000); + + if (typeof timer.unref === 'function') { + timer.unref(); + } + } + + function wasRecentImageEcho(data) { + // Direct match: message text is exactly the remembered image URL + const directKey = `${data.id}:${data.message}`; + const directExpiry = recentSelfImageUrls.get(directKey); + if (directExpiry && directExpiry >= Date.now()) { + return true; + } + if (directExpiry) { + recentSelfImageUrls.delete(directKey); + } + + // Extract image URLs from the message and check each one, + // because Steam may echo the URL wrapped in BBCode like + // [img src=URL ...]...[/img] or [img]URL[/img]. + const urls = extractImageUrls(data.message); + for (const url of urls) { + const key = `${data.id}:${url}`; + const expiresAt = recentSelfImageUrls.get(key); + if (!expiresAt) { + continue; + } + if (expiresAt < Date.now()) { + recentSelfImageUrls.delete(key); + continue; + } + return true; + } + + // Fallback: if we recently sent any image to this uid and + // the echo message contains any URL or image BBCode, suppress + // it even if the exact URL didn't match (Steam may rewrite + // the URL or use a host without a file extension). + const messageText = String(data.message || ''); + const looksLikeImageEcho = urls.length > 0 + || /https?:\/\/\S*(?:image|img|ugc|media|cdn)\S*/i.test(messageText) + || /\[img[\s\]]/i.test(messageText); + if (looksLikeImageEcho) { + const uidKey = `img:${data.id}`; + const uidExpiry = recentSelfImageUrls.get(uidKey); + if (uidExpiry && uidExpiry >= Date.now()) { + return true; + } + if (uidExpiry) { + recentSelfImageUrls.delete(uidKey); + } + } + + return false; + } + + async function handleSendImageRequest(payload, { senderWs } = {}) { + const imageUrl = await sendImageToUser(payload.id, payload.img, payload.url); + const data = await appendOutgoingImageLog(payload.id, imageUrl); + + // Remember the image URL so the friendMessageEcho (which echoes + // the image URL as a text message) gets deduplicated. + rememberSelfImageUrl(payload.id, imageUrl); + + // Broadcast to all clients except the sender (who gets image_sent). + const encoded = JSON.stringify({ type: 'image', data }); + wss.clients.forEach((ws) => { + if (ws !== senderWs && ws.readyState === WebSocketImpl.OPEN) { + ws.send(encoded); + } + }); + + return data; + } + + async function handleHttp(req, res) { + const requestUrl = new URL(req.url, 'http://127.0.0.1'); + + if (requiresHttpAuth(req, chatConfig) && !isAuthorized(req, chatConfig.auth)) { + sendAuthRequired(res); + return; + } + + if (req.method === 'GET' && requestUrl.pathname === '/') { + serveStaticFile(res, 'index.html'); + return; + } + + if (req.method === 'GET' && requestUrl.pathname === '/api/config') { + sendJson(res, 200, { wsPath: chatConfig.wsPath }); + return; + } + + if (req.method === 'GET' && requestUrl.pathname === '/api/emoticons') { + try { + const data = await getEmoticonList(); + sendJson(res, 200, data); + } catch (err) { + logger.error('failed to get emoticon list', err); + sendJson(res, err.code || 500, { error: err.message || 'Internal Server Error' }); + } + return; + } + + if (req.method === 'GET' && requestUrl.pathname.startsWith('/proxy/sticker/')) { + const type = decodeURIComponent(requestUrl.pathname.slice('/proxy/sticker/'.length)); + await handleStickerProxy(req, res, type); + return; + } + + if (req.method === 'GET' && requestUrl.pathname === '/proxy/image') { + await handleImageProxy(req, res, requestUrl.searchParams.get('url') || ''); + return; + } + + if (req.method === 'GET' && requestUrl.pathname === '/history') { + try { + const items = await readChatHistory({ + id: requestUrl.searchParams.get('id') || undefined, + limit: requestUrl.searchParams.get('limit') || undefined, + }); + sendJson(res, 200, { items }); + } catch (err) { + logger.error('failed to read chat history', err); + sendJson(res, 500, { error: err.message || 'Internal Server Error' }); + } + return; + } + + if (req.method === 'GET' && requestUrl.pathname === '/conversations') { + try { + const items = await readConversationSummaries({ + limit: requestUrl.searchParams.get('limit') || undefined, + }); + sendJson(res, 200, { items }); + } catch (err) { + logger.error('failed to read conversations', err); + sendJson(res, 500, { error: err.message || 'Internal Server Error' }); + } + return; + } + + if (req.method === 'GET') { + const ext = path.extname(requestUrl.pathname); + if (ext && STATIC_CONTENT_TYPES[ext]) { + serveStaticFile(res, requestUrl.pathname); + return; + } + } + + if (req.method !== 'POST') { + sendJson(res, 404, { error: 'Not Found' }); + return; + } + + try { + const payload = await readJsonBody(req); + + if (requestUrl.pathname === '/img' || requestUrl.pathname === '/image') { + const data = await handleSendImageRequest(payload); + sendJson(res, 200, data); + return; + } + + if (requestUrl.pathname === '/' || requestUrl.pathname === '/message') { + const data = await handleSendMessageRequest(payload); + sendJson(res, 200, data); + return; + } + + sendJson(res, 404, { error: 'Not Found' }); + } catch (err) { + logger.error('An error occurred while processing the request', err); + sendJson(res, err.code || 500, { error: err.message || 'Internal Server Error' }); + } + } + + async function handleWsCommand(ws, payload) { + const request = normalizeWsRequest(payload); + + switch (request.action) { + case 'send_message': { + const data = await handleSendMessageRequest(request); + sendWs(ws, { + type: 'message_sent', + requestId: request.requestId, + data, + }); + return; + } + case 'send_image': { + const data = await handleSendImageRequest(request, { senderWs: ws }); + sendWs(ws, { + type: 'image_sent', + requestId: request.requestId, + data, + }); + return; + } + case 'get_history': { + const items = await readChatHistory({ + id: request.id, + limit: request.limit, + }); + sendWs(ws, { + type: 'history', + requestId: request.requestId, + data: { + items, + }, + }); + return; + } + case 'get_conversations': { + const items = await readConversationSummaries({ + limit: request.limit, + }); + sendWs(ws, { + type: 'conversations', + requestId: request.requestId, + data: { + items, + }, + }); + return; + } + case 'get_emoticons': { + const emoticonData = await getEmoticonList(); + sendWs(ws, { + type: 'emoticons', + requestId: request.requestId, + data: emoticonData, + }); + return; + } + case 'ping': + sendWs(ws, { + type: 'pong', + requestId: request.requestId, + data: { + now: new Date().toISOString(), + }, + }); + return; + default: { + const error = new Error(`Unsupported WebSocket message type: ${payload.type}`); + error.code = 400; + throw error; + } + } + } + + function handleWs(ws) { + logger.info('WebSocket connection established'); + sendWs(ws, { + type: 'ready', + data: { + wsPath: chatConfig.wsPath, + }, + }); + + ws.on('message', async (message) => { + let payload; + try { + payload = JSON.parse(message.toString()); + } catch (err) { + sendWs(ws, { + type: 'error', + message: 'Invalid JSON', + }); + return; + } + + try { + await handleWsCommand(ws, payload); + } catch (err) { + logger.error('WebSocket command failed', err); + sendWs(ws, { + type: 'error', + requestId: payload.requestId, + message: err.message || 'Internal Server Error', + }); + } + }); + + ws.on('close', () => { + logger.info('WebSocket connection closed'); + }); + + ws.on('error', (err) => { + logger.error('WebSocket error', err); + }); + } + + async function start() { + if (started || !chatConfig.enabled) { + return; + } + started = true; + + await client.steamLoginPromise; + + steamUser.chat.on('friendMessage', (message) => { + broadcastSteamMessage(message, false).catch((err) => { + logger.error('failed to broadcast friend message', err); + }); + }); + + steamUser.chat.on('friendMessageEcho', (message) => { + broadcastSteamMessage(message, true, { dedupe: true }).catch((err) => { + logger.error('failed to broadcast echoed friend message', err); + }); + }); wss.on('connection', handleWs); - server.listen(3000, '0.0.0.0', () => { - console.log('Server running at http://0.0.0.0:3000/'); - console.log('WebSocket server is also running.'); + await new Promise((resolve, reject) => { + server.listen(chatConfig.port, chatConfig.host, (err) => { + if (err) { + reject(err); + return; + } + + logger.info('chat server started', { + host: chatConfig.host, + port: chatConfig.port, + wsPath: chatConfig.wsPath, + }); + resolve(); + }); }); - } catch (err) { - console.error('Error during initialization:', err); } -}); + + return { + chatConfig, + server, + wss, + start, + sendFriendMessage, + sendImageToUser, + getEmoticonList, + readChatHistory, + readConversationSummaries, + fetchStickerBuffer, + fetchCachedImage, + handleSendMessageRequest, + handleSendImageRequest, + handleHttp, + handleWs, + handleWsCommand, + broadcastSteamMessage, + encodeSteamMessage, + parseImageBuffer, + parseLogLines, + readJsonBody, + wasRecentlyBroadcasted, + rememberSelfMessage, + sendWs, + broadcastWs, + }; +} + +let defaultChatService = null; + +if (!process.env.STEAM_CHAT_DISABLE_AUTOSTART) { + defaultChatService = createChatService(); + defaultChatService.start().catch((err) => { + console.error('Error during chat service initialization', err); + }); +} + +module.exports = { + CHAT_LOG_FILE, + STICKER_CACHE_DIR, + IMAGE_CACHE_DIR, + createChatService, + normalizeAuthConfig, + normalizeChatConfig, + normalizeHistoryEntry, + normalizeWsRequest, + normalizeIpAddress, + parseForwardedHeader, + getClientIp, + isLanIp, + isAuthEnabled, + parseBasicAuthHeader, + isAuthorized, + requiresHttpAuth, + sanitizeLimit, + extractStickerType, + extractEmoticonNames, + extractImageUrls, + extractOpenGraphEmbeds, + buildSteamEmoticonUrl, + buildSteamStickerCandidateUrls, + buildStickerCachePath, + buildImageCachePaths, + guessImageContentType, + buildConversationPreview, + sortHistoryItems, + buildConversationSummaries, + buildMessageKey, + defaultChatService, +}; diff --git a/client.js b/client.js index d396e44..8e14217 100644 --- a/client.js +++ b/client.js @@ -2,6 +2,7 @@ const SteamUser = require('steam-user'); const SteamCommunity = require('steamcommunity'); const fs = require('fs'); const winston = require("winston"); +const config = require("./config.js"); const logger = winston.createLogger({ level: 'info', @@ -23,10 +24,8 @@ steamUser.on("refreshToken", (refreshToken) => { fs.writeFileSync('refresh.token', refreshToken); }); -const config = require("./config.js"); - try { - refreshToken = fs.readFileSync('refresh.token', 'utf8'); + const refreshToken = fs.readFileSync('refresh.token', 'utf8'); if (refreshToken && refreshToken.length > 0) { const LogOnOptionsAUTO = { @@ -54,20 +53,27 @@ try { }); } -steamLoginPromise = new Promise((resolve, reject) => { +const steamLoginPromise = new Promise((resolve, reject) => { steamUser.on('loggedOn', async () => { logger.info(`login to Steam as ${steamUser.steamID}`); + try { + steamUser.webLogOn(); + } catch (err) { + logger.warn(`failed to start web login: ${err.message}`); + } resolve(); }); }); -steamWebLoginPromise = new Promise((resolve, reject) => { +const steamWebLoginPromise = new Promise((resolve, reject) => { steamUser.on('webSession', async (sessionID, cookies) => { logger.info(`web session received: ${sessionID}`); steamCommunity.setCookies(cookies); - steamCommunity.startConfirmationChecker(10000, config.identitySecret); + if (config.identitySecret) { + steamCommunity.startConfirmationChecker(10000, config.identitySecret); + } resolve() }); @@ -109,4 +115,3 @@ module.exports = { steamLoginPromise: steamLoginPromise, steamWebLoginPromise: steamWebLoginPromise, } - diff --git a/config.example.js b/config.example.js index 53fed5a..4925734 100644 --- a/config.example.js +++ b/config.example.js @@ -9,5 +9,16 @@ module.exports = { password: 'password', logonID: getRandomInt(1000000, 999999999), steamID: "xxxxxxxxxx", - chat: false, + chat: { + enabled: false, + host: '0.0.0.0', + port: 3000, + wsPath: '/ws', + auth: { + username: 'admin', + password: 'change-me', + realm: 'Steam Chat', + trustProxy: false, + }, + }, }; diff --git a/logger.js b/logger.js index 7c8423b..6d832df 100644 --- a/logger.js +++ b/logger.js @@ -101,7 +101,7 @@ function importChatHistory(steamID) { }); } -if (config.chat == true) { +if (config.chat) { require("./chat.js") } diff --git a/package-lock.json b/package-lock.json index 1df0e07..f5a4d9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,90 +25,107 @@ }, "node_modules/@bbob/parser": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@bbob/parser/-/parser-2.9.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@bbob/parser/-/parser-2.9.0.tgz", "integrity": "sha512-tldSYsMoEclke/B1nqL7+HbYMWZHTKvpbEHRSHuY+sZvS1o7Jpdfjb+KPpwP9wLI3p3r7GPv69/wGy+Xibs9yA==", + "license": "MIT", "dependencies": { "@bbob/plugin-helper": "^2.9.0" } }, "node_modules/@bbob/plugin-helper": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@bbob/plugin-helper/-/plugin-helper-2.9.0.tgz", - "integrity": "sha512-idpUcNQ2co6T1oU/7/DG/ZRfipSSkTn9Ozw9f5vaXH7nzV3qhqZnhFVlHTzGGnRlzKlBwWOBzOdWi4Zeqg1c5A==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@bbob/plugin-helper/-/plugin-helper-2.9.0.tgz", + "integrity": "sha512-idpUcNQ2co6T1oU/7/DG/ZRfipSSkTn9Ozw9f5vaXH7nzV3qhqZnhFVlHTzGGnRlzKlBwWOBzOdWi4Zeqg1c5A==", + "license": "MIT" }, "node_modules/@colors/colors": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@colors/colors/-/colors-1.6.0.tgz", "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", "engines": { "node": ">=0.1.90" } }, "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "version": "2.0.8", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", "dependencies": { - "colorspace": "1.1.x", + "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "node_modules/@doctormckay/stats-reporter": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@doctormckay/stats-reporter/-/stats-reporter-1.0.5.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@doctormckay/stats-reporter/-/stats-reporter-1.0.5.tgz", "integrity": "sha512-lCAuKW053zz91sKZZcGfOHxigBqn0Lo+/JvHBQq3XqzLJxn0YeZ5mJ96+PZto+PDCkgg+c/BX2Xo8DvAN44xLg==", + "license": "MIT", "engines": { "node": ">=4.0.0" } }, "node_modules/@doctormckay/stdlib": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-1.16.1.tgz", - "integrity": "sha512-XhuUOzElz6fnNdt70IYNKqhPAEpGaL4JHOhAvklRh0hAhVPW+/wLxaWT3DWUbaG5Dta5YvIp7+cZK3GhIpAuug==", + "version": "2.10.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@doctormckay/stdlib/-/stdlib-2.10.0.tgz", + "integrity": "sha512-bwy+gPn6oa2KTpfxJKX3leZoV/wHDVtO0/gq3usPvqPswG//dcf3jVB8LcbRRsKO3BXCt5DqctOQ+Xb07ivxnw==", + "license": "MIT", + "dependencies": { + "psl": "^1.9.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=12.22.0" } }, "node_modules/@doctormckay/steam-crypto": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@doctormckay/steam-crypto/-/steam-crypto-1.2.0.tgz", - "integrity": "sha512-lsxgLw640gEdZBOXpVIcYWcYD+V+QbtEsMPzRvjmjz2XXKc7QeEMyHL07yOFRmay+cUwO4ObKTJO0dSInEuq5g==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@doctormckay/steam-crypto/-/steam-crypto-1.2.0.tgz", + "integrity": "sha512-lsxgLw640gEdZBOXpVIcYWcYD+V+QbtEsMPzRvjmjz2XXKc7QeEMyHL07yOFRmay+cUwO4ObKTJO0dSInEuq5g==", + "license": "MIT" }, "node_modules/@doctormckay/user-agents": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@doctormckay/user-agents/-/user-agents-1.0.0.tgz", - "integrity": "sha512-F+sL1YmebZTY2CnjoR9BXFEULpq7y8dxyLx48LZVa0BSDseXdLG/DtPISfM1iNv1XKCeiBzVNfAT/MOQ69v1Zw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@doctormckay/user-agents/-/user-agents-1.0.0.tgz", + "integrity": "sha512-F+sL1YmebZTY2CnjoR9BXFEULpq7y8dxyLx48LZVa0BSDseXdLG/DtPISfM1iNv1XKCeiBzVNfAT/MOQ69v1Zw==", + "license": "MIT" }, "node_modules/@matteo.collina/dateformat": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@matteo.collina/dateformat/-/dateformat-5.0.1.tgz", - "integrity": "sha512-BxOmQxcfZxoo+qxI+/lQ28aoh1IpKAiuYLk7sIiCiqOXY9cWfMOwcyJY1xFWPQdHzYlMeeRo0oVABkB3b0ueWw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@matteo.collina/dateformat/-/dateformat-5.0.1.tgz", + "integrity": "sha512-BxOmQxcfZxoo+qxI+/lQ28aoh1IpKAiuYLk7sIiCiqOXY9cWfMOwcyJY1xFWPQdHzYlMeeRo0oVABkB3b0ueWw==", + "license": "MIT" }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -116,28 +133,43 @@ }, "node_modules/@protobufjs/float": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } }, "node_modules/@types/bytebuffer": { "version": "5.0.49", @@ -150,13 +182,6 @@ "@types/node": "*" } }, - "node_modules/@types/bytebuffer/node_modules/@types/long": { - "version": "3.0.32", - "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/long/-/long-3.0.32.tgz", - "integrity": "sha512-ZXyOOm83p7X8p3s0IYM3VeueNmHpkk/yMlP8CLeOnEcu6hIwPH7YjZBvhQkR0ZFS2DqZAxKtJ/M5fcuv3OU5BA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/caseless": { "version": "0.12.5", "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/caseless/-/caseless-0.12.5.tgz", @@ -175,47 +200,32 @@ } }, "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + "version": "3.0.32", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/long/-/long-3.0.32.tgz", + "integrity": "sha512-ZXyOOm83p7X8p3s0IYM3VeueNmHpkk/yMlP8CLeOnEcu6hIwPH7YjZBvhQkR0ZFS2DqZAxKtJ/M5fcuv3OU5BA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { - "version": "20.11.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.28.tgz", - "integrity": "sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==", + "version": "25.5.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.18.0" } }, "node_modules/@types/request": { - "version": "2.48.12", - "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/request/-/request-2.48.12.tgz", - "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "version": "2.48.13", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", "dev": true, "license": "MIT", "dependencies": { "@types/caseless": "*", "@types/node": "*", "@types/tough-cookie": "*", - "form-data": "^2.5.0" - } - }, - "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://mvn.tursom.cn:20080/repository/npm/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" + "form-data": "^2.5.5" } }, "node_modules/@types/steam-totp": { @@ -229,9 +239,9 @@ } }, "node_modules/@types/steam-user": { - "version": "5.1.0", - "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/steam-user/-/steam-user-5.1.0.tgz", - "integrity": "sha512-jMCzwmvBA1DaqLTD9h23LzVmzyHy53cdzpJjBSYDI6qT/1LqEUM9xRt4Vb0Y9kOIr+BZckCewDw8Bk6n8e3BTA==", + "version": "5.1.1", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/steam-user/-/steam-user-5.1.1.tgz", + "integrity": "sha512-jYAsDpp30eC+/EWwG9Ea2qyGuKBWsTXgl7qVUiAMP3YtsCJi4/rCj0w8W8167RdhrkY7FhSC0OScQt5fYK8cGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -254,9 +264,9 @@ } }, "node_modules/@types/steamid": { - "version": "2.0.3", - "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/steamid/-/steamid-2.0.3.tgz", - "integrity": "sha512-ozNMQViUYLU+NBN4v7X0bV1O8uTL1bA+WvfHtt9IKcydS4tyYKH7w1vq+xcPGWGL0PRhGtY7C1Zhaeyj2IsETw==", + "version": "2.0.4", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/steamid/-/steamid-2.0.4.tgz", + "integrity": "sha512-LC0oaiNq3gqMI18MV935CnlyHmDooKRszK56jHv0+b7EbJwf5k++YsD52zWeJUD8nuYqP9LpIgpXGiYBOxpcYQ==", "dev": true, "license": "MIT" }, @@ -269,21 +279,24 @@ }, "node_modules/@types/triple-beam": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" }, "node_modules/adm-zip": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz", - "integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==", + "version": "0.5.16", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", "engines": { - "node": ">=6.0" + "node": ">=12.0" } }, "node_modules/agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", "dependencies": { "debug": "4" }, @@ -292,9 +305,10 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -308,62 +322,73 @@ }, "node_modules/asn1": { "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", "dependencies": { "safer-buffer": "~2.1.0" } }, "node_modules/assert-plus": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", "engines": { "node": ">=0.8" } }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "version": "2.6.4", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/aws-sign2": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/aws-sign2/-/aws-sign2-0.7.0.tgz", "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", "engines": { "node": "*" } }, "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + "version": "1.13.2", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://mvn.tursom.cn:20080/repository/npm/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.13.6", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "node_modules/axios/node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://mvn.tursom.cn:20080/repository/npm/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.5", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -372,40 +397,36 @@ }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", "dependencies": { "tweetnacl": "^0.14.3" } }, "node_modules/binarykvparser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binarykvparser/-/binarykvparser-2.2.0.tgz", - "integrity": "sha512-mGBKngQF9ui53THcMjgjd0LrBH/HsI2Vywfjq52udSAmRGG87h0vjhkqun0kF+iC4rQ2jLZqldwJE7YN2ueiWw==", + "version": "2.3.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/binarykvparser/-/binarykvparser-2.3.0.tgz", + "integrity": "sha512-B1N5ZxC8I9oSLis7Rg36DxsZJoIikUGU2XwpI0FKFCaPIJIEYi0B9UeIk3QU006axzq0TI9KC3iXelfGGgnWew==", "bundleDependencies": [ "long" ], + "license": "MIT", "dependencies": { "long": "^3.2.0" } }, - "node_modules/binarykvparser/node_modules/long": { - "version": "3.2.0", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.6" - } - }, "node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" }, "node_modules/bytebuffer": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/bytebuffer/-/bytebuffer-5.0.1.tgz", "integrity": "sha512-IuzSdmADppkZ6DlpycMkm8l9zeEq16fWtLvunEwFiYciR/BHo4E8/xs5piFquG+Za8OWmMqHF8zuRviz2LHvRQ==", + "license": "Apache-2.0", "dependencies": { "long": "~3" }, @@ -417,7 +438,6 @@ "version": "1.0.2", "resolved": "https://mvn.tursom.cn:20080/repository/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -429,13 +449,15 @@ }, "node_modules/caseless": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" }, "node_modules/cheerio": { "version": "0.22.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/cheerio/-/cheerio-0.22.0.tgz", "integrity": "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==", + "license": "MIT", "dependencies": { "css-select": "~1.2.0", "dom-serializer": "~0.1.0", @@ -459,49 +481,56 @@ } }, "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "version": "5.0.3", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "3.1.3", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" + "version": "2.1.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" } }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -511,13 +540,15 @@ }, "node_modules/core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" }, "node_modules/css-select": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/css-select/-/css-select-1.2.0.tgz", "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", + "license": "BSD-like", "dependencies": { "boolbase": "~1.0.0", "css-what": "2.1", @@ -527,21 +558,24 @@ }, "node_modules/css-what": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/css-what/-/css-what-2.1.3.tgz", "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "license": "BSD-2-Clause", "engines": { "node": "*" } }, "node_modules/cuint": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", - "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==", + "license": "MIT" }, "node_modules/dashdash": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" }, @@ -550,11 +584,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -567,16 +602,18 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/dom-serializer": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/dom-serializer/-/dom-serializer-0.1.1.tgz", "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "license": "MIT", "dependencies": { "domelementtype": "^1.3.0", "entities": "^1.1.1" @@ -584,20 +621,22 @@ }, "node_modules/domelementtype": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/domhandler/-/domhandler-2.4.2.tgz", "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "1" } }, "node_modules/domutils": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/domutils/-/domutils-1.5.1.tgz", "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", "dependencies": { "dom-serializer": "0", @@ -608,7 +647,6 @@ "version": "1.0.1", "resolved": "https://mvn.tursom.cn:20080/repository/npm/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -621,33 +659,30 @@ }, "node_modules/ecc-jsbn": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, - "node_modules/ecc-jsbn/node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, "node_modules/enabled": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" }, "node_modules/entities": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "license": "BSD-2-Clause" }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://mvn.tursom.cn:20080/repository/npm/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -657,7 +692,6 @@ "version": "1.3.0", "resolved": "https://mvn.tursom.cn:20080/repository/npm/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -667,7 +701,6 @@ "version": "1.1.1", "resolved": "https://mvn.tursom.cn:20080/repository/npm/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -680,7 +713,6 @@ "version": "2.1.0", "resolved": "https://mvn.tursom.cn:20080/repository/npm/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -694,36 +726,42 @@ }, "node_modules/extend": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" }, "node_modules/extsprintf": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "engines": [ "node >=0.6.0" - ] + ], + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" }, "node_modules/fecha": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" }, "node_modules/file-manager": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/file-manager/-/file-manager-2.0.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/file-manager/-/file-manager-2.0.1.tgz", "integrity": "sha512-y/K/1OCha04OXOxzo3cXJYtIzEk/CUMBb7Okipxueu0u+xCiuoocbwPyh1smUBasOobo4GAYmjgjD9Vh5zI51w==", + "license": "MIT", "dependencies": { "@doctormckay/stdlib": "^1.14.1" }, @@ -731,15 +769,25 @@ "node": ">=8.0.0" } }, + "node_modules/file-manager/node_modules/@doctormckay/stdlib": { + "version": "1.16.1", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@doctormckay/stdlib/-/stdlib-1.16.1.tgz", + "integrity": "sha512-XhuUOzElz6fnNdt70IYNKqhPAEpGaL4JHOhAvklRh0hAhVPW+/wLxaWT3DWUbaG5Dta5YvIp7+cZK3GhIpAuug==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fn.name": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://mvn.tursom.cn:20080/repository/npm/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -758,20 +806,26 @@ }, "node_modules/forever-agent": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", "engines": { "node": "*" } }, "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "2.5.5", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" }, "engines": { "node": ">= 0.12" @@ -781,7 +835,6 @@ "version": "1.1.2", "resolved": "https://mvn.tursom.cn:20080/repository/npm/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -791,7 +844,6 @@ "version": "1.3.0", "resolved": "https://mvn.tursom.cn:20080/repository/npm/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -816,7 +868,6 @@ "version": "1.0.1", "resolved": "https://mvn.tursom.cn:20080/repository/npm/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -828,8 +879,9 @@ }, "node_modules/getpass": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/getpass/-/getpass-0.1.7.tgz", "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" } @@ -838,7 +890,6 @@ "version": "1.2.0", "resolved": "https://mvn.tursom.cn:20080/repository/npm/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -849,17 +900,19 @@ }, "node_modules/har-schema": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/har-schema/-/har-schema-2.0.0.tgz", "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", "engines": { "node": ">=4" } }, "node_modules/har-validator": { "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/har-validator/-/har-validator-5.1.5.tgz", "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "deprecated": "this library is no longer supported", + "license": "MIT", "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -872,7 +925,6 @@ "version": "1.1.0", "resolved": "https://mvn.tursom.cn:20080/repository/npm/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -885,7 +937,6 @@ "version": "1.0.2", "resolved": "https://mvn.tursom.cn:20080/repository/npm/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -901,7 +952,6 @@ "version": "2.0.2", "resolved": "https://mvn.tursom.cn:20080/repository/npm/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -912,8 +962,9 @@ }, "node_modules/htmlparser2": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/htmlparser2/-/htmlparser2-3.10.1.tgz", "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "license": "MIT", "dependencies": { "domelementtype": "^1.3.1", "domhandler": "^2.3.0", @@ -925,8 +976,9 @@ }, "node_modules/http-signature": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -939,8 +991,9 @@ }, "node_modules/image-size": { "version": "0.8.3", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.8.3.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/image-size/-/image-size-0.8.3.tgz", "integrity": "sha512-SMtq1AJ+aqHB45c3FsB4ERK0UCiA2d3H1uq8s+8T0Pf8A3W4teyBQyaFaktH6xvZqh+npwlKU7i4fJo0r7TYTg==", + "license": "MIT", "dependencies": { "queue": "6.0.1" }, @@ -953,30 +1006,24 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, + "version": "10.1.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", "engines": { "node": ">= 12" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, "node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -986,38 +1033,45 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" }, "node_modules/isstream": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" }, "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + "version": "0.1.1", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" }, "node_modules/json-schema": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" }, "node_modules/jsprim": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/jsprim/-/jsprim-1.4.2.tgz", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -1030,86 +1084,103 @@ }, "node_modules/kuler": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" }, "node_modules/kvparser": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/kvparser/-/kvparser-1.0.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/kvparser/-/kvparser-1.0.2.tgz", "integrity": "sha512-5P/5qpTAHjVYWqcI55B3yQwSY2FUrYYrJj5i65V1Wmg7/4W4OnBcaodaEvLyVuugeOnS+BAaKm9LbPazGJcRyA==", + "license": "MIT", "engines": { "node": ">=4.0.0" } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" }, "node_modules/lodash.assignin": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==", + "license": "MIT" }, "node_modules/lodash.bind": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", - "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==", + "license": "MIT" }, "node_modules/lodash.defaults": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" }, "node_modules/lodash.filter": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", + "license": "MIT" }, "node_modules/lodash.flatten": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" }, "node_modules/lodash.foreach": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "license": "MIT" }, "node_modules/lodash.map": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" }, "node_modules/lodash.pick": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", + "license": "MIT" }, "node_modules/lodash.reduce": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "license": "MIT" }, "node_modules/lodash.reject": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", - "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==", + "license": "MIT" }, "node_modules/lodash.some": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "license": "MIT" }, "node_modules/logform": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", - "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "version": "2.7.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", @@ -1124,16 +1195,18 @@ }, "node_modules/long": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/long/-/long-3.2.0.tgz", "integrity": "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==", + "license": "Apache-2.0", "engines": { "node": ">=0.6" } }, "node_modules/lzma": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/lzma/-/lzma-2.3.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/lzma/-/lzma-2.3.2.tgz", "integrity": "sha512-DcfiawQ1avYbW+hsILhF38IKAlnguc/fjHrychs9hdxe4qLykvhT5VTGNs5YRWgaNePh7NTxGD4uv4gKsRomCQ==", + "license": "MIT", "bin": { "lzma.js": "bin/lzma.js" } @@ -1142,7 +1215,6 @@ "version": "1.1.0", "resolved": "https://mvn.tursom.cn:20080/repository/npm/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1150,16 +1222,18 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1168,13 +1242,14 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/node-bignumber": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/node-bignumber/-/node-bignumber-1.2.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/node-bignumber/-/node-bignumber-1.2.2.tgz", "integrity": "sha512-VoTZHmdFQpZH1+q1dz2qcHNCwTWsJg2T3PYwlAyDNFOfVhSYUKQBLFcCpCud+wJBGgCttGavZILaIggDIKqEQQ==", "engines": { "node": ">=0.4.0" @@ -1182,37 +1257,42 @@ }, "node_modules/nth-check": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/nth-check/-/nth-check-1.0.2.tgz", "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "license": "BSD-2-Clause", "dependencies": { "boolbase": "~1.0.0" } }, "node_modules/oauth-sign": { "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", "engines": { "node": "*" } }, "node_modules/one-time": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/one-time/-/one-time-1.0.0.tgz", "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", "dependencies": { "fn.name": "1.x.x" } }, "node_modules/performance-now": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" }, "node_modules/permessage-deflate": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/permessage-deflate/-/permessage-deflate-0.1.7.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/permessage-deflate/-/permessage-deflate-0.1.7.tgz", "integrity": "sha512-EUNi/RIsyJ1P1u9QHFwMOUWMYetqlE22ZgGbad7YP856WF4BFF0B7DuNy6vEGsgNNud6c/SkdWzkne71hH8MjA==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "*" }, @@ -1221,10 +1301,11 @@ } }, "node_modules/protobufjs": { - "version": "6.11.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", - "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "version": "7.5.4", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -1236,19 +1317,18 @@ "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", "@types/node": ">=13.7.0", - "long": "^4.0.0" + "long": "^5.0.0" }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" + "engines": { + "node": ">=12.0.0" } }, "node_modules/protobufjs/node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "version": "5.3.2", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" }, "node_modules/proxy-from-env": { "version": "1.1.0", @@ -1257,38 +1337,49 @@ "license": "MIT" }, "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + "version": "1.15.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "version": "6.5.5", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.6" } }, "node_modules/queue": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/queue/-/queue-6.0.1.tgz", "integrity": "sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg==", + "license": "MIT", "dependencies": { "inherits": "~2.0.3" } }, "node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -1300,9 +1391,10 @@ }, "node_modules/request": { "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -1329,9 +1421,23 @@ "node": ">= 6" } }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { @@ -1346,49 +1452,50 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "version": "2.5.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dependencies": { - "is-arrayish": "^0.3.1" + "version": "1.6.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" } }, "node_modules/smart-buffer": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz", - "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==", + "version": "2.8.7", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -1398,8 +1505,9 @@ }, "node_modules/socks-proxy-agent": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "license": "MIT", "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", @@ -1409,15 +1517,11 @@ "node": ">= 10" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, "node_modules/sshpk": { "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/sshpk/-/sshpk-1.18.0.tgz", "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -1438,23 +1542,20 @@ "node": ">=0.10.0" } }, - "node_modules/sshpk/node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, "node_modules/stack-trace": { "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/stack-trace/-/stack-trace-0.0.10.tgz", "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", "engines": { "node": "*" } }, "node_modules/steam-appticket": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/steam-appticket/-/steam-appticket-1.0.1.tgz", - "integrity": "sha512-oYVInCvJlPPaQPYW1+iGcVP0N0ZvwtWiCDM1Z353XJ8l4DXQI/N+R5yyaRQcHRH5oQv3+BY6gPF40lu7gwEiJw==", + "version": "1.0.2", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/steam-appticket/-/steam-appticket-1.0.2.tgz", + "integrity": "sha512-zwDwZALGv3RanE8RHNYcQU3u4Ez23EzMuQ4Lh15uIHddpDh6TI6uFGbC0HNyt6y+UJYSILe77A33VhFZKQiaqQ==", + "license": "MIT", "dependencies": { "@doctormckay/stdlib": "^1.6.0", "@doctormckay/steam-crypto": "^1.2.0", @@ -1466,10 +1567,67 @@ "node": ">=4.0.0" } }, + "node_modules/steam-appticket/node_modules/@doctormckay/stdlib": { + "version": "1.16.1", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@doctormckay/stdlib/-/stdlib-1.16.1.tgz", + "integrity": "sha512-XhuUOzElz6fnNdt70IYNKqhPAEpGaL4JHOhAvklRh0hAhVPW+/wLxaWT3DWUbaG5Dta5YvIp7+cZK3GhIpAuug==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/steam-appticket/node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/steam-appticket/node_modules/long": { + "version": "4.0.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/steam-appticket/node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/steam-appticket/node_modules/steamid": { + "version": "1.1.3", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/steamid/-/steamid-1.1.3.tgz", + "integrity": "sha512-t86YjtP1LtPt8D+TaIARm6PtC9tBnF1FhxQeLFs6ohG7vDUfQuy/M8II14rx1TTUkVuYoWHP/7DlvTtoCGULcw==", + "license": "MIT", + "dependencies": { + "cuint": "^0.2.1" + } + }, "node_modules/steam-session": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/steam-session/-/steam-session-1.7.2.tgz", - "integrity": "sha512-BfOhwKqrzuiX9xeZ0X9IUhd3cwsZYzfGSkk51Oah7FM7JqDCDlcMrdv/1Q+YT7pBWpPCnnrncVkxiZ5mrRToCg==", + "version": "1.9.4", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/steam-session/-/steam-session-1.9.4.tgz", + "integrity": "sha512-MLvg1uMLEOIRHZS5LKruy1w5OqHb8EL7TeMxIY2a4mUcaVOgszz060+jo4c7s3gFeedyuoqGcfwJrR0pLKYzLw==", + "license": "MIT", "dependencies": { "@doctormckay/stdlib": "^2.9.0", "@doctormckay/user-agents": "^1.0.0", @@ -1486,65 +1644,20 @@ "node": ">=12.22.0" } }, - "node_modules/steam-session/node_modules/@doctormckay/stdlib": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-2.10.0.tgz", - "integrity": "sha512-bwy+gPn6oa2KTpfxJKX3leZoV/wHDVtO0/gq3usPvqPswG//dcf3jVB8LcbRRsKO3BXCt5DqctOQ+Xb07ivxnw==", - "dependencies": { - "psl": "^1.9.0" - }, - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/steam-session/node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" - }, - "node_modules/steam-session/node_modules/protobufjs": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", - "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/steam-session/node_modules/steamid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/steamid/-/steamid-2.0.0.tgz", - "integrity": "sha512-+BFJMbo+IxzyfovLR37E7APkaNfmrL3S+88T7wTMRHnQ6LBhzEawPnjfWNKM9eUL/dH45j+7vhSX4WaGXoa4/Q==", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/steam-totp": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/steam-totp/-/steam-totp-2.1.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/steam-totp/-/steam-totp-2.1.2.tgz", "integrity": "sha512-bTKlc/NoIUQId+my+O556s55DDsNNXfVIPWFDNVu68beql7AJhV0c+GTjFxfwCDYfdc4NkAme+0WrDdnY2D2VA==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/steam-user": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/steam-user/-/steam-user-5.0.8.tgz", - "integrity": "sha512-PPOgZr+YpiuqY2msvcxWoDpNm58E4HHs1yg0BS8w854qIn23jTqKk8U4wj9V1KS8pWs/JJ9BzgwIMLBzQ1g0zw==", + "version": "5.3.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/steam-user/-/steam-user-5.3.0.tgz", + "integrity": "sha512-/92MOZGIocixlgzjloXrDffbAL5sF9rz4sbsafZ73samhkv2ITQNJrKaKGCJbAq5Xz8CbtCzstdjH8qXawLJVg==", + "license": "MIT", "dependencies": { "@bbob/parser": "^2.2.0", "@doctormckay/stdlib": "^2.9.1", @@ -1558,73 +1671,36 @@ "protobufjs": "^7.2.4", "socks-proxy-agent": "^7.0.0", "steam-appticket": "^1.0.1", - "steam-session": "^1.7.0", + "steam-session": "^1.8.0", "steam-totp": "^2.0.1", "steamid": "^2.0.0", - "websocket13": "^4.0.0" + "websocket13": "^4.0.0", + "zstddec": "^0.1.0" }, "engines": { "node": ">=14.0.0" - } - }, - "node_modules/steam-user/node_modules/@doctormckay/stdlib": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-2.10.0.tgz", - "integrity": "sha512-bwy+gPn6oa2KTpfxJKX3leZoV/wHDVtO0/gq3usPvqPswG//dcf3jVB8LcbRRsKO3BXCt5DqctOQ+Xb07ivxnw==", - "dependencies": { - "psl": "^1.9.0" }, - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/steam-user/node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" - }, - "node_modules/steam-user/node_modules/protobufjs": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", - "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" + "peerDependencies": { + "lzma-native": "^8.0.0" }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/steam-user/node_modules/steamid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/steamid/-/steamid-2.0.0.tgz", - "integrity": "sha512-+BFJMbo+IxzyfovLR37E7APkaNfmrL3S+88T7wTMRHnQ6LBhzEawPnjfWNKM9eUL/dH45j+7vhSX4WaGXoa4/Q==", - "engines": { - "node": ">=12.0.0" + "peerDependenciesMeta": { + "lzma-native": { + "optional": true + } } }, "node_modules/steamcommunity": { - "version": "3.48.2", - "resolved": "https://registry.npmjs.org/steamcommunity/-/steamcommunity-3.48.2.tgz", - "integrity": "sha512-447UMw5KQpzZfAfo68qV6DWU0blNAhphDGnhbtCFG6GIE3fZdnD5uRIswMsmVZ+hnLvpks/+AWeZHguSTWjc6A==", + "version": "3.49.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/steamcommunity/-/steamcommunity-3.49.0.tgz", + "integrity": "sha512-f5w+/mOcrfobueEG0n77RMh09SqEqqv90Rm6l/AYhANw9q8T/SLKIHEp3ya1rvt5GWCiDoCVjCbAfCtd633S0g==", + "license": "MIT", "dependencies": { "@doctormckay/user-agents": "^1.0.0", "async": "^2.6.3", "cheerio": "0.22.0", "image-size": "^0.8.2", "request": "^2.88.0", - "steam-session": "^1.7.2", + "steam-session": "^1.9.1", "steam-totp": "^1.5.0", "steamid": "^1.1.3", "xml2js": "^0.6.2" @@ -1633,52 +1709,59 @@ "node": ">=4.0.0" } }, - "node_modules/steamcommunity/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dependencies": { - "lodash": "^4.17.14" - } - }, "node_modules/steamcommunity/node_modules/steam-totp": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/steam-totp/-/steam-totp-1.5.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/steam-totp/-/steam-totp-1.5.0.tgz", "integrity": "sha512-RMlBK5dFtgplDMYYGg/k80RqEntzBcl7C/0RF18fQh9+XPe/iEMsfKmIE+xj8I3hqJW1akANAC6gf+YpfZq52w==", + "license": "MIT", "dependencies": { "@doctormckay/stats-reporter": "^1.0.0" } }, - "node_modules/steamid": { + "node_modules/steamcommunity/node_modules/steamid": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/steamid/-/steamid-1.1.3.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/steamid/-/steamid-1.1.3.tgz", "integrity": "sha512-t86YjtP1LtPt8D+TaIARm6PtC9tBnF1FhxQeLFs6ohG7vDUfQuy/M8II14rx1TTUkVuYoWHP/7DlvTtoCGULcw==", + "license": "MIT", "dependencies": { "cuint": "^0.2.1" } }, + "node_modules/steamid": { + "version": "2.1.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/steamid/-/steamid-2.1.0.tgz", + "integrity": "sha512-ndt1cvuuSC+i8fcxVsmeyRlgGsR1QsoAuIXz+eabj8/Y4GIWE2+mgHA7Hys61JDHOxttfWtXHtN2m5TNYTlORg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/text-hex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" }, "node_modules/tiny-typed-emitter": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" }, "node_modules/tough-cookie": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -1689,16 +1772,18 @@ }, "node_modules/triple-beam": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/triple-beam/-/triple-beam-1.4.1.tgz", "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", "engines": { "node": ">= 14.0.0" } }, "node_modules/tunnel-agent": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" }, @@ -1708,43 +1793,49 @@ }, "node_modules/tweetnacl": { "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "7.18.2", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "resolved": "https://mvn.tursom.cn:20080/repository/npm/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, "node_modules/uuid": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", "bin": { "uuid": "bin/uuid" } }, "node_modules/verror": { "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/verror/-/verror-1.10.0.tgz", "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "engines": [ "node >=0.6.0" ], + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -1753,16 +1844,18 @@ }, "node_modules/websocket-extensions": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", "engines": { "node": ">=0.8.0" } }, "node_modules/websocket13": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/websocket13/-/websocket13-4.0.0.tgz", - "integrity": "sha512-/ujP9ZfihyAZIXKGxcYpoe7Gj4r5o3WYSfP93o9lVNhhqoBtYba4m1s3mxdjKZu/HOhX5Mcqrt89dv/gC3b06A==", + "version": "4.1.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/websocket13/-/websocket13-4.1.0.tgz", + "integrity": "sha512-7+hxkUVTKQlUDTzN2rJI7fJRBXCT6dvRXr1aZflxUZlpNJutHBkKiEIbZOCGs0A1s7vxAmcAXngsNUQMSUTiVQ==", + "license": "MIT", "dependencies": { "@doctormckay/stdlib": "^2.7.1", "bytebuffer": "^5.0.1", @@ -1774,55 +1867,52 @@ "node": ">=12.22.0" } }, - "node_modules/websocket13/node_modules/@doctormckay/stdlib": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-2.10.0.tgz", - "integrity": "sha512-bwy+gPn6oa2KTpfxJKX3leZoV/wHDVtO0/gq3usPvqPswG//dcf3jVB8LcbRRsKO3BXCt5DqctOQ+Xb07ivxnw==", - "dependencies": { - "psl": "^1.9.0" - }, - "engines": { - "node": ">=12.22.0" - } - }, "node_modules/winston": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.12.0.tgz", - "integrity": "sha512-OwbxKaOlESDi01mC9rkM0dQqQt2I8DAUMRLZ/HpbwvDXm85IryEHgoogy5fziQy38PntgZsLlhAYHz//UPHZ5w==", + "version": "3.19.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", + "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.4.0", + "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.7.0" + "winston-transport": "^4.9.0" }, "engines": { "node": ">= 12.0.0" } }, "node_modules/winston-transport": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", - "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", + "version": "4.9.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", "dependencies": { - "logform": "^2.3.2", - "readable-stream": "^3.6.0", + "logform": "^2.7.0", + "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" }, "engines": { "node": ">= 12.0.0" } }, + "node_modules/winston/node_modules/async": { + "version": "3.2.6", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://mvn.tursom.cn:20080/repository/npm/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.19.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -1842,8 +1932,9 @@ }, "node_modules/xml2js": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/xml2js/-/xml2js-0.6.2.tgz", "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -1854,11 +1945,18 @@ }, "node_modules/xmlbuilder": { "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", "engines": { "node": ">=4.0" } + }, + "node_modules/zstddec": { + "version": "0.1.0", + "resolved": "https://mvn.tursom.cn:20080/repository/npm/zstddec/-/zstddec-0.1.0.tgz", + "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==", + "license": "MIT AND BSD-3-Clause" } } } diff --git a/package.json b/package.json index 8bf5497..d35326d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "commonjs", "main": "logger.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "STEAM_CHAT_DISABLE_AUTOSTART=1 node --test" }, "keywords": [], "author": "", diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..bdcd62e --- /dev/null +++ b/public/app.js @@ -0,0 +1,2523 @@ +(() => { + const targetIdInput = document.getElementById('targetId'); + const historyLimitInput = document.getElementById('historyLimit'); + const reloadHistoryButton = document.getElementById('reloadHistory'); + const openConversationButton = document.getElementById('openConversation'); + const reloadConversationsButton = document.getElementById('reloadConversations'); + const conversationListEl = document.getElementById('conversationList'); + const chatTitleEl = document.getElementById('chatTitle'); + const chatSubtitleEl = document.getElementById('chatSubtitle'); + const statusEl = document.getElementById('status'); + const messagesEl = document.getElementById('messages'); + const dropOverlay = document.getElementById('dropOverlay'); + const sidebarEl = document.querySelector('.sidebar'); + const sidebarBackdrop = document.getElementById('sidebarBackdrop'); + const mobileSidebarToggleButton = document.getElementById('mobileSidebarToggle'); + const closeSidebarButton = document.getElementById('closeSidebar'); + const imageLightbox = document.getElementById('imageLightbox'); + const imageLightboxViewport = document.getElementById('imageLightboxViewport'); + const imageLightboxImage = document.getElementById('imageLightboxImage'); + const imageLightboxCaption = document.getElementById('imageLightboxCaption'); + const closeImageLightboxButton = document.getElementById('closeImageLightbox'); + const imageZoomOutButton = document.getElementById('imageZoomOut'); + const imageZoomResetButton = document.getElementById('imageZoomReset'); + const imageZoomInButton = document.getElementById('imageZoomIn'); + const messageInput = document.getElementById('messageInput'); + const emoticonSuggestions = document.getElementById('emoticonSuggestions'); + const emoticonPreview = document.getElementById('emoticonPreview'); + const emoticonPreviewImage = document.getElementById('emoticonPreviewImage'); + const emoticonPreviewLabel = document.getElementById('emoticonPreviewLabel'); + const sendMessageButton = document.getElementById('sendMessage'); + const attachmentButton = document.getElementById('attachmentButton'); + const attachmentMenu = document.getElementById('attachmentMenu'); + const chooseImageButton = document.getElementById('chooseImageButton'); + const chooseUrlButton = document.getElementById('chooseUrlButton'); + const imageFileInput = document.getElementById('imageFile'); + const urlPanel = document.getElementById('urlPanel'); + const imageUrlInput = document.getElementById('imageUrl'); + const confirmImageUrlButton = document.getElementById('confirmImageUrl'); + const cancelImageUrlButton = document.getElementById('cancelImageUrl'); + const attachmentPreview = document.getElementById('attachmentPreview'); + const attachmentPreviewImage = document.getElementById('attachmentPreviewImage'); + const attachmentPreviewTitle = document.getElementById('attachmentPreviewTitle'); + const attachmentPreviewSubtitle = document.getElementById('attachmentPreviewSubtitle'); + const clearAttachmentButton = document.getElementById('clearAttachment'); + const uploadQueue = document.getElementById('uploadQueue'); + const uploadQueueList = document.getElementById('uploadQueueList'); + const pickerButton = document.getElementById('pickerButton'); + const pickerPanel = document.getElementById('pickerPanel'); + const pickerSearch = document.getElementById('pickerSearch'); + const pickerGrid = document.getElementById('pickerGrid'); + const pickerEmpty = document.getElementById('pickerEmpty'); + const pickerTabs = pickerPanel.querySelectorAll('.picker-tab'); + + targetIdInput.value = localStorage.getItem('steam-chat-target-id') || ''; + historyLimitInput.value = localStorage.getItem('steam-chat-history-limit') || '100'; + + let socket = null; + let nextRequestId = 1; + let activeConversationId = ''; + let conversations = []; + let pendingAttachment = null; + let dragDepth = 0; + const knownEmoticons = new Set(['steamhappy', 'steamfacepalm', 'steamthumbsup', 'steamheart', 'steamsad', 'steammocking']); + let currentSuggestions = []; + let activeSuggestionIndex = 0; + let uploadQueueItems = []; + const uploadRequestMap = new Map(); + const managedImageRequestMap = new WeakMap(); + let activePickerTab = 'emoticons'; + let emoticonInventory = []; + let stickerInventory = []; + const defaultDocumentTitle = document.title || 'Steam Chat'; + let unreadCount = 0; + let notificationPermissionRequested = false; + let nextManagedImageToken = 1; + const mobileLayoutMedia = window.matchMedia('(max-width: 900px)'); + const imageLightboxState = { + scale: 1, + minScale: 1, + maxScale: 6, + offsetX: 0, + offsetY: 0, + dragging: false, + dragPointerId: null, + dragStartX: 0, + dragStartY: 0, + dragOriginX: 0, + dragOriginY: 0, + activePointers: new Map(), + pinching: false, + pinchStartDistance: 0, + pinchStartScale: 1, + pinchContentX: 0, + pinchContentY: 0, + rafPending: false, + cachedBaseSize: null, + cachedViewportRect: null, + }; + + function setStatus(text) { + statusEl.textContent = text; + } + + function updateViewportHeightVar() { + const viewport = window.visualViewport; + const height = viewport && Number.isFinite(viewport.height) + ? viewport.height + : window.innerHeight; + const offsetTop = viewport && Number.isFinite(viewport.offsetTop) + ? viewport.offsetTop + : 0; + document.documentElement.style.setProperty('--app-height', height + 'px'); + document.documentElement.style.setProperty('--viewport-offset-top', offsetTop + 'px'); + } + + function isMobileLayout() { + return mobileLayoutMedia.matches; + } + + function setSidebarOpen(open) { + const shouldOpen = Boolean(open && isMobileLayout()); + sidebarEl.classList.toggle('open', shouldOpen); + sidebarBackdrop.classList.toggle('open', shouldOpen); + sidebarBackdrop.setAttribute('aria-hidden', shouldOpen ? 'false' : 'true'); + mobileSidebarToggleButton.setAttribute('aria-expanded', shouldOpen ? 'true' : 'false'); + } + + function closeSidebar() { + setSidebarOpen(false); + } + + function toggleSidebar() { + setSidebarOpen(!sidebarEl.classList.contains('open')); + } + + function syncResponsiveLayout() { + if (!isMobileLayout()) { + closeSidebar(); + } + syncMessageInputPlaceholder(); + } + + function autoResizeMessageInput() { + const minHeight = isMobileLayout() ? 30 : 80; + const maxHeight = isMobileLayout() ? 72 : 220; + messageInput.style.height = 'auto'; + const nextHeight = Math.max(minHeight, Math.min(messageInput.scrollHeight, maxHeight)); + messageInput.style.height = nextHeight + 'px'; + messageInput.style.overflowY = messageInput.scrollHeight > maxHeight ? 'auto' : 'hidden'; + } + + function syncMessageInputPlaceholder() { + const mobilePlaceholder = messageInput.dataset.mobilePlaceholder || '输入消息'; + const desktopPlaceholder = messageInput.dataset.desktopPlaceholder || mobilePlaceholder; + messageInput.placeholder = isMobileLayout() ? mobilePlaceholder : desktopPlaceholder; + } + + function updateDocumentTitle() { + document.title = unreadCount > 0 + ? '(' + unreadCount + ') ' + defaultDocumentTitle + : defaultDocumentTitle; + } + + function clearUnreadCount() { + if (!unreadCount) { + return; + } + + unreadCount = 0; + updateDocumentTitle(); + } + + function resolveDisplayImageUrl(url) { + const value = String(url || '').trim(); + if (!value) { + return ''; + } + + try { + const parsed = new URL(value, location.origin); + if (parsed.origin === location.origin) { + return parsed.toString(); + } + if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { + return buildCachedImageUrl(parsed.toString()); + } + return parsed.toString(); + } catch { + return value; + } + } + + function computeImageLightboxBaseSize() { + const viewportWidth = imageLightboxViewport.clientWidth || 1; + const viewportHeight = imageLightboxViewport.clientHeight || 1; + const naturalWidth = imageLightboxImage.naturalWidth || viewportWidth; + const naturalHeight = imageLightboxImage.naturalHeight || viewportHeight; + const fitScale = Math.min(viewportWidth / naturalWidth, viewportHeight / naturalHeight, 1); + + return { + width: naturalWidth * fitScale, + height: naturalHeight * fitScale, + viewportWidth, + viewportHeight, + }; + } + + function refreshImageLightboxBaseSize() { + imageLightboxState.cachedBaseSize = computeImageLightboxBaseSize(); + imageLightboxState.cachedViewportRect = imageLightboxViewport.getBoundingClientRect(); + } + + function getImageLightboxBaseSize() { + return imageLightboxState.cachedBaseSize || computeImageLightboxBaseSize(); + } + + function clampImageLightboxOffset() { + if (imageLightboxState.scale <= 1) { + imageLightboxState.offsetX = 0; + imageLightboxState.offsetY = 0; + return; + } + + const { width, height, viewportWidth, viewportHeight } = getImageLightboxBaseSize(); + const scaledWidth = width * imageLightboxState.scale; + const scaledHeight = height * imageLightboxState.scale; + const limitX = Math.max(0, (scaledWidth - viewportWidth) / 2); + const limitY = Math.max(0, (scaledHeight - viewportHeight) / 2); + + imageLightboxState.offsetX = Math.min(limitX, Math.max(-limitX, imageLightboxState.offsetX)); + imageLightboxState.offsetY = Math.min(limitY, Math.max(-limitY, imageLightboxState.offsetY)); + } + + function getViewportRelativePoint(clientX, clientY) { + const rect = imageLightboxState.cachedViewportRect || imageLightboxViewport.getBoundingClientRect(); + return { + x: clientX - rect.left - (rect.width / 2), + y: clientY - rect.top - (rect.height / 2), + }; + } + + function getTouchPointerList() { + return [...imageLightboxState.activePointers.values()].filter((pointer) => pointer.pointerType === 'touch'); + } + + function getTouchPointerMetrics(pointers) { + if (!pointers || pointers.length < 2) { + return null; + } + + const [first, second] = pointers; + const deltaX = second.clientX - first.clientX; + const deltaY = second.clientY - first.clientY; + + return { + distance: Math.hypot(deltaX, deltaY), + centerX: (first.clientX + second.clientX) / 2, + centerY: (first.clientY + second.clientY) / 2, + }; + } + + function applyImageLightboxTransform() { + imageLightboxState.rafPending = false; + clampImageLightboxOffset(); + imageLightboxImage.style.transform = 'translate3d(' + imageLightboxState.offsetX + 'px, ' + imageLightboxState.offsetY + 'px, 0) scale(' + imageLightboxState.scale + ')'; + } + + function updateImageLightboxTransform() { + const isActive = imageLightboxState.dragging || imageLightboxState.pinching; + imageLightboxImage.classList.toggle('is-dragging', isActive); + imageLightboxImage.style.cursor = imageLightboxState.scale > 1 + ? (isActive ? 'grabbing' : 'grab') + : 'zoom-in'; + imageZoomResetButton.textContent = Math.round(imageLightboxState.scale * 100) + '%'; + if (!imageLightboxState.rafPending) { + imageLightboxState.rafPending = true; + requestAnimationFrame(applyImageLightboxTransform); + } + } + + function scheduleTransformOnly() { + if (!imageLightboxState.rafPending) { + imageLightboxState.rafPending = true; + requestAnimationFrame(applyImageLightboxTransform); + } + } + + function beginImageLightboxDrag(pointerId, clientX, clientY) { + imageLightboxState.dragging = true; + imageLightboxState.dragPointerId = pointerId; + imageLightboxState.dragStartX = clientX; + imageLightboxState.dragStartY = clientY; + imageLightboxState.dragOriginX = imageLightboxState.offsetX; + imageLightboxState.dragOriginY = imageLightboxState.offsetY; + updateImageLightboxTransform(); + } + + function resetImageLightboxTransform() { + imageLightboxState.scale = 1; + imageLightboxState.offsetX = 0; + imageLightboxState.offsetY = 0; + imageLightboxState.dragging = false; + imageLightboxState.dragPointerId = null; + imageLightboxState.activePointers.clear(); + imageLightboxState.pinching = false; + imageLightboxState.pinchStartDistance = 0; + imageLightboxState.pinchStartScale = 1; + imageLightboxState.pinchContentX = 0; + imageLightboxState.pinchContentY = 0; + updateImageLightboxTransform(); + } + + function setImageLightboxScale(nextScale, clientX, clientY) { + const clampedScale = Math.min(imageLightboxState.maxScale, Math.max(imageLightboxState.minScale, nextScale)); + const previousScale = imageLightboxState.scale; + + if (Math.abs(clampedScale - previousScale) < 0.001) { + return; + } + + const anchorPoint = (typeof clientX === 'number' && typeof clientY === 'number') + ? getViewportRelativePoint(clientX, clientY) + : { x: 0, y: 0 }; + const anchorX = anchorPoint.x; + const anchorY = anchorPoint.y; + + imageLightboxState.offsetX = anchorX - (((anchorX - imageLightboxState.offsetX) / previousScale) * clampedScale); + imageLightboxState.offsetY = anchorY - (((anchorY - imageLightboxState.offsetY) / previousScale) * clampedScale); + imageLightboxState.scale = clampedScale; + updateImageLightboxTransform(); + } + + function beginImageLightboxPinch() { + const metrics = getTouchPointerMetrics(getTouchPointerList()); + if (!metrics) { + return; + } + + const center = getViewportRelativePoint(metrics.centerX, metrics.centerY); + imageLightboxState.pinching = true; + imageLightboxState.dragging = false; + imageLightboxState.dragPointerId = null; + imageLightboxState.pinchStartDistance = Math.max(metrics.distance, 1); + imageLightboxState.pinchStartScale = imageLightboxState.scale; + imageLightboxState.pinchContentX = (center.x - imageLightboxState.offsetX) / imageLightboxState.scale; + imageLightboxState.pinchContentY = (center.y - imageLightboxState.offsetY) / imageLightboxState.scale; + updateImageLightboxTransform(); + } + + function updateImageLightboxPinch() { + const metrics = getTouchPointerMetrics(getTouchPointerList()); + if (!metrics || !imageLightboxState.pinching) { + return; + } + + const center = getViewportRelativePoint(metrics.centerX, metrics.centerY); + const nextScale = Math.min( + imageLightboxState.maxScale, + Math.max( + imageLightboxState.minScale, + imageLightboxState.pinchStartScale * (metrics.distance / Math.max(imageLightboxState.pinchStartDistance, 1)), + ), + ); + + imageLightboxState.scale = nextScale; + imageLightboxState.offsetX = center.x - (imageLightboxState.pinchContentX * nextScale); + imageLightboxState.offsetY = center.y - (imageLightboxState.pinchContentY * nextScale); + scheduleTransformOnly(); + } + + function endImageLightboxPinch() { + if (!imageLightboxState.pinching) { + return; + } + + imageLightboxState.pinching = false; + imageLightboxState.pinchStartDistance = 0; + imageLightboxState.pinchStartScale = imageLightboxState.scale; + const remainingTouch = getTouchPointerList()[0]; + if (remainingTouch && imageLightboxState.scale > 1) { + beginImageLightboxDrag(remainingTouch.pointerId, remainingTouch.clientX, remainingTouch.clientY); + return; + } + updateImageLightboxTransform(); + } + + function openImageLightbox(url, caption) { + const displayUrl = resolveDisplayImageUrl(url); + if (!displayUrl) { + return; + } + + resetImageLightboxTransform(); + loadManagedImage(imageLightboxViewport, imageLightboxImage, displayUrl, { + loadingText: '大图加载中', + errorText: '大图加载失败', + onLoad() { + refreshImageLightboxBaseSize(); + updateImageLightboxTransform(); + }, + }); + imageLightboxCaption.textContent = caption || url || ''; + imageLightbox.classList.add('open'); + imageLightbox.setAttribute('aria-hidden', 'false'); + } + + function closeImageLightbox() { + if (!imageLightbox.classList.contains('open')) { + return; + } + + imageLightbox.classList.remove('open'); + imageLightbox.setAttribute('aria-hidden', 'true'); + resetManagedImage(imageLightboxViewport, imageLightboxImage); + imageLightboxCaption.textContent = ''; + imageLightboxState.cachedBaseSize = null; + imageLightboxState.cachedViewportRect = null; + resetImageLightboxTransform(); + } + + function makeImageZoomable(element, url, caption) { + element.classList.add('zoomable-image'); + element.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + openImageLightbox(url, caption || element.getAttribute('alt') || ''); + }); + } + + function currentTargetId() { + return targetIdInput.value.trim(); + } + + function currentHistoryLimit() { + const value = Number.parseInt(historyLimitInput.value, 10); + if (!Number.isFinite(value) || value <= 0) { + return 100; + } + return Math.min(value, 500); + } + + function savePreferences() { + localStorage.setItem('steam-chat-target-id', currentTargetId()); + localStorage.setItem('steam-chat-history-limit', String(currentHistoryLimit())); + } + + function formatConversationTime(value) { + if (!value) { + return ''; + } + return String(value).slice(5, 16); + } + + function parseDateString(value) { + if (!value) { + return null; + } + const match = String(value).match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?$/); + if (!match) { + return null; + } + return new Date( + Number(match[1]), + Number(match[2]) - 1, + Number(match[3]), + Number(match[4]), + Number(match[5]), + Number(match[6]), + Number(match[7] || 0), + ); + } + + function sameDay(left, right) { + return left && right && + left.getFullYear() === right.getFullYear() && + left.getMonth() === right.getMonth() && + left.getDate() === right.getDate(); + } + + function formatDayLabel(value) { + const date = parseDateString(value); + if (!date) { + return value || ''; + } + return date.getFullYear() + '-' + + String(date.getMonth() + 1).padStart(2, '0') + '-' + + String(date.getDate()).padStart(2, '0'); + } + + function formatTimeLabel(value) { + const date = parseDateString(value); + if (!date) { + return value || ''; + } + return String(date.getHours()).padStart(2, '0') + ':' + + String(date.getMinutes()).padStart(2, '0'); + } + + function extractStickerType(message) { + const match = String(message || '').match(/\[sticker\s+type="([^"]+)"/i); + return match ? match[1] : null; + } + + function extractEmoticonNames(message) { + const content = String(message || ''); + const names = new Set(); + + for (const match of content.matchAll(/\[emoticon\s+name="([^"]+)"\](?:\[\/emoticon\])?/gi)) { + if (match[1]) { + names.add(match[1]); + } + } + + for (const match of content.matchAll(/\[emoticon\]([^\[]+)\[\/emoticon\]/gi)) { + if (match[1]) { + names.add(match[1].trim()); + } + } + + for (const match of content.matchAll(/(^|\s):([a-z0-9_][a-z0-9_\-]*):(?=\s|$|[!?,.])/gi)) { + if (match[2]) { + names.add(match[2]); + } + } + + return [...names]; + } + + function extractImageUrls(message) { + const content = String(message || ''); + const urls = new Set(); + + for (const match of content.matchAll(/\[img\](https?:\/\/[^\s\[\]]+?)\[\/img\]/gi)) { + if (match[1]) { + urls.add(match[1]); + } + } + + for (const match of content.matchAll(/]*?\bsrc=["'](https?:\/\/[^"']+)["'][^>]*>/gi)) { + if (match[1]) { + urls.add(match[1]); + } + } + + for (const match of content.matchAll(/https?:\/\/\S+?(?:png|jpe?g|gif|webp|bmp)(?:\?\S*)?/gi)) { + if (match[0]) { + urls.add(match[0]); + } + } + + return [...urls]; + } + + function parseBbCodeAttributes(rawAttributes) { + const attrs = {}; + const content = String(rawAttributes || ''); + const attributeRegex = /([a-z][a-z0-9_-]*)=(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^\s"'=<>`]+))/gi; + let match; + + while ((match = attributeRegex.exec(content)) !== null) { + const key = match[1].toLowerCase(); + const value = match[2] ?? match[3] ?? match[4] ?? ''; + attrs[key] = value.replace(/\\(["'])/g, '$1'); + } + + return attrs; + } + + function extractOpenGraphEmbeds(message) { + const content = String(message || ''); + const embeds = []; + + for (const match of content.matchAll(/\[og\s+([^\]]+)\]([\s\S]*?)\[\/og\]/gi)) { + const attrs = parseBbCodeAttributes(match[1] || ''); + const fallbackUrl = String(match[2] || '').trim(); + + embeds.push({ + url: attrs.url || fallbackUrl, + img: attrs.img || null, + title: attrs.title || '', + }); + } + + return embeds.filter((item) => item.url); + } + + function buildSteamEmoticonUrl(name, large = true) { + const normalized = String(name || '').trim().replace(/^:+|:+$/g, ''); + if (!normalized) { + return ''; + } + return 'https://steamcommunity-a.akamaihd.net/economy/' + (large ? 'emoticonlarge' : 'emoticon') + '/' + encodeURIComponent(normalized); + } + + function buildCachedImageUrl(url) { + return location.origin + '/proxy/image?url=' + encodeURIComponent(String(url || '')); + } + + function createManagedImageHost(hostClassName, imageClassName) { + const host = document.createElement('div'); + host.className = 'image-loading-host'; + if (hostClassName) { + hostClassName.split(/\s+/).filter(Boolean).forEach((className) => host.classList.add(className)); + } + + const img = document.createElement('img'); + img.dataset.managedImage = 'true'; + img.classList.add('image-loading-target'); + if (imageClassName) { + imageClassName.split(/\s+/).filter(Boolean).forEach((className) => img.classList.add(className)); + } + + host.appendChild(img); + return { host, img }; + } + + function ensureManagedImageUi(host) { + if (host.__managedImageUi) { + return host.__managedImageUi; + } + + const overlay = document.createElement('div'); + overlay.className = 'image-loading-overlay'; + + const label = document.createElement('div'); + label.className = 'image-loading-label'; + label.textContent = '正在加载中'; + + const progress = document.createElement('div'); + progress.className = 'image-loading-progress'; + + const bar = document.createElement('div'); + bar.className = 'image-loading-progress-bar'; + + progress.appendChild(bar); + overlay.appendChild(label); + overlay.appendChild(progress); + host.appendChild(overlay); + + host.__managedImageUi = { overlay, label, progress, bar }; + return host.__managedImageUi; + } + + function setManagedImageState(host, state, labelText, progressValue, indeterminate) { + const ui = ensureManagedImageUi(host); + const nextState = state || 'idle'; + + host.classList.toggle('is-loading', nextState === 'loading'); + host.classList.toggle('is-loaded', nextState === 'loaded'); + host.classList.toggle('is-error', nextState === 'error'); + host.classList.toggle('is-indeterminate', Boolean(indeterminate)); + + if (labelText) { + ui.label.textContent = labelText; + } else if (nextState === 'error') { + ui.label.textContent = '图片加载失败'; + } else if (nextState === 'loaded') { + ui.label.textContent = ''; + } else { + ui.label.textContent = '正在加载中'; + } + + const width = Number.isFinite(progressValue) + ? Math.max(0, Math.min(100, progressValue)) + : 0; + ui.bar.style.width = width + '%'; + } + + function abortManagedImageRequest(img) { + const request = managedImageRequestMap.get(img); + if (!request) { + return; + } + + managedImageRequestMap.delete(img); + + try { + request.abort(); + } catch (error) { + // ignore + } + } + + function revokeManagedImageObjectUrl(img) { + const objectUrl = img && img.dataset ? img.dataset.objectUrl : ''; + if (!objectUrl) { + return; + } + + try { + URL.revokeObjectURL(objectUrl); + } catch (error) { + // ignore + } + + delete img.dataset.objectUrl; + } + + function resetManagedImage(host, img) { + abortManagedImageRequest(img); + revokeManagedImageObjectUrl(img); + delete img.dataset.managedImageLoadToken; + img.removeAttribute('src'); + + if (host) { + setManagedImageState(host, 'idle', '', 0, false); + } + } + + function cleanupManagedImages(root) { + if (!root || typeof root.querySelectorAll !== 'function') { + return; + } + + root.querySelectorAll('img[data-managed-image="true"]').forEach((img) => { + resetManagedImage(img.closest('.image-loading-host'), img); + }); + } + + function loadManagedImage(host, img, src, options) { + const settings = options || {}; + const loadingText = settings.loadingText || '正在加载中'; + const errorText = settings.errorText || '图片加载失败'; + const normalizedSrc = String(src || '').trim(); + + if (!host || !img) { + return; + } + + host.classList.add('image-loading-host'); + img.dataset.managedImage = 'true'; + img.classList.add('image-loading-target'); + + resetManagedImage(host, img); + + if (!normalizedSrc) { + setManagedImageState(host, 'error', errorText, 100, false); + if (typeof settings.onError === 'function') { + settings.onError(); + } + return; + } + + const token = String(nextManagedImageToken++); + img.dataset.managedImageLoadToken = token; + setManagedImageState(host, 'loading', loadingText, 8, true); + + const request = new XMLHttpRequest(); + managedImageRequestMap.set(img, request); + request.open('GET', normalizedSrc, true); + request.responseType = 'blob'; + + request.onprogress = (event) => { + if (img.dataset.managedImageLoadToken !== token) { + return; + } + + if (event.lengthComputable && event.total > 0) { + const percent = Math.max(1, Math.min(99, Math.round((event.loaded / event.total) * 100))); + setManagedImageState(host, 'loading', loadingText + ' ' + percent + '%', percent, false); + } else { + setManagedImageState(host, 'loading', loadingText, 32, true); + } + }; + + request.onerror = () => { + if (img.dataset.managedImageLoadToken !== token) { + return; + } + managedImageRequestMap.delete(img); + setManagedImageState(host, 'error', errorText, 100, false); + if (typeof settings.onError === 'function') { + settings.onError(); + } + }; + + request.onabort = () => { + if (img.dataset.managedImageLoadToken !== token) { + return; + } + managedImageRequestMap.delete(img); + }; + + request.onload = () => { + if (img.dataset.managedImageLoadToken !== token) { + return; + } + + managedImageRequestMap.delete(img); + + if (request.status < 200 || request.status >= 300 || !(request.response instanceof Blob)) { + setManagedImageState(host, 'error', errorText, 100, false); + if (typeof settings.onError === 'function') { + settings.onError(); + } + return; + } + + const objectUrl = URL.createObjectURL(request.response); + img.dataset.objectUrl = objectUrl; + + img.addEventListener('load', () => { + if (img.dataset.managedImageLoadToken !== token) { + return; + } + setManagedImageState(host, 'loaded', '', 100, false); + if (typeof settings.onLoad === 'function') { + settings.onLoad(); + } + }, { once: true }); + + img.addEventListener('error', () => { + if (img.dataset.managedImageLoadToken !== token) { + return; + } + revokeManagedImageObjectUrl(img); + setManagedImageState(host, 'error', errorText, 100, false); + if (typeof settings.onError === 'function') { + settings.onError(); + } + }, { once: true }); + + setManagedImageState(host, 'loading', '即将显示', 100, false); + img.src = objectUrl; + }; + + request.send(); + } + + function shouldRequestNotificationPermission() { + return typeof Notification !== 'undefined' + && Notification.permission === 'default' + && !notificationPermissionRequested; + } + + async function ensureNotificationPermission() { + if (!shouldRequestNotificationPermission()) { + return typeof Notification === 'undefined' ? 'unsupported' : Notification.permission; + } + + notificationPermissionRequested = true; + + try { + return await Notification.requestPermission(); + } catch (error) { + return Notification.permission; + } + } + + function warmupNotifications() { + ensureNotificationPermission().catch(() => {}); + } + + function shouldNotifyForEntry(entry) { + const activeId = activeConversationId || currentTargetId(); + return document.hidden || !document.hasFocus() || !activeId || entry.id !== activeId; + } + + function buildNotificationBody(entry) { + if (!entry) { + return '你有一条新消息'; + } + + if (entry.type === 'image' || entry.imageUrl) { + return '[图片]'; + } + + const stickerType = extractStickerType(entry.message); + if (stickerType) { + return '[贴纸] ' + stickerType.replace(/^Sticker_/, ''); + } + + const text = String(entry.message || '').replace(/\s+/g, ' ').trim(); + return text || '你有一条新消息'; + } + + function notifyIncomingEntry(entry) { + if (!entry || entry.echo || !shouldNotifyForEntry(entry)) { + return; + } + + unreadCount += 1; + updateDocumentTitle(); + + if (typeof Notification === 'undefined' || Notification.permission !== 'granted') { + return; + } + + const notification = new Notification(entry.name || entry.id || 'Steam Chat', { + body: buildNotificationBody(entry), + tag: 'steam-chat-' + (entry.id || 'unknown'), + }); + + notification.addEventListener('click', () => { + window.focus(); + if (entry.id) { + setActiveConversation(entry.id, entry.name); + requestHistory(); + } + clearUnreadCount(); + notification.close(); + }); + } + + function appendEmoticonImage(fragment, name) { + const rawUrl = buildSteamEmoticonUrl(name, true); + if (!rawUrl) { + fragment.appendChild(document.createTextNode(':' + name + ':')); + return; + } + + const link = document.createElement('a'); + link.href = rawUrl; + link.target = '_blank'; + link.rel = 'noreferrer'; + + const img = document.createElement('img'); + img.src = buildCachedImageUrl(rawUrl); + img.alt = ':' + name + ':'; + img.title = ':' + name + ':'; + img.style.display = 'inline-block'; + img.style.width = '28px'; + img.style.height = '28px'; + img.style.verticalAlign = 'middle'; + img.style.margin = '0 2px'; + img.style.objectFit = 'contain'; + link.appendChild(img); + fragment.appendChild(link); + } + + function appendInlineImage(fragment, url, altText) { + const link = document.createElement('a'); + link.href = url; + link.target = '_blank'; + link.rel = 'noreferrer'; + link.style.display = 'inline-block'; + link.style.margin = '4px 4px 4px 0'; + + const { host, img } = createManagedImageHost('image-loading-host--inline', 'image-preview'); + img.alt = altText || '[图片]'; + img.title = altText || url; + makeImageZoomable(host, url, altText || url); + loadManagedImage(host, img, buildCachedImageUrl(url)); + + link.appendChild(host); + fragment.appendChild(link); + } + + function appendOpenGraphCard(fragment, embed) { + const wrapper = document.createElement('a'); + wrapper.href = embed.url; + wrapper.target = '_blank'; + wrapper.rel = 'noreferrer'; + wrapper.style.display = 'flex'; + wrapper.style.flexDirection = 'column'; + wrapper.style.gap = '8px'; + wrapper.style.margin = '6px 0'; + wrapper.style.padding = '10px'; + wrapper.style.border = '1px solid #475569'; + wrapper.style.borderRadius = '12px'; + wrapper.style.background = 'rgba(15, 23, 42, 0.45)'; + wrapper.style.color = 'inherit'; + wrapper.style.textDecoration = 'none'; + + if (embed.img) { + const { host, img } = createManagedImageHost('image-loading-host--card', 'image-preview'); + img.alt = embed.title || embed.url; + makeImageZoomable(host, embed.img, embed.title || embed.url); + loadManagedImage(host, img, buildCachedImageUrl(embed.img)); + wrapper.appendChild(host); + } + + const title = document.createElement('div'); + title.style.fontWeight = '600'; + title.style.lineHeight = '1.5'; + title.textContent = embed.title || embed.url; + wrapper.appendChild(title); + + const url = document.createElement('div'); + url.style.fontSize = '12px'; + url.style.opacity = '0.8'; + url.textContent = embed.url; + wrapper.appendChild(url); + + fragment.appendChild(wrapper); + } + + function createRichMessageContent(text) { + const fragment = document.createDocumentFragment(); + // Normalize [img src=URL ...]...[/img] to [img]URL[/img] + const content = String(text || '').replace(/\[img\s+src=(https?:\/\/[^\s\]]+)[^\]]*\][\s\S]*?\[\/img\]/gi, '[img]$1[/img]'); + const tokenRegex = /(\[emoticon\s+name="([^"]+)"\](?:\[\/emoticon\])?)|(\[emoticon\]([^\[]+)\[\/emoticon\])|(:([a-z0-9_][a-z0-9_\-]*):)|(\[img\](https?:\/\/[^\s\[\]]+?)\[\/img\])|(]*?\bsrc=["'](https?:\/\/[^"']+)["'][^>]*>)|(\[og\s+([^\]]+)\]([\s\S]*?)\[\/og\])|(\[url=([^\]]+)\]([\s\S]*?)\[\/url\])|(\[url\]([\s\S]*?)\[\/url\])|(https?:\/\/\S+)/gi; + let cursor = 0; + let match; + + while ((match = tokenRegex.exec(content)) !== null) { + if (match.index > cursor) { + fragment.appendChild(document.createTextNode(content.slice(cursor, match.index))); + } + + if (match[2]) { + appendEmoticonImage(fragment, match[2]); + } else if (match[4]) { + appendEmoticonImage(fragment, match[4].trim()); + } else if (match[6]) { + appendEmoticonImage(fragment, match[6]); + } else if (match[8]) { + appendInlineImage(fragment, match[8], '[img]'); + } else if (match[10]) { + appendInlineImage(fragment, match[10], ''); + } else if (match[11]) { + const attrs = parseBbCodeAttributes(match[12] || ''); + const fallbackUrl = String(match[13] || '').trim(); + const embed = { + url: attrs.url || fallbackUrl, + img: attrs.img || null, + title: attrs.title || '', + }; + if (embed.url) { + appendOpenGraphCard(fragment, embed); + } + } else if (match[14]) { + // [url=HREF]LABEL[/url] + const href = match[15]; + const label = match[16] || href; + const link = document.createElement('a'); + link.href = href; + link.target = '_blank'; + link.rel = 'noreferrer'; + link.textContent = label; + fragment.appendChild(link); + } else if (match[17]) { + // [url]HREF[/url] + const href = match[18]; + const link = document.createElement('a'); + link.href = href; + link.target = '_blank'; + link.rel = 'noreferrer'; + link.textContent = href; + fragment.appendChild(link); + } else if (match[19]) { + const rawUrl = match[19]; + if (extractImageUrls(rawUrl).length > 0) { + appendInlineImage(fragment, rawUrl, rawUrl); + } else { + const link = document.createElement('a'); + link.href = rawUrl; + link.target = '_blank'; + link.rel = 'noreferrer'; + link.textContent = rawUrl; + fragment.appendChild(link); + } + } + + cursor = match.index + match[0].length; + } + + if (cursor < content.length) { + fragment.appendChild(document.createTextNode(content.slice(cursor))); + } + + return fragment; + } + + function clearMessages() { + cleanupManagedImages(messagesEl); + messagesEl.innerHTML = ''; + } + + function categorizeEmoticon(name) { + if (/^steam/i.test(name)) { + return 'Steam'; + } + return '最近使用'; + } + + function renderUploadQueue() { + uploadQueueList.innerHTML = ''; + uploadQueue.classList.toggle('active', uploadQueueItems.length > 0); + + uploadQueueItems.forEach((item) => { + const row = document.createElement('div'); + row.className = 'upload-queue-item'; + if (item.state) { + row.classList.add('is-' + item.state); + } + + const name = document.createElement('div'); + name.className = 'upload-queue-name'; + name.textContent = item.name; + + const status = document.createElement('div'); + status.className = 'upload-queue-status'; + status.textContent = item.statusText; + + const progress = document.createElement('div'); + progress.className = 'upload-queue-progress'; + + const bar = document.createElement('div'); + bar.className = 'upload-queue-progress-bar'; + if (item.state) { + bar.classList.add('is-' + item.state); + } + bar.style.width = item.progress + '%'; + + progress.appendChild(bar); + row.appendChild(name); + row.appendChild(status); + row.appendChild(progress); + uploadQueueList.appendChild(row); + }); + } + + function updateQueueItem(id, patch) { + const item = uploadQueueItems.find((entry) => entry.id === id); + if (!item) { + return; + } + Object.assign(item, patch); + renderUploadQueue(); + } + + function removeQueueItem(id) { + const nextItems = uploadQueueItems.filter((entry) => entry.id !== id); + if (nextItems.length === uploadQueueItems.length) { + return; + } + uploadQueueItems = nextItems; + renderUploadQueue(); + } + + function scheduleQueueItemRemoval(id, delay) { + window.setTimeout(() => { + removeQueueItem(id); + }, delay || 1500); + } + + function createQueueItem(name, statusText, progress) { + const queueId = 'queue-' + Date.now() + '-' + Math.random().toString(16).slice(2); + uploadQueueItems.push({ + id: queueId, + name: name || '图片', + progress: Number.isFinite(progress) ? progress : 0, + statusText: statusText || '准备中', + state: 'pending', + }); + renderUploadQueue(); + return queueId; + } + + function markQueueItemCompleted(id, statusText) { + updateQueueItem(id, { + progress: 100, + statusText: statusText || '已发送', + state: 'done', + }); + scheduleQueueItemRemoval(id, 1200); + } + + function markQueueItemFailed(id, statusText) { + updateQueueItem(id, { + progress: 100, + statusText: statusText || '发送失败', + state: 'error', + }); + scheduleQueueItemRemoval(id, 2500); + } + + function resolveUploadRequest(requestId, ok, message) { + if (!requestId || !uploadRequestMap.has(requestId)) { + return false; + } + + const queueId = uploadRequestMap.get(requestId); + uploadRequestMap.delete(requestId); + + if (ok) { + markQueueItemCompleted(queueId, message || '已发送'); + } else { + markQueueItemFailed(queueId, message || '发送失败'); + } + return true; + } + + function clearPendingUploadRequests(message) { + for (const [, queueId] of uploadRequestMap.entries()) { + markQueueItemFailed(queueId, message || '发送中断'); + } + uploadRequestMap.clear(); + } + + function buildSteamStickerCandidateUrls(type) { + const normalized = String(type || '').trim(); + if (!normalized) { + return []; + } + + return [ + 'https://steamcommunity-a.akamaihd.net/economy/sticker/' + encodeURIComponent(normalized), + 'https://steamcommunity-a.akamaihd.net/economy/stickerlarge/' + encodeURIComponent(normalized), + 'https://steamcommunity.com/economy/sticker/' + encodeURIComponent(normalized), + 'https://steamcommunity.com/economy/stickerlarge/' + encodeURIComponent(normalized), + location.origin + '/proxy/sticker/' + encodeURIComponent(normalized), + ]; + } + + function closeAttachmentMenu() { + attachmentMenu.classList.remove('open'); + } + + function toggleAttachmentMenu() { + attachmentMenu.classList.toggle('open'); + } + + function closeUrlPanel() { + urlPanel.classList.remove('open'); + } + + function togglePickerPanel() { + const isOpen = pickerPanel.classList.toggle('open'); + if (isOpen) { + closeAttachmentMenu(); + pickerSearch.value = ''; + renderPickerGrid(); + pickerSearch.focus(); + } + } + + function closePickerPanel() { + pickerPanel.classList.remove('open'); + } + + function setPickerTab(tab) { + activePickerTab = tab; + pickerTabs.forEach((btn) => { + btn.classList.toggle('active', btn.dataset.tab === tab); + }); + pickerSearch.value = ''; + renderPickerGrid(); + } + + function renderPickerGrid() { + pickerGrid.innerHTML = ''; + const query = (pickerSearch.value || '').trim().toLowerCase(); + + if (activePickerTab === 'emoticons') { + pickerGrid.className = 'picker-grid emoticon-grid'; + const items = emoticonInventory + .filter((e) => !query || e.name.toLowerCase().includes(query)) + .sort((a, b) => (b.use_count || 0) - (a.use_count || 0) || a.name.localeCompare(b.name)); + + if (!items.length) { + pickerEmpty.textContent = emoticonInventory.length ? '无匹配结果' : '加载中…'; + pickerEmpty.classList.add('active'); + return; + } + pickerEmpty.classList.remove('active'); + + items.forEach((e) => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'picker-item emoticon-item'; + btn.title = ':' + e.name + ':'; + + const img = document.createElement('img'); + img.src = buildCachedImageUrl(buildSteamEmoticonUrl(e.name, true)); + img.alt = e.name; + img.loading = 'lazy'; + + const label = document.createElement('div'); + label.className = 'picker-item-name'; + label.textContent = e.name; + + btn.appendChild(img); + btn.appendChild(label); + btn.addEventListener('click', () => { + insertEmoticonAtCursor(e.name); + closePickerPanel(); + }); + pickerGrid.appendChild(btn); + }); + } else { + pickerGrid.className = 'picker-grid sticker-grid'; + const items = stickerInventory + .filter((s) => !query || s.name.toLowerCase().includes(query)) + .sort((a, b) => (b.use_count || 0) - (a.use_count || 0) || a.name.localeCompare(b.name)); + + if (!items.length) { + pickerEmpty.textContent = stickerInventory.length ? '无匹配结果' : '加载中…'; + pickerEmpty.classList.add('active'); + return; + } + pickerEmpty.classList.remove('active'); + + items.forEach((s) => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'picker-item sticker-item'; + btn.title = s.name; + + const img = document.createElement('img'); + img.src = location.origin + '/proxy/sticker/' + encodeURIComponent(s.name); + img.alt = s.name; + img.loading = 'lazy'; + + const label = document.createElement('div'); + label.className = 'picker-item-name'; + label.textContent = s.name.replace(/^Sticker_/, ''); + + btn.appendChild(img); + btn.appendChild(label); + btn.addEventListener('click', () => { + sendStickerMessage(s.name); + closePickerPanel(); + }); + pickerGrid.appendChild(btn); + }); + } + } + + function insertEmoticonAtCursor(name) { + const value = messageInput.value; + const selectionStart = messageInput.selectionStart || 0; + const selectionEnd = messageInput.selectionEnd || selectionStart; + let replaceStart = selectionStart; + let replaceEnd = selectionEnd; + + if (selectionStart === selectionEnd) { + const leftSide = value.slice(0, selectionStart); + const colonIndex = leftSide.lastIndexOf(':'); + + if (colonIndex !== -1) { + const partialName = value.slice(colonIndex + 1, selectionStart); + if (/^[a-z0-9_\-]*$/i.test(partialName)) { + let tokenEnd = selectionStart; + + while (tokenEnd < value.length && /[a-z0-9_\-]/i.test(value.charAt(tokenEnd))) { + tokenEnd += 1; + } + + if (value.charAt(tokenEnd) === ':') { + tokenEnd += 1; + } + + replaceStart = colonIndex; + replaceEnd = tokenEnd; + } + } + } + + const before = value.slice(0, replaceStart); + const after = value.slice(replaceEnd); + const insertion = ':' + name + ': '; + messageInput.value = before + insertion + after; + const newCaret = replaceStart + insertion.length; + messageInput.setSelectionRange(newCaret, newCaret); + autoResizeMessageInput(); + messageInput.focus(); + knownEmoticons.add(name); + } + + function sendStickerMessage(name) { + const id = activeConversationId || currentTargetId(); + if (!id) { + setStatus('请先选择会话'); + return; + } + const msg = '[sticker type="' + name + '" limit="0"][/sticker]'; + if (send({ + type: 'send_message', + requestId: 'sticker-' + (nextRequestId++), + id, + msg, + })) { + setStatus('贴纸已发送'); + } + } + + function fetchEmoticonInventory() { + send({ + type: 'get_emoticons', + requestId: 'emoticons-' + (nextRequestId++), + }); + } + + function revokeAttachmentPreviewUrl(attachment) { + if (attachment && attachment.previewUrl && String(attachment.previewUrl).startsWith('blob:')) { + URL.revokeObjectURL(attachment.previewUrl); + } + } + + function revokePendingAttachmentUrl() { + revokeAttachmentPreviewUrl(pendingAttachment); + } + + function clearPendingAttachment() { + revokePendingAttachmentUrl(); + pendingAttachment = null; + attachmentPreview.classList.remove('active'); + attachmentPreviewImage.hidden = true; + attachmentPreviewImage.removeAttribute('src'); + attachmentPreviewTitle.textContent = ''; + attachmentPreviewSubtitle.textContent = ''; + imageFileInput.value = ''; + } + + function setPendingAttachment(attachment) { + if (!attachment) { + clearPendingAttachment(); + return; + } + + revokePendingAttachmentUrl(); + pendingAttachment = attachment; + + attachmentPreview.classList.add('active'); + attachmentPreviewTitle.textContent = attachment.title || '待发送图片'; + attachmentPreviewSubtitle.textContent = attachment.subtitle || ''; + + if (attachment.previewUrl) { + attachmentPreviewImage.hidden = false; + attachmentPreviewImage.src = attachment.previewUrl; + } else { + attachmentPreviewImage.hidden = true; + attachmentPreviewImage.removeAttribute('src'); + } + } + + function readFileAsDataUrl(file, onProgress) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onprogress = (event) => { + if (typeof onProgress === 'function') { + onProgress(event.loaded || 0, event.total || file.size || 0); + } + }; + reader.onload = () => resolve(String(reader.result || '')); + reader.onerror = () => reject(reader.error || new Error('文件读取失败')); + reader.readAsDataURL(file); + }); + } + + function createFileAttachment(file, sourceLabel) { + if (!file) { + throw new Error('请选择图片文件'); + } + + return { + kind: 'file', + file, + previewUrl: URL.createObjectURL(file), + title: file.name || '待发送图片', + subtitle: sourceLabel || '本地图片', + }; + } + + async function resolveAttachmentPayload(attachment, onProgress) { + if (!attachment) { + throw new Error('缺少图片内容'); + } + + if (attachment.kind === 'url' || attachment.kind === 'base64') { + return attachment; + } + + if (attachment.kind !== 'file' || !attachment.file) { + throw new Error('暂不支持的图片类型'); + } + + const dataUrl = await readFileAsDataUrl(attachment.file, onProgress); + + const base64 = dataUrl.split(',')[1] || ''; + if (!base64) { + throw new Error('图片编码失败'); + } + + return { + kind: 'base64', + payload: base64, + previewUrl: attachment.previewUrl, + title: attachment.title, + subtitle: attachment.subtitle, + }; + } + + function attachFile(file, sourceLabel) { + if (!file) { + return; + } + + const attachment = createFileAttachment(file, sourceLabel); + setPendingAttachment(attachment); + setStatus((sourceLabel || '图片') + ' 已添加,点击发送即可发送'); + } + + async function sendFileDirectly(file, sourceLabel) { + const id = activeConversationId || currentTargetId(); + if (!id) { + await attachFile(file, sourceLabel); + setStatus('请先选择会话,图片已加入待发送'); + return; + } + + const attachment = createFileAttachment(file, sourceLabel); + if (await sendAttachmentWithQueue(id, attachment, 'drop-')) { + revokeAttachmentPreviewUrl(attachment); + setStatus((sourceLabel || '图片') + ' 已发送'); + } + } + + async function sendFilesDirectly(files, sourceLabel) { + const imageFiles = Array.from(files || []).filter((file) => String(file.type || '').startsWith('image/')); + if (!imageFiles.length) { + return; + } + + for (let index = 0; index < imageFiles.length; index += 1) { + await sendFileDirectly(imageFiles[index], imageFiles.length > 1 ? ((sourceLabel || '拖拽图片') + ' #' + (index + 1)) : sourceLabel); + } + + if (imageFiles.length > 1) { + setStatus('已连续发送 ' + imageFiles.length + ' 张图片'); + } + } + + function confirmImageUrl() { + const url = imageUrlInput.value.trim(); + if (!url) { + setStatus('请输入图片 URL'); + return; + } + + setPendingAttachment({ + kind: 'url', + payload: url, + previewUrl: url, + title: '待发送图片 URL', + subtitle: url, + }); + closeUrlPanel(); + closeAttachmentMenu(); + setStatus('图片 URL 已添加,点击发送即可发送'); + } + + function sendAttachmentPayload(id, attachment, requestId) { + const payload = { + type: 'send_image', + requestId, + id, + }; + + if (attachment.kind === 'url') { + payload.url = attachment.payload; + } else { + payload.img = attachment.payload; + } + + return send(payload); + } + + async function sendAttachmentWithQueue(id, attachment, requestPrefix) { + if (!id) { + throw new Error('请先选择会话'); + } + if (!attachment) { + return false; + } + + const name = attachment.title || attachment.subtitle || '图片'; + const isFileAttachment = attachment.kind === 'file' && attachment.file; + const queueId = createQueueItem(name, isFileAttachment ? '读取中 0%' : '准备发送', isFileAttachment ? 0 : 20); + + let resolvedAttachment; + try { + resolvedAttachment = await resolveAttachmentPayload(attachment, (loaded, total) => { + if (!isFileAttachment) { + return; + } + + const ratio = total > 0 ? loaded / total : 0; + const progress = Math.max(1, Math.min(90, Math.round(ratio * 90))); + const percent = Math.max(0, Math.min(100, Math.round(ratio * 100))); + + updateQueueItem(queueId, { + progress, + statusText: '读取中 ' + percent + '%', + state: 'pending', + }); + }); + } catch (error) { + markQueueItemFailed(queueId, error.message || '图片读取失败'); + throw error; + } + + const requestId = requestPrefix + (nextRequestId++); + updateQueueItem(queueId, { + progress: 95, + statusText: '等待发送确认', + state: 'pending', + }); + uploadRequestMap.set(requestId, queueId); + + if (!sendAttachmentPayload(id, resolvedAttachment, requestId)) { + uploadRequestMap.delete(requestId); + markQueueItemFailed(queueId, '发送失败'); + return false; + } + + return true; + } + + function hideSuggestions() { + currentSuggestions = []; + activeSuggestionIndex = 0; + emoticonSuggestions.classList.remove('open'); + } + + function getAutocompleteContext() { + const value = messageInput.value; + const caret = messageInput.selectionStart || 0; + const textBefore = value.slice(0, caret); + const match = textBefore.match(/(^|\s):([a-z0-9_][a-z0-9_\-]*)?$/i); + if (!match) { + return null; + } + + return { + start: caret - (match[2] ? match[2].length + 1 : 1), + end: caret, + query: (match[2] || '').toLowerCase(), + }; + } + + function refreshSuggestionHighlight() { + Array.from(emoticonSuggestions.querySelectorAll('.emoticon-option')).forEach((node, index) => { + node.classList.toggle('active', index === activeSuggestionIndex); + }); + if (currentSuggestions[activeSuggestionIndex]) { + updateSuggestionPreview(currentSuggestions[activeSuggestionIndex]); + } + } + + function updateSuggestionPreview(name) { + emoticonPreviewImage.src = buildSteamEmoticonUrl(name, true); + emoticonPreviewLabel.textContent = ':' + name + ': · ' + categorizeEmoticon(name); + } + + function applySuggestion(index = activeSuggestionIndex) { + const suggestion = currentSuggestions[index]; + const context = getAutocompleteContext(); + if (!suggestion || !context) { + hideSuggestions(); + return; + } + + const value = messageInput.value; + const replacement = ':' + suggestion + ': '; + messageInput.value = value.slice(0, context.start) + replacement + value.slice(context.end); + const caret = context.start + replacement.length; + messageInput.setSelectionRange(caret, caret); + knownEmoticons.add(suggestion); + autoResizeMessageInput(); + hideSuggestions(); + messageInput.focus(); + } + + function renderSuggestionList(names) { + Array.from(emoticonSuggestions.querySelectorAll('.emoticon-option')).forEach((node) => node.remove()); + currentSuggestions = names.slice(0, 8); + activeSuggestionIndex = 0; + + if (!currentSuggestions.length) { + hideSuggestions(); + return; + } + + updateSuggestionPreview(currentSuggestions[0]); + + currentSuggestions.forEach((name, index) => { + const option = document.createElement('button'); + option.type = 'button'; + option.className = 'emoticon-option' + (index === 0 ? ' active' : ''); + + const img = document.createElement('img'); + img.src = buildSteamEmoticonUrl(name, true); + img.alt = name; + + const label = document.createElement('div'); + label.innerHTML = ':' + name + ':
' + categorizeEmoticon(name) + '
'; + + option.appendChild(img); + option.appendChild(label); + option.addEventListener('click', () => applySuggestion(index)); + option.addEventListener('mouseenter', () => { + activeSuggestionIndex = index; + refreshSuggestionHighlight(); + updateSuggestionPreview(name); + }); + emoticonSuggestions.appendChild(option); + }); + + emoticonSuggestions.classList.add('open'); + } + + function updateEmoticonSuggestions() { + const context = getAutocompleteContext(); + if (!context) { + hideSuggestions(); + return; + } + + const query = context.query; + const names = [...knownEmoticons] + .filter((name) => !query || name.toLowerCase().includes(query)) + .sort((a, b) => { + const aLower = a.toLowerCase(); + const bLower = b.toLowerCase(); + const aCategory = categorizeEmoticon(a); + const bCategory = categorizeEmoticon(b); + const aStarts = query ? aLower.startsWith(query) : true; + const bStarts = query ? bLower.startsWith(query) : true; + + if (aStarts !== bStarts) { + return aStarts ? -1 : 1; + } + + if (aCategory !== bCategory) { + return aCategory.localeCompare(bCategory); + } + + return a.localeCompare(b); + }); + + renderSuggestionList(names); + } + + async function handlePasteImage(event) { + const items = Array.from((event.clipboardData && event.clipboardData.items) || []); + const imageItem = items.find((item) => item && item.type && item.type.startsWith('image/')); + if (!imageItem) { + return false; + } + + const file = imageItem.getAsFile(); + if (!file) { + return false; + } + + event.preventDefault(); + const attachment = createFileAttachment(file, '剪切板图片'); + const id = activeConversationId || currentTargetId(); + + if (id && socket && socket.readyState === WebSocket.OPEN) { + if (await sendAttachmentWithQueue(id, attachment, 'paste-')) { + revokeAttachmentPreviewUrl(attachment); + imageUrlInput.value = ''; + setStatus('剪切板图片发送中'); + return true; + } + } + + setPendingAttachment(attachment); + setStatus('剪切板图片已添加,点击发送即可发送'); + return true; + } + + function setActiveConversation(id, name) { + activeConversationId = id || ''; + targetIdInput.value = activeConversationId; + savePreferences(); + const conversation = conversations.find((item) => item.id === activeConversationId); + chatTitleEl.textContent = name || (conversation && conversation.name) || activeConversationId || '未选择会话'; + chatSubtitleEl.textContent = activeConversationId + ? ('SteamID64: ' + activeConversationId) + : '请选择左侧会话,或手动输入 SteamID64'; + renderConversations(); + + if (!document.hidden && document.hasFocus()) { + clearUnreadCount(); + } + + if (activeConversationId) { + closeSidebar(); + } + } + + function renderConversations() { + conversationListEl.innerHTML = ''; + + if (!conversations.length) { + const empty = document.createElement('div'); + empty.className = 'empty-state'; + empty.textContent = '暂无历史会话'; + conversationListEl.appendChild(empty); + return; + } + + conversations.forEach((conversation) => { + const item = document.createElement('button'); + item.type = 'button'; + item.className = 'conversation-item' + (conversation.id === activeConversationId ? ' active' : ''); + + const top = document.createElement('div'); + top.className = 'conversation-top'; + + const name = document.createElement('div'); + name.className = 'conversation-name'; + name.textContent = conversation.name || conversation.id; + + const time = document.createElement('div'); + time.className = 'conversation-time'; + time.textContent = formatConversationTime(conversation.updatedAt); + + top.appendChild(name); + top.appendChild(time); + + const preview = document.createElement('div'); + preview.className = 'conversation-preview'; + preview.textContent = conversation.preview || '[空会话]'; + + const idLine = document.createElement('div'); + idLine.className = 'conversation-id'; + idLine.textContent = conversation.id; + idLine.style.marginTop = '6px'; + + item.appendChild(top); + item.appendChild(preview); + item.appendChild(idLine); + item.addEventListener('click', () => { + setActiveConversation(conversation.id, conversation.name); + requestHistory(); + }); + conversationListEl.appendChild(item); + }); + } + + function insertDivider(text, className) { + const divider = document.createElement('div'); + divider.className = className; + divider.textContent = text; + messagesEl.appendChild(divider); + } + + function appendEntry(entry, previousEntry) { + if (!entry || !entry.id) { + return; + } + + const activeId = activeConversationId || currentTargetId(); + if (activeId && entry.id !== activeId) { + return; + } + + const currentDate = parseDateString(entry.date || entry.sentAt); + const previousDate = previousEntry ? parseDateString(previousEntry.date || previousEntry.sentAt) : null; + + if (currentDate && (!previousDate || !sameDay(currentDate, previousDate))) { + insertDivider(formatDayLabel(entry.date || entry.sentAt), 'day-divider'); + } else if (currentDate && previousDate && (currentDate.getTime() - previousDate.getTime()) >= 10 * 60 * 1000) { + insertDivider(formatTimeLabel(entry.date || entry.sentAt), 'time-divider'); + } + + const row = document.createElement('div'); + row.className = 'message-row ' + (entry.echo ? 'self' : 'other'); + + const meta = document.createElement('div'); + meta.className = 'message-meta'; + meta.textContent = (entry.name || (entry.echo ? '我' : '对方')) + ' · ' + formatTimeLabel(entry.date || entry.sentAt); + + const bubble = document.createElement('div'); + bubble.className = 'bubble'; + + if (entry.type === 'image' || entry.imageUrl) { + bubble.classList.add('image-bubble'); + + const rawImageUrl = entry.imageUrl; + const { host, img } = createManagedImageHost('image-loading-host--bubble'); + img.alt = 'image'; + makeImageZoomable(host, rawImageUrl, rawImageUrl); + loadManagedImage(host, img, buildCachedImageUrl(rawImageUrl)); + bubble.appendChild(host); + } else { + const stickerType = extractStickerType(entry.message); + if (stickerType) { + bubble.classList.add('sticker-bubble'); + + const stickerCandidates = buildSteamStickerCandidateUrls(stickerType); + if (stickerCandidates.length) { + const stickerImage = document.createElement('img'); + stickerImage.className = 'sticker-image'; + stickerImage.alt = stickerType; + let stickerIndex = 0; + stickerImage.src = location.origin + '/proxy/sticker/' + encodeURIComponent(stickerType); + stickerImage.addEventListener('error', () => { + stickerIndex += 1; + if (stickerIndex < stickerCandidates.length) { + stickerImage.src = stickerCandidates[stickerIndex]; + } else { + stickerImage.remove(); + } + }); + bubble.appendChild(stickerImage); + } + + const title = document.createElement('div'); + title.className = 'sticker-title'; + title.textContent = '\u2728'; + + const name = document.createElement('div'); + name.className = 'sticker-name'; + name.textContent = stickerType.replace(/^Sticker_/, ''); + + const raw = document.createElement('div'); + raw.style.marginTop = '6px'; + raw.style.fontSize = '12px'; + raw.style.opacity = '0.8'; + raw.textContent = 'Sticker'; + + bubble.appendChild(title); + bubble.appendChild(name); + bubble.appendChild(raw); + } else { + bubble.appendChild(createRichMessageContent(entry.message || '')); + } + } + + row.appendChild(meta); + row.appendChild(bubble); + messagesEl.appendChild(row); + messagesEl.scrollTop = messagesEl.scrollHeight; + } + + function renderHistory(items) { + clearMessages(); + if (!items.length) { + const empty = document.createElement('div'); + empty.className = 'empty-state'; + empty.textContent = '暂无历史消息'; + messagesEl.appendChild(empty); + return; + } + + let previousEntry = null; + items.forEach((entry) => { + appendEntry(entry, previousEntry); + previousEntry = entry; + }); + } + + function updateConversationList(entry) { + if (!entry || !entry.id) { + return; + } + + extractEmoticonNames(entry.message).forEach((name) => knownEmoticons.add(name)); + + const preview = entry.type === 'image' || entry.imageUrl + ? '[图片]' + : (extractStickerType(entry.message) ? '[贴纸] ' + extractStickerType(entry.message).replace(/^Sticker_/, '') : String(entry.message || '').trim().slice(0, 60)); + const current = conversations.find((item) => item.id === entry.id); + + if (current) { + current.name = entry.name || current.name; + current.updatedAt = entry.date || entry.sentAt || current.updatedAt; + current.preview = preview || current.preview; + } else { + conversations.push({ + id: entry.id, + name: entry.name || entry.id, + updatedAt: entry.date || entry.sentAt || '', + preview, + }); + } + + conversations.sort((left, right) => String(right.updatedAt || '').localeCompare(String(left.updatedAt || ''))); + renderConversations(); + } + + function send(payload) { + if (!socket || socket.readyState !== WebSocket.OPEN) { + setStatus('WebSocket 未连接'); + return false; + } + + savePreferences(); + socket.send(JSON.stringify(payload)); + return true; + } + + function requestConversations() { + send({ + type: 'get_conversations', + requestId: 'conv-' + (nextRequestId++), + limit: 200, + }); + } + + function requestHistory() { + const id = activeConversationId || currentTargetId(); + if (!id) { + clearMessages(); + setStatus('请输入对方 SteamID64 后再加载历史'); + return; + } + + send({ + type: 'get_history', + requestId: 'history-' + (nextRequestId++), + id, + limit: currentHistoryLimit(), + }); + } + + function sendImageFile() { + const file = imageFileInput.files && imageFileInput.files[0]; + if (!file) { + setStatus('请选择图片文件'); + return; + } + attachFile(file, '本地图片'); + } + + function connect(wsPath) { + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + socket = new WebSocket(protocol + '//' + location.host + wsPath); + + socket.addEventListener('open', () => { + setStatus('WebSocket 已连接'); + }); + + socket.addEventListener('close', () => { + clearPendingUploadRequests('连接已断开'); + setStatus('WebSocket 已断开,3 秒后重连'); + setTimeout(() => connect(wsPath), 3000); + }); + + socket.addEventListener('error', () => { + setStatus('WebSocket 连接异常'); + }); + + socket.addEventListener('message', (event) => { + let payload; + try { + payload = JSON.parse(event.data); + } catch (error) { + setStatus('收到无法解析的消息'); + return; + } + + switch (payload.type) { + case 'ready': + setStatus('WebSocket 已连接'); + requestConversations(); + fetchEmoticonInventory(); + break; + case 'emoticons': + emoticonInventory = (payload.data && payload.data.emoticons) || []; + emoticonInventory.forEach((e) => { e.name = e.name.replace(/^:+|:+$/g, ''); }); + stickerInventory = (payload.data && payload.data.stickers) || []; + emoticonInventory.forEach((e) => knownEmoticons.add(e.name)); + if (pickerPanel.classList.contains('open')) { + renderPickerGrid(); + } + break; + case 'conversations': + conversations = (payload.data && payload.data.items) || []; + renderConversations(); + if (activeConversationId) { + requestHistory(); + } else if (currentTargetId()) { + setActiveConversation(currentTargetId()); + requestHistory(); + } else if (conversations.length) { + setActiveConversation(conversations[0].id, conversations[0].name); + requestHistory(); + } + break; + case 'history': + ((payload.data && payload.data.items) || []).forEach((item) => extractEmoticonNames(item.message).forEach((name) => knownEmoticons.add(name))); + renderHistory((payload.data && payload.data.items) || []); + updateConversationList(((payload.data && payload.data.items) || []).slice(-1)[0]); + setStatus('历史消息已加载'); + break; + case 'message': + case 'image': + updateConversationList(payload.data); + appendEntry(payload.data, null); + notifyIncomingEntry(payload.data); + break; + case 'message_sent': + hideSuggestions(); + break; + case 'image_sent': + resolveUploadRequest(payload.requestId, true, '已发送'); + updateConversationList(payload.data); + appendEntry(payload.data, null); + setStatus('图片已发送'); + break; + case 'error': + resolveUploadRequest(payload.requestId, false, payload.message); + setStatus(payload.message || '请求失败'); + break; + case 'pong': + break; + default: + console.log('unknown payload', payload); + } + }); + } + + function openConversation() { + const id = currentTargetId(); + if (!id) { + setStatus('请输入 SteamID64'); + return; + } + + setActiveConversation(id); + requestHistory(); + } + + updateViewportHeightVar(); + syncResponsiveLayout(); + syncMessageInputPlaceholder(); + reloadHistoryButton.addEventListener('click', requestHistory); + reloadConversationsButton.addEventListener('click', requestConversations); + openConversationButton.addEventListener('click', openConversation); + targetIdInput.addEventListener('change', openConversation); + historyLimitInput.addEventListener('change', requestHistory); + mobileSidebarToggleButton.addEventListener('click', toggleSidebar); + closeSidebarButton.addEventListener('click', closeSidebar); + sidebarBackdrop.addEventListener('click', closeSidebar); + if (typeof mobileLayoutMedia.addEventListener === 'function') { + mobileLayoutMedia.addEventListener('change', syncResponsiveLayout); + } else if (typeof mobileLayoutMedia.addListener === 'function') { + mobileLayoutMedia.addListener(syncResponsiveLayout); + } + + sendMessageButton.addEventListener('click', async () => { + const id = activeConversationId || currentTargetId(); + const msg = messageInput.value.trim(); + if (!id || (!msg && !pendingAttachment)) { + setStatus('请输入目标 SteamID,并填写消息或添加图片'); + return; + } + + try { + if (pendingAttachment) { + const attachment = pendingAttachment; + if (await sendAttachmentWithQueue(id, attachment, 'img-')) { + clearPendingAttachment(); + imageUrlInput.value = ''; + setStatus('图片发送中'); + } + } + + if (msg) { + if (send({ + type: 'send_message', + requestId: 'msg-' + (nextRequestId++), + id, + msg, + })) { + messageInput.value = ''; + autoResizeMessageInput(); + } + } + } catch (error) { + setStatus(error.message || '发送图片失败'); + } + }); + + attachmentButton.addEventListener('click', (event) => { + event.stopPropagation(); + closePickerPanel(); + toggleAttachmentMenu(); + }); + + pickerButton.addEventListener('click', (event) => { + event.stopPropagation(); + togglePickerPanel(); + }); + + pickerTabs.forEach((tab) => { + tab.addEventListener('click', () => { + setPickerTab(tab.dataset.tab); + }); + }); + + pickerSearch.addEventListener('input', renderPickerGrid); + + pickerPanel.addEventListener('click', (event) => { + event.stopPropagation(); + }); + + chooseImageButton.addEventListener('click', () => { + closeAttachmentMenu(); + closeUrlPanel(); + imageFileInput.click(); + }); + + chooseUrlButton.addEventListener('click', () => { + closeAttachmentMenu(); + urlPanel.classList.add('open'); + imageUrlInput.focus(); + }); + + imageFileInput.addEventListener('change', () => { + try { + sendImageFile(); + } catch (error) { + setStatus(error.message || '读取图片失败'); + } + }); + + confirmImageUrlButton.addEventListener('click', confirmImageUrl); + cancelImageUrlButton.addEventListener('click', () => { + closeUrlPanel(); + imageUrlInput.value = ''; + }); + clearAttachmentButton.addEventListener('click', clearPendingAttachment); + + document.addEventListener('click', (event) => { + if (isMobileLayout() + && sidebarEl.classList.contains('open') + && !sidebarEl.contains(event.target) + && event.target !== mobileSidebarToggleButton) { + closeSidebar(); + } + if (!attachmentMenu.contains(event.target) && event.target !== attachmentButton) { + closeAttachmentMenu(); + } + if (!pickerPanel.contains(event.target) && event.target !== pickerButton) { + closePickerPanel(); + } + }); + + imageLightbox.addEventListener('click', (event) => { + if (event.target === imageLightbox) { + closeImageLightbox(); + } + }); + + imageLightboxViewport.addEventListener('wheel', (event) => { + if (!imageLightbox.classList.contains('open')) { + return; + } + + event.preventDefault(); + const factor = Math.exp(-event.deltaY * 0.0025); + setImageLightboxScale(imageLightboxState.scale * factor, event.clientX, event.clientY); + }, { passive: false }); + + imageLightboxViewport.addEventListener('pointerdown', (event) => { + if (!imageLightbox.classList.contains('open')) { + return; + } + + imageLightboxViewport.setPointerCapture(event.pointerId); + imageLightboxState.activePointers.set(event.pointerId, { + pointerId: event.pointerId, + pointerType: event.pointerType, + clientX: event.clientX, + clientY: event.clientY, + }); + + if (event.pointerType === 'touch') { + event.preventDefault(); + if (getTouchPointerList().length >= 2) { + beginImageLightboxPinch(); + return; + } + } + + if (event.button !== 0 || imageLightboxState.scale <= 1) { + return; + } + + event.preventDefault(); + beginImageLightboxDrag(event.pointerId, event.clientX, event.clientY); + }); + + imageLightboxViewport.addEventListener('pointermove', (event) => { + if (imageLightboxState.activePointers.has(event.pointerId)) { + imageLightboxState.activePointers.set(event.pointerId, { + pointerId: event.pointerId, + pointerType: event.pointerType, + clientX: event.clientX, + clientY: event.clientY, + }); + } + + if (imageLightboxState.pinching) { + event.preventDefault(); + updateImageLightboxPinch(); + return; + } + + if (!imageLightboxState.dragging || imageLightboxState.dragPointerId !== event.pointerId) { + return; + } + + imageLightboxState.offsetX = imageLightboxState.dragOriginX + (event.clientX - imageLightboxState.dragStartX); + imageLightboxState.offsetY = imageLightboxState.dragOriginY + (event.clientY - imageLightboxState.dragStartY); + scheduleTransformOnly(); + }); + + function stopImageLightboxDrag(event) { + const hadPointer = imageLightboxState.activePointers.delete(event.pointerId); + + if (imageLightboxState.pinching && getTouchPointerList().length < 2) { + endImageLightboxPinch(); + } + + if (!imageLightboxState.dragging || imageLightboxState.dragPointerId !== event.pointerId) { + if (hadPointer && imageLightboxViewport.hasPointerCapture(event.pointerId)) { + imageLightboxViewport.releasePointerCapture(event.pointerId); + } + return; + } + + imageLightboxState.dragging = false; + imageLightboxState.dragPointerId = null; + if (imageLightboxViewport.hasPointerCapture(event.pointerId)) { + imageLightboxViewport.releasePointerCapture(event.pointerId); + } + updateImageLightboxTransform(); + } + + imageLightboxViewport.addEventListener('pointerup', stopImageLightboxDrag); + imageLightboxViewport.addEventListener('pointercancel', stopImageLightboxDrag); + imageLightboxViewport.addEventListener('dblclick', (event) => { + event.preventDefault(); + if (imageLightboxState.scale > 1) { + resetImageLightboxTransform(); + return; + } + setImageLightboxScale(2, event.clientX, event.clientY); + }); + + imageLightboxImage.addEventListener('load', () => { + refreshImageLightboxBaseSize(); + resetImageLightboxTransform(); + }); + + imageZoomOutButton.addEventListener('click', () => { + setImageLightboxScale(imageLightboxState.scale / 1.2); + }); + + imageZoomResetButton.addEventListener('click', resetImageLightboxTransform); + + imageZoomInButton.addEventListener('click', () => { + setImageLightboxScale(imageLightboxState.scale * 1.2); + }); + + closeImageLightboxButton.addEventListener('click', closeImageLightbox); + + document.addEventListener('visibilitychange', () => { + if (!document.hidden && document.hasFocus()) { + clearUnreadCount(); + } + }); + + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape' && imageLightbox.classList.contains('open')) { + closeImageLightbox(); + return; + } + + if (!imageLightbox.classList.contains('open')) { + return; + } + + if (event.key === '+' || event.key === '=') { + event.preventDefault(); + setImageLightboxScale(imageLightboxState.scale * 1.2); + } else if (event.key === '-') { + event.preventDefault(); + setImageLightboxScale(imageLightboxState.scale / 1.2); + } else if (event.key === '0') { + event.preventDefault(); + resetImageLightboxTransform(); + } + }); + + window.addEventListener('focus', clearUnreadCount); + window.addEventListener('resize', () => { + updateViewportHeightVar(); + syncResponsiveLayout(); + autoResizeMessageInput(); + if (imageLightbox.classList.contains('open')) { + refreshImageLightboxBaseSize(); + } + }); + if (window.visualViewport) { + window.visualViewport.addEventListener('resize', () => { + updateViewportHeightVar(); + autoResizeMessageInput(); + }); + window.visualViewport.addEventListener('scroll', updateViewportHeightVar); + } + window.addEventListener('pointerdown', warmupNotifications, { once: true }); + window.addEventListener('keydown', warmupNotifications, { once: true }); + + messageInput.addEventListener('keydown', (event) => { + if (emoticonSuggestions.classList.contains('open')) { + if (event.key === 'ArrowDown') { + event.preventDefault(); + activeSuggestionIndex = (activeSuggestionIndex + 1) % currentSuggestions.length; + refreshSuggestionHighlight(); + return; + } + + if (event.key === 'ArrowUp') { + event.preventDefault(); + activeSuggestionIndex = (activeSuggestionIndex - 1 + currentSuggestions.length) % currentSuggestions.length; + refreshSuggestionHighlight(); + return; + } + + if (event.key === 'Tab' || (event.key === 'Enter' && !event.shiftKey)) { + event.preventDefault(); + applySuggestion(); + return; + } + + if (event.key === 'Escape') { + event.preventDefault(); + hideSuggestions(); + return; + } + } + + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + sendMessageButton.click(); + } + }); + + messageInput.addEventListener('input', () => { + autoResizeMessageInput(); + updateEmoticonSuggestions(); + }); + messageInput.addEventListener('click', updateEmoticonSuggestions); + messageInput.addEventListener('blur', () => { + setTimeout(hideSuggestions, 120); + }); + + messageInput.addEventListener('paste', (event) => { + handlePasteImage(event).catch((error) => { + setStatus(error.message || '处理剪切板图片失败'); + }); + }); + + document.addEventListener('paste', (event) => { + if (document.activeElement === messageInput) { + return; + } + + handlePasteImage(event).catch((error) => { + setStatus(error.message || '处理剪切板图片失败'); + }); + }); + + document.addEventListener('dragenter', (event) => { + const hasFile = Array.from((event.dataTransfer && event.dataTransfer.items) || []).some((item) => item.kind === 'file'); + if (!hasFile) { + return; + } + + dragDepth += 1; + dropOverlay.classList.add('active'); + }); + + document.addEventListener('dragover', (event) => { + const hasFile = Array.from((event.dataTransfer && event.dataTransfer.items) || []).some((item) => item.kind === 'file'); + if (!hasFile) { + return; + } + + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'copy'; + } + }); + + document.addEventListener('dragleave', () => { + dragDepth = Math.max(0, dragDepth - 1); + if (dragDepth === 0) { + dropOverlay.classList.remove('active'); + } + }); + + document.addEventListener('drop', (event) => { + const files = Array.from((event.dataTransfer && event.dataTransfer.files) || []).filter((file) => String(file.type || '').startsWith('image/')); + if (!files.length) { + dragDepth = 0; + dropOverlay.classList.remove('active'); + return; + } + + event.preventDefault(); + dragDepth = 0; + dropOverlay.classList.remove('active'); + + sendFilesDirectly(files, '拖拽图片').catch((error) => { + setStatus(error.message || '发送拖拽图片失败'); + }); + }); + + autoResizeMessageInput(); + + // Fetch wsPath from backend config, then connect + fetch('/api/config') + .then((res) => res.json()) + .then((config) => { + connect(config.wsPath || '/ws'); + }) + .catch(() => { + // Fallback to default wsPath + connect('/ws'); + }); +})(); diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..8d621c4 --- /dev/null +++ b/public/index.html @@ -0,0 +1,144 @@ + + + + + + Steam Chat + + + +
+
松开即可发送图片
+
+ + +
+ + +
+
+
+
+
未选择会话
+
请选择左侧会话,或手动输入 SteamID64
+
+
+ + WebSocket +
+
+
+ +
+
请选择一个会话开始聊天
+
+ +
+
+ +
+
+
+
+ +
+
+
发送队列
+
+
+
+ + + +
+
+
+ +
+ + +
支持直接粘贴剪切板图片到输入框
+
+ +
+
+ + +
+ +
+
加载中…
+
+ +
+ + +
+
+
+
+ + + + diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..15b2ea0 --- /dev/null +++ b/public/style.css @@ -0,0 +1,1335 @@ +:root { + color-scheme: dark; + --page-padding: 16px; + --app-height: 100dvh; + --viewport-offset-top: 0px; + --safe-top: env(safe-area-inset-top, 0px); + --safe-bottom: env(safe-area-inset-bottom, 0px); +} +* { box-sizing: border-box; } +html, body { + margin: 0; + height: 100%; + min-height: 100%; + overflow: hidden; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: #111827; + color: #e5e7eb; +} +body { + min-height: var(--app-height); +} +.app { + height: var(--app-height); + max-height: var(--app-height); + display: grid; + grid-template-columns: 320px minmax(0, 1fr); + gap: 16px; + padding: var(--page-padding); + overflow: hidden; +} +.card { + background: #1f2937; + border: 1px solid #374151; + border-radius: 12px; + padding: 16px; +} +.sidebar, .chat-panel { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + gap: 16px; + min-width: 0; +} +.toolbar, .send-row, .image-row, .chat-header { + display: flex; + gap: 12px; + flex-wrap: wrap; + align-items: center; +} +.chat-header { + justify-content: space-between; +} +.chat-header-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 8px; +} +.chat-heading { + min-width: 0; +} +.chat-title { + font-size: 18px; + font-weight: 700; + line-height: 1.35; +} +.chat-subtitle { + margin-top: 2px; + font-size: 13px; + color: #9ca3af; + line-height: 1.4; + word-break: break-word; +} +.chat-panel > .card { + flex-shrink: 0; +} +.chat-panel > #messages { + flex-shrink: 1; +} +label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 14px; + color: #d1d5db; + flex: 1; + min-width: 180px; +} +.field-label { + display: inline-flex; + align-items: center; + min-height: 18px; +} +input, textarea, button { + border-radius: 8px; + border: 1px solid #4b5563; + background: #111827; + color: #f9fafb; + padding: 10px 12px; + font: inherit; +} +textarea { + min-height: 80px; + resize: vertical; + width: 100%; +} +button { + cursor: pointer; + background: #2563eb; + border-color: #2563eb; +} +button.secondary { + background: #374151; + border-color: #4b5563; +} +button:disabled { + opacity: 0.6; + cursor: not-allowed; +} +.sidebar-backdrop { + position: fixed; + inset: 0; + background: rgba(2, 6, 23, 0.7); + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; + z-index: 35; +} +.sidebar-backdrop.open { + opacity: 1; + pointer-events: auto; +} +.sidebar-mobile-header, +.mobile-nav-button { + display: none; +} +#status { + font-size: 14px; + color: #93c5fd; +} +#conversationList { + overflow: auto; + display: flex; + flex-direction: column; + gap: 8px; + min-height: 0; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; +} +.conversation-item { + border: 1px solid #374151; + border-radius: 10px; + padding: 12px; + background: #111827; + cursor: pointer; +} +.conversation-item.active { + border-color: #2563eb; + background: #172554; +} +.conversation-top { + display: flex; + justify-content: space-between; + gap: 8px; + font-size: 14px; + margin-bottom: 6px; +} +.conversation-name { + font-weight: 600; + color: #f9fafb; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.conversation-time, .conversation-id { + color: #9ca3af; + font-size: 12px; +} +.conversation-preview { + font-size: 13px; + color: #d1d5db; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +#messages { + overflow: auto; + display: flex; + flex-direction: column; + gap: 8px; + min-height: 0; + flex: 1; + padding: 20px; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + background: + radial-gradient(circle at top left, rgba(37, 99, 235, 0.08), transparent 25%), + radial-gradient(circle at bottom right, rgba(99, 102, 241, 0.08), transparent 30%), + #0f172a; +} +.day-divider, .time-divider { + align-self: center; + font-size: 12px; + color: #9ca3af; + background: rgba(17, 24, 39, 0.9); + border: 1px solid #374151; + padding: 4px 10px; + border-radius: 999px; + margin: 8px 0; +} +.message-row { + display: flex; + flex-direction: column; + gap: 4px; + max-width: 78%; +} +.message-row.self { + align-self: flex-end; + align-items: flex-end; +} +.message-row.other { + align-self: flex-start; + align-items: flex-start; +} +.message-meta { + font-size: 12px; + color: #9ca3af; + padding: 0 4px; +} +.bubble { + padding: 10px 14px; + border-radius: 12px; + background: #1f2937; + border: 1px solid #374151; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; +} +.message-row.self .bubble { + background: #1d4ed8; + border-color: #2563eb; + color: #eff6ff; + border-bottom-right-radius: 4px; +} +.message-row.other .bubble { + border-bottom-left-radius: 4px; +} +.sticker-bubble { + min-width: 180px; + padding: 16px; + background: linear-gradient(135deg, #1e3a8a, #312e81); + border-color: #2563eb; +} +.sticker-title { + font-size: 26px; + margin-bottom: 6px; +} +.sticker-name { + font-size: 14px; + font-weight: 600; +} +.sticker-image { + max-width: min(240px, 100%); + max-height: 240px; + display: block; + margin-bottom: 10px; + object-fit: contain; + filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.28)); +} +.image-bubble .image-loading-host { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: min(180px, 100%); + min-height: 140px; + max-width: min(320px, 100%); + border-radius: 10px; +} +.image-bubble .image-loading-target { + max-width: min(320px, 100%); + max-height: 320px; + display: block; + border-radius: 10px; + background: #0b1220; + object-fit: contain; +} +.bubble a { + color: inherit; + text-decoration: underline; +} +.composer { + display: flex; + flex-direction: column; + gap: 12px; + flex-shrink: 0; +} +.composer-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 12px; + align-items: end; +} +.composer-actions { + position: relative; + display: flex; + align-items: center; + height: 100%; +} +.icon-button { + width: 44px; + height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 22px; + padding: 0; +} +.attachment-menu { + position: absolute; + left: 0; + bottom: 52px; + width: 200px; + display: none; + flex-direction: column; + gap: 8px; + padding: 10px; + background: #0f172a; + border: 1px solid #374151; + border-radius: 10px; + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28); + z-index: 20; +} +.attachment-menu.open { + display: flex; +} +.attachment-menu button { + width: 100%; + text-align: left; + background: #1f2937; + border-color: #374151; +} +.attachment-hint { + font-size: 12px; + color: #9ca3af; + line-height: 1.4; + padding: 4px 2px 0; +} +.attachment-preview { + display: none; + align-items: center; + gap: 12px; + padding: 12px; + border: 1px solid #374151; + border-radius: 12px; + background: #111827; +} +.attachment-preview.active { + display: flex; +} +.attachment-preview img { + width: 72px; + height: 72px; + object-fit: cover; + border-radius: 10px; + background: #0b1220; + border: 1px solid #374151; +} +.attachment-preview-body { + min-width: 0; + flex: 1; +} +.attachment-preview-title { + font-size: 14px; + font-weight: 600; + margin-bottom: 4px; +} +.attachment-preview-subtitle { + font-size: 12px; + color: #9ca3af; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.upload-queue { + display: none; + flex-direction: column; + gap: 8px; + padding: 12px; + border: 1px solid #374151; + border-radius: 12px; + background: #111827; +} +.upload-queue.active { + display: flex; +} +.upload-queue-title { + font-size: 13px; + color: #cbd5e1; + font-weight: 600; +} +.upload-queue-item { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + align-items: center; +} +.upload-queue-item.is-done .upload-queue-status { + color: #86efac; +} +.upload-queue-item.is-error .upload-queue-status { + color: #fca5a5; +} +.upload-queue-name { + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.upload-queue-status { + font-size: 12px; + color: #93c5fd; +} +.upload-queue-progress { + grid-column: 1 / -1; + height: 6px; + border-radius: 999px; + background: #1f2937; + overflow: hidden; +} +.upload-queue-progress-bar { + width: 0%; + height: 100%; + background: linear-gradient(90deg, #2563eb, #60a5fa); + transition: width 0.2s ease; +} +.upload-queue-progress-bar.is-done { + background: linear-gradient(90deg, #16a34a, #4ade80); +} +.upload-queue-progress-bar.is-error { + background: linear-gradient(90deg, #dc2626, #f87171); +} +.url-panel { + display: none; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 12px; + align-items: end; +} +.url-panel.open { + display: grid; +} +.drop-overlay { + position: fixed; + inset: 0; + display: none; + align-items: center; + justify-content: center; + background: rgba(15, 23, 42, 0.76); + backdrop-filter: blur(2px); + z-index: 50; +} +.drop-overlay.active { + display: flex; +} +.drop-overlay-card { + padding: 28px 34px; + border: 2px dashed #60a5fa; + border-radius: 18px; + background: rgba(30, 41, 59, 0.92); + color: #dbeafe; + font-size: 18px; + font-weight: 600; + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.28); +} +.zoomable-image { + cursor: zoom-in; +} +.image-loading-host { + position: relative; + overflow: hidden; + background: #0b1220; +} +.image-loading-target { + display: block; + max-width: 100%; + max-height: 100%; + opacity: 0; + transition: opacity 0.2s ease; +} +.image-loading-host.is-loaded .image-loading-target { + opacity: 1; +} +.image-loading-host.is-error .image-loading-target { + opacity: 0.2; +} +.image-loading-overlay { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + padding: 16px; + background: + linear-gradient(180deg, rgba(15, 23, 42, 0.82), rgba(15, 23, 42, 0.92)); + color: #dbeafe; + text-align: center; + pointer-events: none; + opacity: 0; + transition: opacity 0.2s ease; +} +.image-loading-host.is-loading .image-loading-overlay, +.image-loading-host.is-error .image-loading-overlay { + opacity: 1; +} +.image-loading-label { + font-size: 13px; + font-weight: 600; + line-height: 1.5; +} +.image-loading-progress { + width: min(220px, 80%); + height: 6px; + border-radius: 999px; + background: rgba(51, 65, 85, 0.95); + overflow: hidden; +} +.image-loading-progress-bar { + width: 0%; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, #2563eb, #60a5fa); + transition: width 0.2s ease; +} +.image-loading-host.is-indeterminate .image-loading-progress-bar { + width: 35%; + animation: image-loading-indeterminate 1.1s ease-in-out infinite; +} +.image-loading-host.is-error .image-loading-progress-bar { + width: 100%; + background: linear-gradient(90deg, #dc2626, #f87171); + animation: none; +} +.image-loading-host--inline { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: min(180px, 100%); + min-height: 120px; + max-width: 280px; + max-height: 280px; + border-radius: 8px; +} +.image-loading-host--inline .image-loading-target { + max-width: 280px; + max-height: 280px; + object-fit: contain; +} +.image-loading-host--card { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-height: 180px; + max-height: 220px; + border-radius: 10px; +} +.image-loading-host--card .image-loading-target { + width: 100%; + max-height: 220px; + object-fit: cover; +} +@keyframes image-loading-indeterminate { + 0% { + transform: translateX(-120%); + } + 100% { + transform: translateX(320%); + } +} +.image-lightbox { + position: fixed; + inset: 0; + display: none; + align-items: center; + justify-content: center; + padding: 8px; + background: rgba(2, 6, 23, 0.88); + z-index: 60; +} +.image-lightbox.open { + display: flex; +} +.image-lightbox-dialog { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: calc(100vw - 16px); + height: calc(100vh - 16px); + max-width: calc(100vw - 16px); + max-height: calc(100vh - 16px); + padding: 8px; + border-radius: 14px; + border: 1px solid #334155; + background: rgba(15, 23, 42, 0.96); + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.4); + overflow: hidden; +} +.image-lightbox-viewport { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + touch-action: none; + user-select: none; +} +.image-lightbox-viewport.image-loading-host { + background: transparent; +} +.image-lightbox-close { + position: absolute; + top: 12px; + right: 12px; + width: 40px; + height: 40px; + padding: 0; + font-size: 24px; + line-height: 1; + border-radius: 999px; +} +.image-lightbox img { + display: block; + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 12px; + background: #020617; + transform-origin: center center; + transition: transform 0.12s ease; + will-change: transform; + user-select: none; + -webkit-user-drag: none; +} +.image-lightbox img.is-dragging { + transition: none; +} +.image-lightbox-toolbar { + position: absolute; + left: 12px; + top: 12px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px; + border-radius: 12px; + background: rgba(15, 23, 42, 0.72); + backdrop-filter: blur(2px); +} +.image-lightbox-toolbar button { + min-width: 52px; + padding: 8px 10px; +} +.image-lightbox-caption { + position: absolute; + left: 12px; + right: 12px; + bottom: 12px; + padding: 8px 12px; + font-size: 12px; + color: #cbd5e1; + text-align: center; + word-break: break-all; + border-radius: 10px; + background: rgba(15, 23, 42, 0.72); + backdrop-filter: blur(2px); +} +.image-lightbox-caption:empty { + display: none; +} +.composer-field { + position: relative; + min-width: 0; +} +.emoticon-suggestions { + position: absolute; + left: 0; + right: 0; + bottom: calc(100% + 10px); + display: none; + flex-direction: column; + gap: 4px; + padding: 8px; + background: #0f172a; + border: 1px solid #374151; + border-radius: 12px; + box-shadow: 0 16px 32px rgba(0, 0, 0, 0.28); + max-height: 220px; + overflow: auto; + z-index: 25; +} +.emoticon-suggestions.open { + display: flex; +} +.emoticon-preview { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 10px 10px; + border-bottom: 1px solid #334155; + margin-bottom: 4px; +} +.emoticon-preview img { + width: 40px; + height: 40px; + object-fit: contain; + flex: none; +} +.emoticon-preview-label { + font-size: 13px; + color: #cbd5e1; +} +.emoticon-option { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + background: #111827; + border: 1px solid transparent; + border-radius: 10px; + padding: 8px 10px; + color: #e5e7eb; + text-align: left; +} +.emoticon-option.active { + border-color: #2563eb; + background: #172554; +} +.emoticon-option img { + width: 24px; + height: 24px; + object-fit: contain; + flex: none; +} +.emoticon-option code { + color: #bfdbfe; + background: transparent; + padding: 0; +} +.empty-state { + color: #9ca3af; + text-align: center; + margin: auto 0; +} +.chip { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 12px; + background: #111827; + border: 1px solid #374151; + color: #cbd5e1; +} +@media (max-width: 900px) { + .app { + display: flex; + flex-direction: column; + gap: 0; + height: var(--app-height); + max-height: var(--app-height); + padding: 0; + padding-top: max(var(--safe-top), var(--viewport-offset-top)); + padding-bottom: var(--safe-bottom); + background: #111827; + overflow: hidden; + } + .sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: min(88vw, 360px); + max-width: 360px; + padding: calc(12px + var(--safe-top)) 12px calc(12px + var(--safe-bottom)); + background: #111827; + border-right: 1px solid #374151; + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.42); + transform: translateX(-105%); + transition: transform 0.24s ease; + z-index: 40; + overflow: auto; + height: auto; + max-height: none; + color: #e5e7eb; + } + .sidebar.open { + transform: translateX(0); + } + .sidebar strong, + .sidebar label { + color: #d1d5db; + } + .sidebar input { + background: #111827; + border-color: #4b5563; + color: #f9fafb; + } + .sidebar button.secondary { + background: #374151; + border-color: #4b5563; + color: #f9fafb; + } + .sidebar #openConversation { + background: #2563eb; + border-color: #2563eb; + color: #ffffff; + } + .sidebar-mobile-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 0 2px; + } + .mobile-nav-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 32px; + padding: 0 10px; + border-radius: 16px; + background: #374151; + border-color: #4b5563; + color: #f9fafb; + } + .chat-panel, + .sidebar { + gap: 0; + } + .chat-panel { + flex: 1 1 auto; + height: auto; + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + gap: 0; + overflow: hidden; + } + .card { + padding: 0; + border: none; + border-radius: 0; + background: transparent; + } + .chat-topbar { + grid-row: 1; + padding: 10px 12px; + background: #1f2937; + border-bottom: 1px solid #374151; + } + .chat-header { + flex-wrap: nowrap; + align-items: center; + } + .chat-heading { + flex: 1; + min-width: 0; + } + .chat-header-actions { + flex-wrap: nowrap; + justify-content: flex-end; + margin-left: auto; + } + .chat-title { + font-size: 16px; + color: #f9fafb; + } + .chat-subtitle { + display: none; + } + .chip { + display: none; + } + #messages { + grid-row: 2; + min-height: 0; + padding: 12px 10px; + background: + radial-gradient(circle at top left, rgba(37, 99, 235, 0.08), transparent 25%), + radial-gradient(circle at bottom right, rgba(99, 102, 241, 0.08), transparent 30%), + #0f172a; + } + .message-meta { + font-size: 11px; + color: #9ca3af; + } + .message-row { + max-width: 88%; + } + .bubble { + max-width: 100%; + padding: 9px 12px; + line-height: 1.5; + border: 1px solid #374151; + box-shadow: none; + } + .day-divider, + .time-divider { + color: #9ca3af; + background: rgba(17, 24, 39, 0.9); + border: 1px solid #374151; + } + .message-row.self .bubble { + background: #1d4ed8; + border-color: #2563eb; + color: #eff6ff; + border-bottom-right-radius: 6px; + } + .message-row.other .bubble { + background: #1f2937; + border-color: #374151; + color: #e5e7eb; + border-bottom-left-radius: 6px; + } + .bubble a { + color: inherit; + } + .image-bubble img, + .sticker-image { + max-width: min(100%, 280px); + } + .conversation-card, + .sidebar-config-card { + background: transparent; + } + .conversation-item { + border-color: #374151; + background: #111827; + } + .conversation-item.active { + border-color: #2563eb; + background: #172554; + } + .conversation-name { + color: #f9fafb; + } + .conversation-preview { + color: #d1d5db; + } + .conversation-time, + .conversation-id, + #status { + color: #93c5fd; + } + .toolbar { + flex-direction: column; + align-items: stretch; + } + .toolbar label, + .toolbar button, + #reloadConversations, + #clearAttachment { + width: 100%; + min-width: 0; + } + .url-panel { + grid-template-columns: 1fr; + gap: 10px; + } + .attachment-preview { + align-items: flex-start; + flex-wrap: wrap; + gap: 10px; + padding: 10px; + background: #111827; + border: 1px solid #374151; + border-radius: 12px; + } + .attachment-preview img { + width: 56px; + height: 56px; + } + .attachment-preview-body { + width: calc(100% - 84px); + } + .upload-queue { + padding: 10px; + background: #111827; + border: 1px solid #374151; + border-radius: 12px; + } + .composer { + grid-row: 3; + gap: 8px; + padding: 5px 8px calc(6px + var(--safe-bottom)); + background: #1f2937; + border-top: 1px solid #374151; + box-shadow: 0 -10px 26px rgba(2, 6, 23, 0.22); + max-height: min(34vh, 260px); + overflow: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + } + .composer-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: "field actions"; + gap: 6px 8px; + align-items: center; + } + .composer-actions { + grid-area: actions; + justify-content: flex-end; + gap: 6px; + height: auto; + } + .composer-field { + grid-area: field; + display: flex; + align-items: center; + height: 36px; + min-height: 36px; + padding: 0 10px; + border: 1px solid #334155; + border-radius: 18px; + background: #0f172a; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + } + .composer-field:focus-within { + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.16); + } + .composer-field .field-label { + display: none; + } + #messageInput { + min-height: 0; + height: 34px; + max-height: 72px; + border: none; + background: transparent; + padding: 0; + line-height: 34px; + resize: none; + overflow-y: hidden; + box-shadow: none; + color: #f9fafb; + } + #messageInput::placeholder { + color: #9ca3af; + } + #messageInput:focus { + outline: none; + } + #sendMessage { + display: none; + } + textarea, + input, + button { + font-size: 16px; + } + .icon-button { + width: 36px; + height: 36px; + font-size: 17px; + border-radius: 999px; + background: #0f172a; + border-color: #334155; + color: #dbeafe; + } + .sidebar-config-card .field-label, + .url-panel .field-label { + font-size: 12px; + } + .attachment-menu { + position: fixed; + left: 12px; + right: 12px; + bottom: calc(88px + var(--safe-bottom)); + width: auto; + max-width: none; + background: #0f172a; + border-color: #374151; + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28); + } + .attachment-menu button { + background: #1f2937; + border-color: #374151; + color: #e5e7eb; + } + .attachment-hint { + color: #9ca3af; + } + .picker-panel { + background: #0f172a; + border-color: #374151; + box-shadow: 0 16px 32px rgba(0, 0, 0, 0.32); + } + .picker-tab { + color: #9ca3af; + } + .picker-tab.active { + color: #e5e7eb; + border-bottom-color: #2563eb; + } + .picker-item:hover { + background: #1e293b; + border-color: #334155; + } + .picker-item { + color: #e5e7eb; + } + .picker-item-name, + .picker-empty { + color: #9ca3af; + } + .image-lightbox-dialog { + width: 100vw; + height: 100vh; + max-width: 100vw; + max-height: 100vh; + padding: 0; + border-radius: 0; + } + .image-lightbox-toolbar { + left: 8px; + top: 8px; + gap: 6px; + padding: 6px; + } + .image-lightbox-toolbar button { + min-width: 46px; + } + .image-lightbox-close { + top: 8px; + right: 8px; + } + .image-lightbox-caption { + left: 8px; + right: 8px; + bottom: calc(8px + var(--safe-bottom)); + } +} +@media (max-width: 600px) { + :root { + --page-padding: 12px; + } + .app { + gap: 0; + } + .chat-topbar { + padding: 8px 10px; + } + .chat-header { + align-items: center; + gap: 6px; + } + .chat-header-actions { + width: auto; + justify-content: flex-end; + gap: 6px; + } + .conversation-top { + align-items: flex-start; + } + .conversation-time { + white-space: nowrap; + } + #messages { + padding: 10px 8px; + } + .attachment-preview-body { + width: 100%; + } + .composer { + padding: 4px 6px calc(4px + var(--safe-bottom)); + max-height: min(36vh, 260px); + } + .composer-row { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: "field actions"; + gap: 6px; + } + .composer-actions { + gap: 6px; + } + .composer-field { + height: 36px; + min-height: 36px; + padding: 0 8px; + } + #messageInput { + max-height: 68px; + } + .sidebar { + width: 92vw; + } +} +.content img, .image-preview { + border-radius: 8px; + border: 1px solid #4b5563; +} +.image-loading-target.image-preview { + border: 0; +} +.picker-panel { + position: absolute; + left: 0; + bottom: 52px; + width: 360px; + display: none; + flex-direction: column; + background: #0f172a; + border: 1px solid #374151; + border-radius: 12px; + box-shadow: 0 16px 32px rgba(0, 0, 0, 0.32); + z-index: 30; + max-height: 400px; +} +.picker-panel.open { + display: flex; +} +.picker-tabs { + display: flex; + border-bottom: 1px solid #374151; + flex-shrink: 0; +} +.picker-tab { + flex: 1; + padding: 10px 0; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + color: #9ca3af; + font-size: 14px; + cursor: pointer; + border-radius: 0; +} +.picker-tab.active { + color: #e5e7eb; + border-bottom-color: #2563eb; + background: transparent; +} +.picker-search { + padding: 8px 10px; + flex-shrink: 0; +} +.picker-search input { + width: 100%; + padding: 6px 10px; + font-size: 13px; + border-radius: 6px; +} +.picker-grid { + display: grid; + gap: 4px; + padding: 6px 10px 10px; + overflow: auto; + min-height: 0; + flex: 1; +} +.picker-grid.emoticon-grid { + grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); +} +.picker-grid.sticker-grid { + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); +} +.picker-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 4px; + border-radius: 8px; + cursor: pointer; + border: 1px solid transparent; + background: transparent; + color: #e5e7eb; +} +.picker-item:hover { + background: #1e293b; + border-color: #334155; +} +.picker-item img { + object-fit: contain; +} +.picker-item.emoticon-item img { + width: 32px; + height: 32px; +} +.picker-item.sticker-item img { + width: 72px; + height: 72px; +} +.picker-item-name { + font-size: 10px; + color: #9ca3af; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + text-align: center; +} +.picker-empty { + display: none; + color: #9ca3af; + text-align: center; + padding: 24px 10px; + font-size: 13px; +} +.picker-empty.active { + display: block; +} +@media (max-width: 900px) { + .picker-panel { + position: fixed; + left: 12px; + right: 12px; + bottom: calc(88px + var(--safe-bottom)); + width: auto; + max-width: none; + max-height: min(52vh, 420px); + } +} +@media (max-width: 600px) { + .picker-grid.sticker-grid { + grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); + } +} diff --git a/test/chat.test.js b/test/chat.test.js new file mode 100644 index 0000000..ef33424 --- /dev/null +++ b/test/chat.test.js @@ -0,0 +1,1063 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { EventEmitter } = require('node:events'); +const { once } = require('node:events'); + +process.env.STEAM_CHAT_DISABLE_AUTOSTART = '1'; + +const { + CHAT_LOG_FILE, + IMAGE_CACHE_DIR, + STICKER_CACHE_DIR, + buildConversationPreview, + buildImageCachePaths, + buildStickerCachePath, + buildSteamStickerCandidateUrls, + createChatService, + extractEmoticonNames, + extractImageUrls, + extractOpenGraphEmbeds, + getClientIp, + guessImageContentType, + isLanIp, + normalizeAuthConfig, + normalizeChatConfig, + normalizeHistoryEntry, + normalizeIpAddress, + normalizeWsRequest, + parseBasicAuthHeader, + parseForwardedHeader, + requiresHttpAuth, +} = require('../chat'); + +class FakeWebSocketServer { + constructor(options) { + const { server, path } = options; + this.server = server; + this.path = path; + this.options = options; + this.clients = new Set(); + this.handlers = new Map(); + } + + on(event, handler) { + this.handlers.set(event, handler); + } +} + +const FakeWebSocket = { + OPEN: 1, + Server: FakeWebSocketServer, +}; + +class FakeSteamUser extends EventEmitter { + constructor() { + super(); + this.steamID = 'self-id'; + this.chat = new EventEmitter(); + } + + webLogOn() {} +} + +function createMockResponse() { + return { + statusCode: 200, + headers: {}, + body: '', + writeHead(statusCode, headers) { + this.statusCode = statusCode; + if (headers) { + Object.assign(this.headers, headers); + } + }, + setHeader(name, value) { + this.headers[name] = value; + }, + end(body) { + this.body = body; + }, + }; +} + +function createMockRequest(method, url, payload) { + const req = new EventEmitter(); + req.method = method; + req.url = url; + req.headers = {}; + req.socket = { + remoteAddress: '127.0.0.1', + }; + + process.nextTick(() => { + if (payload !== undefined) { + req.emit('data', Buffer.from(payload)); + } + req.emit('end'); + }); + + return req; +} + +function createBroadcastClient() { + return { + readyState: FakeWebSocket.OPEN, + messages: [], + send(payload) { + this.messages.push(JSON.parse(payload)); + }, + }; +} + +function createService(overrides = {}) { + const logger = { + info() {}, + warn() {}, + error() {}, + }; + + const fsCalls = []; + let logContent = overrides.logContent || ''; + const extraFiles = new Map(Object.entries(overrides.extraFiles || {})); + const fsModule = { + mkdir(_path, _options, cb) { + cb(null); + }, + appendFile(path, data, cb) { + fsCalls.push({ path, data }); + if (path === CHAT_LOG_FILE) { + logContent += data; + } + cb(null); + }, + readFile(path, encoding, cb) { + if (typeof encoding === 'function') { + cb = encoding; + encoding = undefined; + } + + if (path === CHAT_LOG_FILE) { + if (encoding) { + assert.equal(encoding, 'utf8'); + } + cb(null, logContent); + return; + } + + if (extraFiles.has(path)) { + cb(null, extraFiles.get(path)); + return; + } + + const err = new Error('not found'); + err.code = 'ENOENT'; + cb(err); + }, + writeFile(path, data, cb) { + extraFiles.set(path, data); + cb(null); + }, + }; + + const server = { + listenArgs: null, + listen(port, host, cb) { + this.listenArgs = { port, host }; + cb(null); + }, + }; + + const httpModule = { + createServer(handler) { + server.handler = handler; + return server; + }, + }; + + const steamUser = new FakeSteamUser(); + steamUser.chat.sendFriendMessage = (uid, msg, cb) => { + cb(null, { + server_timestamp: new Date('2024-01-02T03:04:05.678Z'), + modified_message: msg, + ordinal: 42, + }); + }; + + const steamCommunity = { + sendImageToUser(uid, imageBuffer, cb) { + cb(null, `https://image/${uid}/${imageBuffer.length}`); + }, + }; + + const client = { + steamUser, + steamCommunity, + steamLoginPromise: Promise.resolve(), + steamWebLoginPromise: Promise.resolve(), + async getUserInfo(steamID) { + return { + player_name: steamID === 'self-id' ? 'Self User' : 'Friend User', + }; + }, + }; + + const service = createChatService({ + useDefaultDeps: false, + rawChatConfig: { + enabled: true, + host: '127.0.0.1', + port: 4000, + wsPath: '/chat', + }, + client, + logger, + steamUser, + steamCommunity, + fsModule, + httpModule, + onceFn: once, + axiosInstance: { + get: async (url) => ({ data: Buffer.from(String(url).includes('/sticker/') ? 'sticker-image' : 'image-by-url') }), + }, + WebSocketImpl: FakeWebSocket, + dateToString: () => 'formatted-date', + ...overrides, + }); + + return { + service, + client, + steamUser, + steamCommunity, + server, + fsCalls, + }; +} + +test('normalizeChatConfig supports boolean and object configs', () => { + assert.deepEqual(normalizeChatConfig(true), { + enabled: true, + host: '0.0.0.0', + port: 3000, + wsPath: '/ws', + auth: { + username: '', + password: '', + realm: 'Steam Chat', + trustProxy: false, + }, + }); + + assert.deepEqual(normalizeChatConfig({ + enabled: false, + host: '127.0.0.1', + port: 8080, + wsPath: '/chat', + auth: { + username: 'alice', + password: 'secret', + trustProxy: true, + }, + }), { + enabled: false, + host: '127.0.0.1', + port: 8080, + wsPath: '/chat', + auth: { + username: 'alice', + password: 'secret', + realm: 'Steam Chat', + trustProxy: true, + }, + }); +}); + +test('auth and client ip helpers support proxy-aware LAN checks', () => { + assert.deepEqual(normalizeAuthConfig({ + username: 'alice', + password: 'secret', + trustProxy: true, + }), { + username: 'alice', + password: 'secret', + realm: 'Steam Chat', + trustProxy: true, + }); + + assert.equal(normalizeIpAddress('::ffff:192.168.1.10'), '192.168.1.10'); + assert.equal(normalizeIpAddress('[2001:db8::1]:443'), '2001:db8::1'); + assert.equal(parseForwardedHeader('for=192.168.1.20;proto=https, for=8.8.8.8'), '192.168.1.20'); + + assert.equal(isLanIp('192.168.1.20'), true); + assert.equal(isLanIp('172.20.1.9'), true); + assert.equal(isLanIp('8.8.8.8'), false); + assert.equal(isLanIp('fd00::1234'), true); + + const proxiedReq = { + headers: { + 'x-forwarded-for': '8.8.8.8, 192.168.1.20', + authorization: `Basic ${Buffer.from('alice:secret').toString('base64')}`, + }, + socket: { + remoteAddress: '127.0.0.1', + }, + }; + + assert.equal(getClientIp(proxiedReq, true), '8.8.8.8'); + assert.deepEqual(parseBasicAuthHeader(proxiedReq.headers.authorization), { + username: 'alice', + password: 'secret', + }); + assert.equal(requiresHttpAuth(proxiedReq, { + auth: { + username: 'alice', + password: 'secret', + trustProxy: true, + }, + }), true); +}); + +test('normalizeWsRequest maps legacy and new websocket message types', () => { + assert.deepEqual(normalizeWsRequest({ + type: 'msg', + requestId: '1', + id: 'friend', + msg: 'hello', + }), { + action: 'send_message', + requestId: '1', + id: 'friend', + msg: 'hello', + }); + + assert.deepEqual(normalizeWsRequest({ + type: 'send_image', + requestId: '2', + id: 'friend', + url: 'https://example.com/a.png', + }), { + action: 'send_image', + requestId: '2', + id: 'friend', + img: undefined, + url: 'https://example.com/a.png', + }); + + assert.deepEqual(normalizeWsRequest({ + type: 'get_history', + requestId: '3', + id: 'friend', + limit: 50, + }), { + action: 'get_history', + requestId: '3', + id: 'friend', + limit: 50, + }); + + assert.deepEqual(normalizeWsRequest({ + type: 'get_conversations', + requestId: '4', + limit: 20, + }), { + action: 'get_conversations', + requestId: '4', + limit: 20, + }); +}); + +test('normalizeHistoryEntry fills defaults for old log format', () => { + assert.deepEqual(normalizeHistoryEntry({ + date: '2026-03-20 00:00:00.000', + echo: false, + id: 'friend', + name: 'Friend', + message: 'hello', + ordinal: 1, + }), { + type: 'message', + date: '2026-03-20 00:00:00.000', + echo: false, + id: 'friend', + name: 'Friend', + message: 'hello', + imageUrl: null, + ordinal: 1, + sentAt: null, + }); +}); + +test('extractEmoticonNames parses steam emoticon syntax', () => { + assert.deepEqual( + extractEmoticonNames('hi :steamhappy: [emoticon name="cozy"][/emoticon]').sort(), + ['cozy', 'steamhappy'], + ); + + assert.equal(buildConversationPreview({ + type: 'message', + message: ':steamhappy: :cozy:', + }), '[表情] steamhappy cozy'); +}); + +test('extractEmoticonNames parses [emoticon]name[/emoticon] format', () => { + assert.deepEqual( + extractEmoticonNames('[emoticon]angrylolo[/emoticon]'), + ['angrylolo'], + ); + + assert.deepEqual( + extractEmoticonNames('[emoticon]angrylolo[/emoticon] [emoticon name="cozy"][/emoticon] :steamhappy:').sort(), + ['angrylolo', 'cozy', 'steamhappy'], + ); + + assert.equal(buildConversationPreview({ + type: 'message', + message: '[emoticon]angrylolo[/emoticon]', + }), '[表情] angrylolo'); + + assert.equal(buildConversationPreview({ + type: 'message', + message: '[emoticon]angrylolo[/emoticon][emoticon]steamhappy[/emoticon]', + }), '[表情] angrylolo steamhappy'); +}); + +test('extractImageUrls parses bbcode img, html img and raw image urls', () => { + assert.deepEqual( + extractImageUrls('[img]https://a.com/1.png[/img] https://c.com/3.webp').sort(), + ['https://a.com/1.png', 'https://b.com/2.jpg', 'https://c.com/3.webp'], + ); + + assert.equal(buildConversationPreview({ + type: 'message', + message: '[img]https://a.com/1.png[/img]', + }), '[图片]'); +}); + +test('extractOpenGraphEmbeds parses steam og embed and uses title as preview', () => { + const embeds = extractOpenGraphEmbeds('[og url="https://www.bilibili.com/video/BV1n6A5zAEb7/" img="https://community.steamstatic.com/chat/image/share_image.png" title="伊朗:击中美军F-35战机_哔哩哔哩_bilibili"]https://www.bilibili.com/video/BV1n6A5zAEb7/[/og]'); + assert.deepEqual(embeds, [{ + url: 'https://www.bilibili.com/video/BV1n6A5zAEb7/', + img: 'https://community.steamstatic.com/chat/image/share_image.png', + title: '伊朗:击中美军F-35战机_哔哩哔哩_bilibili', + }]); + + assert.equal(buildConversationPreview({ + type: 'message', + message: '[og url="https://www.bilibili.com/video/BV1n6A5zAEb7/" img="https://community.steamstatic.com/chat/image/share_image.png" title="伊朗:击中美军F-35战机_哔哩哔哩_bilibili"]https://www.bilibili.com/video/BV1n6A5zAEb7/[/og]', + }), '伊朗:击中美军F-35战机_哔哩哔哩_bilibili'); + + assert.deepEqual( + extractOpenGraphEmbeds('[og url="https://www.bilibili.com/video/av116186884935795" img="https://community.steamstatic.com/chat/image/ht6wqt0rqW0CLNV0RzFC0nkBpimO7nDqFKftPDtI2M4oDWov4xFO5mWdNM5W1keOmLyp4sg5qbmKqxjRCAFx34WeM5-AxxkNc9h8Kelj5m1raqVeV8436wdU1iQIPxbL_A/share_image.png" title="1899年,吸铁石和生瓜蛋子的时代已然走到尽头_哔哩哔哩_bilibili"]https://www.bilibili.com/video/av116186884935795[/og]'), + [{ + url: 'https://www.bilibili.com/video/av116186884935795', + img: 'https://community.steamstatic.com/chat/image/ht6wqt0rqW0CLNV0RzFC0nkBpimO7nDqFKftPDtI2M4oDWov4xFO5mWdNM5W1keOmLyp4sg5qbmKqxjRCAFx34WeM5-AxxkNc9h8Kelj5m1raqVeV8436wdU1iQIPxbL_A/share_image.png', + title: '1899年,吸铁石和生瓜蛋子的时代已然走到尽头_哔哩哔哩_bilibili', + }], + ); +}); + +test('image cache helpers build stable paths and types', () => { + const paths = buildImageCachePaths('https://example.com/a.png?x=1'); + const normalizedCacheDir = IMAGE_CACHE_DIR.replace(/^\.\//, '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + assert.match(paths.dataPath, new RegExp(`^${normalizedCacheDir}/[a-f0-9]+\\.bin$`)); + assert.match(paths.metaPath, new RegExp(`^${normalizedCacheDir}/[a-f0-9]+\\.json$`)); + assert.equal(guessImageContentType('https://example.com/a.webp'), 'image/webp'); +}); + +test('buildSteamStickerCandidateUrls returns fallback sticker urls', () => { + assert.deepEqual(buildSteamStickerCandidateUrls('Sticker_MalteseCry'), [ + 'https://steamcommunity-a.akamaihd.net/economy/sticker/Sticker_MalteseCry', + 'https://steamcommunity-a.akamaihd.net/economy/stickerlarge/Sticker_MalteseCry', + 'https://steamcommunity.com/economy/sticker/Sticker_MalteseCry', + 'https://steamcommunity.com/economy/stickerlarge/Sticker_MalteseCry', + ]); + assert.equal( + buildStickerCachePath('Sticker_MalteseCry'), + `${STICKER_CACHE_DIR.replace(/^\.\//, '')}/Sticker_MalteseCry.bin`, + ); +}); + +test('handleHttp returns config for GET /api/config', async () => { + const { service } = createService(); + const req = createMockRequest('GET', '/api/config'); + const res = createMockResponse(); + + await service.handleHttp(req, res); + + assert.equal(res.statusCode, 200); + assert.equal(res.headers['Content-Type'], 'application/json; charset=utf-8'); + assert.deepEqual(JSON.parse(res.body), { wsPath: '/chat' }); +}); + +test('handleHttp requires basic auth for non-LAN requests when configured', async () => { + const { service } = createService({ + rawChatConfig: { + enabled: true, + host: '127.0.0.1', + port: 4000, + wsPath: '/chat', + auth: { + username: 'alice', + password: 'secret', + trustProxy: true, + }, + }, + }); + + const req = createMockRequest('GET', '/api/config'); + req.headers['x-forwarded-for'] = '8.8.8.8'; + const res = createMockResponse(); + + await service.handleHttp(req, res); + + assert.equal(res.statusCode, 401); + assert.match(res.headers['WWW-Authenticate'], /^Basic realm="Steam Chat"$/); + assert.deepEqual(JSON.parse(res.body), { error: 'Authentication Required' }); +}); + +test('handleHttp allows LAN requests and authenticated proxied requests', async () => { + const rawChatConfig = { + enabled: true, + host: '127.0.0.1', + port: 4000, + wsPath: '/chat', + auth: { + username: 'alice', + password: 'secret', + trustProxy: true, + }, + }; + + const { service } = createService({ rawChatConfig }); + + const lanReq = createMockRequest('GET', '/api/config'); + lanReq.headers['x-forwarded-for'] = '192.168.1.20'; + const lanRes = createMockResponse(); + + await service.handleHttp(lanReq, lanRes); + + assert.equal(lanRes.statusCode, 200); + + const authReq = createMockRequest('GET', '/api/config'); + authReq.headers['x-forwarded-for'] = '8.8.8.8'; + authReq.headers.authorization = `Basic ${Buffer.from('alice:secret').toString('base64')}`; + const authRes = createMockResponse(); + + await service.handleHttp(authReq, authRes); + + assert.equal(authRes.statusCode, 200); + assert.deepEqual(JSON.parse(authRes.body), { wsPath: '/chat' }); +}); + +test('handleSendMessageRequest broadcasts messages and deduplicates echoed messages', async () => { + const { service, fsCalls } = createService(); + const wsClient = createBroadcastClient(); + service.wss.clients.add(wsClient); + + const data = await service.handleSendMessageRequest({ + id: 'friend-id', + msg: 'hello', + }); + + assert.equal(data.echo, true); + assert.equal(data.name, 'Self User'); + assert.equal(wsClient.messages.length, 1); + assert.deepEqual(wsClient.messages[0], { + type: 'message', + data, + }); + assert.equal(fsCalls.length, 1); + + await service.broadcastSteamMessage({ + server_timestamp: new Date('2024-01-02T03:04:05.678Z'), + steamid_friend: 'friend-id', + message: 'hello', + ordinal: 42, + }, true, { dedupe: true }); + + assert.equal(wsClient.messages.length, 1); +}); + +test('sendImageToUser retries after refreshing web session', async () => { + const expectedBuffer = Buffer.from('fake-image'); + const steamUser = new FakeSteamUser(); + let uploadAttempts = 0; + let webLogOnCalled = 0; + + steamUser.webLogOn = () => { + webLogOnCalled += 1; + setImmediate(() => { + steamUser.emit('webSession', 'session-id', []); + }); + }; + + const service = createService({ + steamUser, + steamCommunity: { + sendImageToUser(_uid, imageBuffer, cb) { + uploadAttempts += 1; + assert.deepEqual(imageBuffer, expectedBuffer); + if (uploadAttempts === 1) { + cb(new Error('expired session')); + return; + } + cb(null, 'https://image/friend-id/retried'); + }, + }, + client: { + steamUser, + steamCommunity: null, + steamLoginPromise: Promise.resolve(), + steamWebLoginPromise: Promise.resolve(), + async getUserInfo(steamID) { + return { + player_name: steamID === 'self-id' ? 'Self User' : 'Friend User', + }; + }, + }, + }).service; + + const imageUrl = await service.sendImageToUser('friend-id', expectedBuffer.toString('base64')); + + assert.equal(imageUrl, 'https://image/friend-id/retried'); + assert.equal(uploadAttempts, 2); + assert.equal(webLogOnCalled, 1); +}); + +test('sendImageToUser retries once for transient TLS error before refreshing web session', async () => { + const expectedBuffer = Buffer.from('fake-image'); + const steamUser = new FakeSteamUser(); + let uploadAttempts = 0; + let webLogOnCalled = 0; + + steamUser.webLogOn = () => { + webLogOnCalled += 1; + }; + + const service = createService({ + steamUser, + steamCommunity: { + sendImageToUser(_uid, imageBuffer, cb) { + uploadAttempts += 1; + assert.deepEqual(imageBuffer, expectedBuffer); + if (uploadAttempts === 1) { + const error = new Error('Client network socket disconnected before secure TLS connection was established'); + error.code = 'ECONNRESET'; + cb(error); + return; + } + cb(null, 'https://image/friend-id/retried'); + }, + }, + client: { + steamUser, + steamCommunity: null, + steamLoginPromise: Promise.resolve(), + steamWebLoginPromise: Promise.resolve(), + async getUserInfo(steamID) { + return { + player_name: steamID === 'self-id' ? 'Self User' : 'Friend User', + }; + }, + }, + }).service; + + const imageUrl = await service.sendImageToUser('friend-id', expectedBuffer.toString('base64')); + + assert.equal(imageUrl, 'https://image/friend-id/retried'); + assert.equal(uploadAttempts, 2); + assert.equal(webLogOnCalled, 0); +}); + +test('handleHttp returns JSON response for message endpoint', async () => { + const { service } = createService(); + const req = createMockRequest('POST', '/message', JSON.stringify({ + id: 'friend-id', + msg: 'hello via http', + })); + const res = createMockResponse(); + + await service.handleHttp(req, res); + + assert.equal(res.statusCode, 200); + assert.equal(res.headers['Content-Type'], 'application/json; charset=utf-8'); + assert.deepEqual(JSON.parse(res.body), { + type: 'message', + date: 'formatted-date', + echo: true, + id: 'friend-id', + name: 'Self User', + message: 'hello via http', + ordinal: 42, + imageUrl: null, + sentAt: null, + }); +}); + +test('handleWsCommand responds to ping requests', async () => { + const { service } = createService(); + const ws = createBroadcastClient(); + + await service.handleWsCommand(ws, { + type: 'ping', + requestId: 'ping-1', + }); + + assert.equal(ws.messages.length, 1); + assert.equal(ws.messages[0].type, 'pong'); + assert.equal(ws.messages[0].requestId, 'ping-1'); + assert.ok(ws.messages[0].data.now); +}); + +test('websocket verifyClient requires auth for non-LAN requests', async () => { + const { service } = createService({ + rawChatConfig: { + enabled: true, + host: '127.0.0.1', + port: 4000, + wsPath: '/chat', + auth: { + username: 'alice', + password: 'secret', + trustProxy: true, + }, + }, + }); + + const verifyClient = service.wss.options.verifyClient; + + const denied = await new Promise((resolve) => { + verifyClient({ + req: { + headers: { + 'x-forwarded-for': '8.8.8.8', + }, + socket: { + remoteAddress: '127.0.0.1', + }, + }, + }, (...args) => resolve(args)); + }); + + assert.deepEqual(denied, [ + false, + 401, + 'Authentication Required', + { + 'WWW-Authenticate': 'Basic realm="Steam Chat"', + }, + ]); + + const allowed = await new Promise((resolve) => { + verifyClient({ + req: { + headers: { + 'x-forwarded-for': '8.8.8.8', + authorization: `Basic ${Buffer.from('alice:secret').toString('base64')}`, + }, + socket: { + remoteAddress: '127.0.0.1', + }, + }, + }, (...args) => resolve(args)); + }); + + assert.deepEqual(allowed, [true]); +}); + +test('handleHttp serves homepage for GET /', async () => { + const { service } = createService(); + const req = createMockRequest('GET', '/'); + const res = createMockResponse(); + + await service.handleHttp(req, res); + + // serveStaticFile reads the real public/index.html via fs.readFile (async callback), + // so we need to wait for the callback to complete + await new Promise((resolve) => setTimeout(resolve, 50)); + + assert.equal(res.statusCode, 200); + assert.equal(res.headers['Content-Type'], 'text/html; charset=utf-8'); + const body = typeof res.body === 'string' ? res.body : res.body.toString(); + assert.match(body, /Steam Chat/); +}); + +test('handleHttp proxies sticker and caches it locally', async () => { + const { service } = createService(); + const req = createMockRequest('GET', '/proxy/sticker/Sticker_MalteseCry'); + const res = createMockResponse(); + + await service.handleHttp(req, res); + + assert.equal(res.statusCode, 200); + assert.equal(res.headers['Content-Type'], 'image/png'); + assert.deepEqual(res.body, Buffer.from('sticker-image')); + + const cached = await service.fetchStickerBuffer('Sticker_MalteseCry'); + assert.deepEqual(cached, Buffer.from('sticker-image')); +}); + +test('fetchStickerBuffer coalesces concurrent requests for the same sticker', async () => { + let axiosCalls = 0; + let releaseFetch; + const fetchGate = new Promise((resolve) => { + releaseFetch = resolve; + }); + + const { service } = createService({ + axiosInstance: { + get: async (url) => { + axiosCalls += 1; + await fetchGate; + return { + data: Buffer.from('shared-sticker'), + headers: { + 'content-type': 'image/png', + }, + }; + }, + }, + }); + + const firstFetch = service.fetchStickerBuffer('Sticker_MalteseCry'); + const secondFetch = service.fetchStickerBuffer('Sticker_MalteseCry'); + + releaseFetch(); + + const [firstSticker, secondSticker] = await Promise.all([firstFetch, secondFetch]); + + assert.equal(axiosCalls, 1); + assert.deepEqual(firstSticker, Buffer.from('shared-sticker')); + assert.deepEqual(secondSticker, firstSticker); + + const cached = await service.fetchStickerBuffer('Sticker_MalteseCry'); + assert.equal(axiosCalls, 1); + assert.deepEqual(cached, firstSticker); +}); + +test('handleHttp proxies remote image and caches it locally', async () => { + const { service } = createService(); + const req = createMockRequest('GET', '/proxy/image?url=' + encodeURIComponent('https://example.com/a.png')); + const res = createMockResponse(); + + await service.handleHttp(req, res); + + assert.equal(res.statusCode, 200); + assert.equal(res.headers['Content-Type'], 'image/png'); + assert.deepEqual(res.body, Buffer.from('image-by-url')); + + const cached = await service.fetchCachedImage('https://example.com/a.png'); + assert.equal(cached.contentType, 'image/png'); + assert.deepEqual(cached.buffer, Buffer.from('image-by-url')); +}); + +test('fetchCachedImage coalesces concurrent requests for the same image', async () => { + let axiosCalls = 0; + let releaseFetch; + const fetchGate = new Promise((resolve) => { + releaseFetch = resolve; + }); + + const { service } = createService({ + axiosInstance: { + get: async (url) => { + axiosCalls += 1; + await fetchGate; + return { + data: Buffer.from('shared-image'), + headers: { + 'content-type': 'image/png', + }, + }; + }, + }, + }); + + const firstFetch = service.fetchCachedImage('https://example.com/shared.png'); + const secondFetch = service.fetchCachedImage('https://example.com/shared.png'); + + releaseFetch(); + + const [firstImage, secondImage] = await Promise.all([firstFetch, secondFetch]); + + assert.equal(axiosCalls, 1); + assert.deepEqual(firstImage, { + buffer: Buffer.from('shared-image'), + contentType: 'image/png', + }); + assert.deepEqual(secondImage, firstImage); + + const cached = await service.fetchCachedImage('https://example.com/shared.png'); + assert.equal(axiosCalls, 1); + assert.deepEqual(cached, firstImage); +}); + +test('readChatHistory filters by steam id and limits results', async () => { + const { service } = createService({ + logContent: [ + JSON.stringify({ date: '1', echo: false, id: 'a', name: 'A', message: 'x', ordinal: 1 }), + JSON.stringify({ type: 'image', date: '2', echo: true, id: 'b', name: 'Self', imageUrl: 'https://img/1' }), + JSON.stringify({ date: '3', echo: false, id: 'a', name: 'A', message: 'y', ordinal: 2 }), + ].join('\n') + '\n', + }); + + const items = await service.readChatHistory({ id: 'a', limit: 1 }); + assert.deepEqual(items, [{ + type: 'message', + date: '3', + echo: false, + id: 'a', + name: 'A', + message: 'y', + imageUrl: null, + ordinal: 2, + sentAt: null, + }]); +}); + +test('handleWsCommand returns history from local logs', async () => { + const { service } = createService({ + logContent: [ + JSON.stringify({ date: '2026-03-20 10:00:00.000', echo: false, id: 'friend-id', name: 'Friend', message: 'hello', ordinal: 1 }), + JSON.stringify({ type: 'image', date: '2026-03-20 10:01:00.000', echo: true, id: 'friend-id', name: 'Self User', imageUrl: 'https://image/friend-id/1' }), + ].join('\n') + '\n', + }); + const ws = createBroadcastClient(); + + await service.handleWsCommand(ws, { + type: 'get_history', + requestId: 'history-1', + id: 'friend-id', + limit: 10, + }); + + assert.equal(ws.messages.length, 1); + assert.deepEqual(ws.messages[0], { + type: 'history', + requestId: 'history-1', + data: { + items: [ + { + type: 'message', + date: '2026-03-20 10:00:00.000', + echo: false, + id: 'friend-id', + name: 'Friend', + message: 'hello', + imageUrl: null, + ordinal: 1, + sentAt: null, + }, + { + type: 'image', + date: '2026-03-20 10:01:00.000', + echo: true, + id: 'friend-id', + name: 'Self User', + message: '', + imageUrl: 'https://image/friend-id/1', + ordinal: null, + sentAt: null, + }, + ], + }, + }); +}); + +test('handleSendImageRequest appends image log and broadcasts image payload', async () => { + const { service, fsCalls } = createService(); + const wsClient = createBroadcastClient(); + service.wss.clients.add(wsClient); + + const data = await service.handleSendImageRequest({ + id: 'friend-id', + img: Buffer.from('fake-image').toString('base64'), + }); + + assert.equal(data.type, 'image'); + assert.equal(data.id, 'friend-id'); + assert.equal(data.imageUrl, 'https://image/friend-id/10'); + assert.equal(wsClient.messages.length, 1); + assert.deepEqual(wsClient.messages[0], { + type: 'image', + data, + }); + assert.equal(fsCalls.length, 1); + assert.match(fsCalls[0].data, /"type":"image"/); +}); + +test('readConversationSummaries groups recent conversations', async () => { + const { service } = createService({ + logContent: [ + JSON.stringify({ date: '2026-03-20 09:00:00.000', echo: false, id: 'a', name: 'Alice', message: '早', ordinal: 1 }), + JSON.stringify({ date: '2026-03-20 09:10:00.000', echo: true, id: 'b', name: 'Self User', message: 'https://example.com/a.png', ordinal: 1 }), + JSON.stringify({ date: '2026-03-20 09:20:00.000', echo: false, id: 'a', name: 'Alice', message: '[sticker type="Sticker_MalteseCry" limit="0"][/sticker]', ordinal: 2 }), + ].join('\n') + '\n', + }); + + const items = await service.readConversationSummaries({ limit: 10 }); + assert.deepEqual(items, [ + { + id: 'a', + name: 'Alice', + updatedAt: '2026-03-20 09:20:00.000', + preview: '[贴纸] MalteseCry', + lastType: 'message', + lastEcho: false, + messageCount: 2, + }, + { + id: 'b', + name: 'Self User', + updatedAt: '2026-03-20 09:10:00.000', + preview: '[图片]', + lastType: 'message', + lastEcho: true, + messageCount: 1, + }, + ]); +}); + +test('handleWsCommand returns conversation summaries', async () => { + const { service } = createService({ + logContent: [ + JSON.stringify({ date: '2026-03-20 11:00:00.000', echo: false, id: 'friend-1', name: 'Alice', message: 'hello', ordinal: 1 }), + JSON.stringify({ type: 'image', date: '2026-03-20 11:05:00.000', echo: true, id: 'friend-2', name: 'Self User', imageUrl: 'https://image/friend-2/1' }), + ].join('\n') + '\n', + }); + const ws = createBroadcastClient(); + + await service.handleWsCommand(ws, { + type: 'get_conversations', + requestId: 'conv-1', + limit: 20, + }); + + assert.equal(ws.messages.length, 1); + assert.deepEqual(ws.messages[0], { + type: 'conversations', + requestId: 'conv-1', + data: { + items: [ + { + id: 'friend-2', + name: 'Self User', + updatedAt: '2026-03-20 11:05:00.000', + preview: '[图片]', + lastType: 'image', + lastEcho: true, + messageCount: 1, + }, + { + id: 'friend-1', + name: 'Alice', + updatedAt: '2026-03-20 11:00:00.000', + preview: 'hello', + lastType: 'message', + lastEcho: false, + messageCount: 1, + }, + ], + }, + }); +});