From 6b80afca58caf023801c8bcb4f078241467e69ac Mon Sep 17 00:00:00 2001 From: Clansty Date: Mon, 15 Jan 2024 17:58:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=94=A8=20zod=20parse=20=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- pnpm-lock.yaml | 7 ++++ src/client/OicqClient.ts | 11 ++--- src/client/Telegram.ts | 21 +++++----- src/controllers/QuotLyController.ts | 3 +- src/controllers/SetupController.ts | 5 ++- src/encoding/tgsToGif.ts | 3 +- src/helpers/RecoverMessageHelper.ts | 38 ++++++++++-------- src/helpers/dataPath.ts | 5 +-- src/models/Instance.ts | 3 +- src/models/TelegramSession.ts | 13 +++--- src/models/env.ts | 39 ++++++++++++++++++ src/services/ForwardService.ts | 58 +++++++++++++++------------ src/services/InChatCommandsService.ts | 9 +++-- 14 files changed, 142 insertions(+), 76 deletions(-) create mode 100644 src/models/env.ts diff --git a/package.json b/package.json index c059235..8bb6509 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "telegram": "^2.19.10", "tmp-promise": "^3.0.3", "undici": "^6.3.0", - "zincsearch-node": "^2.1.0" + "zincsearch-node": "^2.1.0", + "zod": "^3.22.4" }, "engines": { "node": "^14.13.1 || >=16.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dda1057..5d99266 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ dependencies: zincsearch-node: specifier: ^2.1.0 version: 2.1.1(undici@6.3.0) + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@types/cli-progress': @@ -3721,6 +3724,10 @@ packages: undici: 6.3.0 dev: false + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false + '@github.com/Clansty/quote-api/archive/37a0e48a434b94bb04c04c7d86d9f0d2295df869.tar.gz': resolution: {tarball: https://github.com/Clansty/quote-api/archive/37a0e48a434b94bb04c04c7d86d9f0d2295df869.tar.gz} name: quote-api diff --git a/src/client/OicqClient.ts b/src/client/OicqClient.ts index 5cf2d27..7435dfc 100644 --- a/src/client/OicqClient.ts +++ b/src/client/OicqClient.ts @@ -20,8 +20,9 @@ import { Converter, Image, rand2uuid } from 'icqq/lib/message'; import { randomBytes } from 'crypto'; import { escapeXml, gzip, timestamp } from 'icqq/lib/common'; import { pb } from 'icqq/lib/core'; +import env from '../models/env'; -const LOG_LEVEL: LogLevel = process.env.LOG_LEVEL as LogLevel || 'warn'; +const LOG_LEVEL: LogLevel = env.OICQ_LOG_LEVEL; type MessageHandler = (event: PrivateMessageEvent | GroupMessageEvent) => Promise @@ -123,10 +124,10 @@ export default class OicqClient extends Client { platform: params.platform, data_dir: dataPath(params.uin.toString()), log_level: LOG_LEVEL, - ffmpeg_path: process.env.FFMPEG_PATH, - ffprobe_path: process.env.FFPROBE_PATH, - sign_api_addr: params.signApi || process.env.SIGN_API, - ver: params.signVer || process.env.SIGN_VER, + ffmpeg_path: env.FFMPEG_PATH, + ffprobe_path: env.FFPROBE_PATH, + sign_api_addr: params.signApi || env.SIGN_API, + ver: params.signVer || env.SIGN_VER, }, params.signDockerId); client.on('system.login.device', loginDeviceHandler); client.on('system.login.slider', loginSliderHandler); diff --git a/src/client/Telegram.ts b/src/client/Telegram.ts index 4ee81da..e37ba4b 100644 --- a/src/client/Telegram.ts +++ b/src/client/Telegram.ts @@ -15,6 +15,7 @@ import { BigInteger } from 'big-integer'; import { EditMessageParams, IterMessagesParams } from 'telegram/client/messages'; import { PromisedNetSockets, PromisedWebSockets } from 'telegram/extensions'; import { ConnectionTCPFull, ConnectionTCPObfuscated } from 'telegram/network'; +import env from '../models/env'; type MessageHandler = (message: Api.Message) => Promise; type ServiceMessageHandler = (message: Api.MessageService) => Promise; @@ -41,24 +42,24 @@ export default class Telegram { private constructor(appName: string, sessionId?: number) { this.client = new TelegramClient( new TelegramSession(sessionId), - parseInt(process.env.TG_API_ID), - process.env.TG_API_HASH, + env.TG_API_ID, + env.TG_API_HASH, { connectionRetries: 20, langCode: 'zh', deviceModel: `${appName} On ${os.hostname()}`, appVersion: 'rainbowcat', - useIPV6: !!process.env.IPV6, - proxy: process.env.PROXY_IP ? { + useIPV6: !!env.IPV6, + proxy: env.PROXY_IP ? { socksType: 5, - ip: process.env.PROXY_IP, - port: parseInt(process.env.PROXY_PORT), - ...(process.env.PROXY_USERNAME && { username: process.env.PROXY_USERNAME }), - ...(process.env.PROXY_PASSWORD && { password: process.env.PROXY_PASSWORD }), + ip: env.PROXY_IP, + port: env.PROXY_PORT, + username: env.PROXY_USERNAME, + password: env.PROXY_PASSWORD, } : undefined, autoReconnect: true, - networkSocket: process.env.TG_CONNECTION === 'websocket' ? PromisedWebSockets : PromisedNetSockets, - connection: process.env.TG_CONNECTION === 'websocket' ? ConnectionTCPObfuscated : ConnectionTCPFull, + networkSocket: env.TG_CONNECTION === 'websocket' ? PromisedWebSockets : PromisedNetSockets, + connection: env.TG_CONNECTION === 'websocket' ? ConnectionTCPObfuscated : ConnectionTCPFull, }, ); // this.client.logger.setLevel(LogLevel.WARN); diff --git a/src/controllers/QuotLyController.ts b/src/controllers/QuotLyController.ts index c332acf..cd371c0 100644 --- a/src/controllers/QuotLyController.ts +++ b/src/controllers/QuotLyController.ts @@ -12,6 +12,7 @@ import BigInteger from 'big-integer'; import { getAvatarUrl } from '../utils/urls'; import convert from '../helpers/convert'; import { Pair } from '../models/Pair'; +import env from '../models/env'; export default class { private readonly log: Logger; @@ -280,7 +281,7 @@ export default class { throw new Error('不支持的消息类型'); } const res = await quotly({ - botToken: process.env.TG_BOT_TOKEN, + botToken: env.TG_BOT_TOKEN, type, format, backgroundColor, diff --git a/src/controllers/SetupController.ts b/src/controllers/SetupController.ts index ad9e0c2..ebf942e 100644 --- a/src/controllers/SetupController.ts +++ b/src/controllers/SetupController.ts @@ -9,6 +9,7 @@ import { WorkMode } from '../types/definitions'; import OicqClient from '../client/OicqClient'; import { md5Hex } from '../utils/hashing'; import Instance from '../models/Instance'; +import env from '../models/env'; export default class SetupController { private readonly setupService: SetupService; @@ -106,7 +107,7 @@ export default class SetupController { let signApi: string; - if (!process.env.SIGN_API) { + if (!env.SIGN_API) { signApi = await this.setupService.waitForOwnerInput('请输入签名服务器地址', [ [Button.text('不需要签名服务器', true, true)], ]); @@ -115,7 +116,7 @@ export default class SetupController { let signVer: string; - if (signApi && !process.env.SIGN_VER) { + if (signApi && !env.SIGN_VER) { signVer = await this.setupService.waitForOwnerInput('请输入签名服务器版本', [ [Button.text('8.9.63', true, true), Button.text('8.9.68', true, true)], diff --git a/src/encoding/tgsToGif.ts b/src/encoding/tgsToGif.ts index 739e9e0..19f8225 100644 --- a/src/encoding/tgsToGif.ts +++ b/src/encoding/tgsToGif.ts @@ -1,8 +1,9 @@ import { spawn } from 'child_process'; +import env from '../models/env'; export default function tgsToGif(tgsPath: string) { return new Promise(resolve => { - spawn(process.env.TGS_TO_GIF || 'tgs_to_gif', [tgsPath]).on('exit', () => { + spawn(env.TGS_TO_GIF, [tgsPath]).on('exit', () => { resolve(tgsPath + '.gif'); }); }); diff --git a/src/helpers/RecoverMessageHelper.ts b/src/helpers/RecoverMessageHelper.ts index fe5f49a..b72ab85 100644 --- a/src/helpers/RecoverMessageHelper.ts +++ b/src/helpers/RecoverMessageHelper.ts @@ -19,6 +19,7 @@ import axios from 'axios'; import { CustomFile } from 'telegram/client/uploads'; import fsP from 'fs/promises'; import { file } from 'tmp-promise'; +import env from '../models/env'; export default class { private readonly log: Logger; @@ -197,21 +198,26 @@ export default class { } break; case 'forward': - try { - const messages = await this.pair.qq.getForwardMsg(result.resId); - const hash = md5Hex(result.resId); - text += `转发的消息记录 ${process.env.CRV_API}/?hash=${hash}`; - // 传到 Cloudflare - axios.post(`${process.env.CRV_API}/add`, { - auth: process.env.CRV_KEY, - key: hash, - data: messages, - }) - .then(data => this.log.trace('上传消息记录到 Cloudflare', data.data)) - .catch(e => this.log.error('上传消息记录到 Cloudflare 失败', e)); + if (env.CRV_API) { + try { + const messages = await this.pair.qq.getForwardMsg(result.resId); + const hash = md5Hex(result.resId); + text += `转发的消息记录 ${env.CRV_API}/?hash=${hash}`; + // 传到 Cloudflare + axios.post(`${env.CRV_API}/add`, { + auth: env.CRV_KEY, + key: hash, + data: messages, + }) + .then(data => this.log.trace('上传消息记录到 Cloudflare', data.data)) + .catch(e => this.log.error('上传消息记录到 Cloudflare 失败', e)); + } + catch (e) { + text += '[转发多条消息(无法获取)]'; + } } - catch (e) { - text += '[转发多条消息(无法获取)]'; + else { + text += '[转发多条消息]'; } break; } @@ -255,11 +261,11 @@ export default class { ext: 'tgs', mime: 'application/x-tgsticker', } : await fileTypeFromFile(filePath); - if(!type){ + if (!type) { type = { ext: 'bin', mime: 'application/octet-stream', - } + }; } let media: Api.TypeInputMedia; if (['.webp', '.tgs'].includes(path.extname(filePath))) { diff --git a/src/helpers/dataPath.ts b/src/helpers/dataPath.ts index 9fd5ec0..25573db 100644 --- a/src/helpers/dataPath.ts +++ b/src/helpers/dataPath.ts @@ -1,7 +1,6 @@ import path from 'path'; - -const DATA_DIR = process.env.DATA_DIR || path.resolve('./data'); +import env from '../models/env'; // Wrap of path.join, add base DATA_DIR export default (...paths: string[]) => - path.join(DATA_DIR, ...paths); + path.join(env.DATA_DIR, ...paths); diff --git a/src/models/Instance.ts b/src/models/Instance.ts index bdd8e0d..a8c51f9 100644 --- a/src/models/Instance.ts +++ b/src/models/Instance.ts @@ -24,6 +24,7 @@ import StatusReportController from '../controllers/StatusReportController'; import HugController from '../controllers/HugController'; import QuotLyController from '../controllers/QuotLyController'; import MiraiSkipFilterController from '../controllers/MiraiSkipFilterController'; +import env from './env'; export default class Instance { private _owner = 0; @@ -96,7 +97,7 @@ export default class Instance { this.tgBot = await Telegram.connect(this._botSessionId); } else { - const token = this.id === 0 ? process.env.TG_BOT_TOKEN : botToken; + const token = this.id === 0 ? env.TG_BOT_TOKEN : botToken; if (!token) { throw new Error('botToken 未指定'); } diff --git a/src/models/TelegramSession.ts b/src/models/TelegramSession.ts index 8ed78a3..89be198 100644 --- a/src/models/TelegramSession.ts +++ b/src/models/TelegramSession.ts @@ -2,6 +2,7 @@ import { MemorySession } from 'telegram/sessions'; import db from './db'; import { AuthKey } from 'telegram/crypto/AuthKey'; import { getLogger, Logger } from 'log4js'; +import env from './env'; const PASS = () => 0; @@ -19,19 +20,19 @@ export default class TelegramSession extends MemorySession { async load() { this.log.trace('load'); - if (process.env.TG_INITIAL_DCID) { - this._dcId = Number(process.env.TG_INITIAL_DCID); + if (env.TG_INITIAL_DCID) { + this._dcId = env.TG_INITIAL_DCID; } - if (process.env.TG_INITIAL_SERVER) { - this._serverAddress = process.env.TG_INITIAL_SERVER; + if (env.TG_INITIAL_SERVER) { + this._serverAddress = env.TG_INITIAL_SERVER; } if (!this._dbId) { this.log.debug('Session 不存在,创建'); // 创建并返回 const newDbEntry = await db.session.create({ data: { - dcId: process.env.TG_INITIAL_DCID ? Number(process.env.TG_INITIAL_DCID) : null, - serverAddress: process.env.TG_INITIAL_SERVER, + dcId: env.TG_INITIAL_DCID, + serverAddress: env.TG_INITIAL_SERVER, }, }); this._dbId = newDbEntry.id; diff --git a/src/models/env.ts b/src/models/env.ts new file mode 100644 index 0000000..05bd360 --- /dev/null +++ b/src/models/env.ts @@ -0,0 +1,39 @@ +import z from 'zod'; +import path from 'path'; + +const configParsed = z.object({ + DATA_DIR: z.string().default(path.resolve('./data')), + OICQ_LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'mark', 'off']).default('warn'), + FFMPEG_PATH: z.string().optional(), + FFPROBE_PATH: z.string().optional(), + SIGN_API: z.string().url().optional(), + SIGN_VER: z.string().optional(), + TG_API_ID: z.string().regex(/^\d+$/).transform(Number), + TG_API_HASH: z.string(), + TG_BOT_TOKEN: z.string(), + TG_CONNECTION: z.enum(['websocket', 'tcp']).default('tcp'), + TG_INITIAL_DCID: z.string().regex(/^\d+$/).transform(Number).optional(), + TG_INITIAL_SERVER: z.string().ip().optional(), + IPV6: z.string().transform((v) => ['true', '1', 'yes'].includes(v.toLowerCase())).default('false'), + PROXY_IP: z.string().ip().optional(), + PROXY_PORT: z.string().regex(/^\d+$/).transform(Number).optional(), + PROXY_USERNAME: z.string().optional(), + PROXY_PASSWORD: z.string().optional(), + TGS_TO_GIF: z.string().default('tgs_to_gif'), + CRV_API: z.string().url().optional(), + CRV_KEY: z.string().optional(), + ZINC_URL: z.string().url().optional(), + ZINC_USERNAME: z.string().optional(), + ZINC_PASSWORD: z.string().optional(), + BAIDU_APP_ID: z.string().optional(), + BAIDU_API_KEY: z.string().optional(), + BAIDU_SECRET_KEY: z.string().optional(), + DISABLE_FILE_UPLOAD_TIP: z.string().transform((v) => ['true', '1', 'yes'].includes(v.toLowerCase())).default('false'), +}).safeParse(process.env); + +if (!configParsed.success) { + console.error('环境变量解析错误:', (configParsed as any).error); + process.exit(1); +} + +export default configParsed.data; diff --git a/src/services/ForwardService.ts b/src/services/ForwardService.ts index 564315d..2ef7677 100644 --- a/src/services/ForwardService.ts +++ b/src/services/ForwardService.ts @@ -41,6 +41,7 @@ import random from '../utils/random'; import { escapeXml } from 'icqq/lib/common'; import Docker from 'dockerode'; import ReplyKeyboardHide = Api.ReplyKeyboardHide; +import env from '../models/env'; const NOT_CHAINABLE_ELEMENTS = ['flash', 'record', 'video', 'location', 'share', 'json', 'xml', 'poke']; @@ -55,18 +56,18 @@ export default class ForwardService { private readonly tgBot: Telegram, private readonly oicq: OicqClient) { this.log = getLogger(`ForwardService - ${instance.id}`); - if (process.env.ZINC_URL) { + if (env.ZINC_URL) { this.zincSearch = new ZincSearch({ - url: process.env.ZINC_URL, - user: process.env.ZINC_USERNAME, - password: process.env.ZINC_PASSWORD, + url: env.ZINC_URL, + user: env.ZINC_USERNAME, + password: env.ZINC_PASSWORD, }); } - if (process.env.BAIDU_APP_ID) { + if (env.BAIDU_APP_ID) { this.speechClient = new AipSpeechClient( - process.env.BAIDU_APP_ID, - process.env.BAIDU_API_KEY, - process.env.BAIDU_SECRET_KEY, + env.BAIDU_APP_ID, + env.BAIDU_API_KEY, + env.BAIDU_SECRET_KEY, ); } if (oicq.signDockerId) { @@ -110,22 +111,27 @@ export default class ForwardService { } }; const useForward = async (resId: string) => { - try { - const messages = await pair.qq.getForwardMsg(resId); - message = helper.generateForwardBrief(messages); - const hash = md5Hex(resId); - buttons.push(Button.url('📃查看', `${process.env.CRV_API}/?hash=${hash}`)); - // 传到 Cloudflare - axios.post(`${process.env.CRV_API}/add`, { - auth: process.env.CRV_KEY, - key: hash, - data: messages, - }) - .then(data => this.log.trace('上传消息记录到 Cloudflare', data.data)) - .catch(e => this.log.error('上传消息记录到 Cloudflare 失败', e)); + if(env.CRV_API) { + try { + const messages = await pair.qq.getForwardMsg(resId); + message = helper.generateForwardBrief(messages); + const hash = md5Hex(resId); + buttons.push(Button.url('📃查看', `${env.CRV_API}/?hash=${hash}`)); + // 传到 Cloudflare + axios.post(`${env.CRV_API}/add`, { + auth: env.CRV_KEY, + key: hash, + data: messages, + }) + .then(data => this.log.trace('上传消息记录到 Cloudflare', data.data)) + .catch(e => this.log.error('上传消息记录到 Cloudflare 失败', e)); + } + catch (e) { + message = '[转发多条消息(无法获取)]'; + } } - catch (e) { - message = '[转发多条消息(无法获取)]'; + else { + message = '[转发多条消息(未配置)]'; } }; for (const elem of event.message) { @@ -541,7 +547,7 @@ export default class ForwardService { } } brief += '[文件]'; - if (process.env.DISABLE_FILE_UPLOAD_TIP) { + if (env.DISABLE_FILE_UPLOAD_TIP) { chain = []; } } @@ -713,10 +719,10 @@ export default class ForwardService { nick: string, }) { if (!this.zincSearch) return; - const existsReq = await fetch(process.env.ZINC_URL + `/api/index/q2tg-${pairId}`, { + const existsReq = await fetch(env.ZINC_URL + `/api/index/q2tg-${pairId}`, { method: 'HEAD', headers: { - Authorization: 'Basic ' + Buffer.from(process.env.ZINC_USERNAME + ':' + process.env.ZINC_PASSWORD).toString('base64'), + Authorization: 'Basic ' + Buffer.from(env.ZINC_USERNAME + ':' + env.ZINC_PASSWORD).toString('base64'), }, }); if (existsReq.status === 404) { diff --git a/src/services/InChatCommandsService.ts b/src/services/InChatCommandsService.ts index 4c91cf1..2679c43 100644 --- a/src/services/InChatCommandsService.ts +++ b/src/services/InChatCommandsService.ts @@ -11,6 +11,7 @@ import db from '../models/db'; import { Friend, Group } from 'icqq'; import { format } from 'date-and-time'; import ZincSearch from 'zincsearch-node'; +import env from '../models/env'; export default class InChatCommandsService { private readonly log: Logger; @@ -20,11 +21,11 @@ export default class InChatCommandsService { private readonly tgBot: Telegram, private readonly oicq: OicqClient) { this.log = getLogger(`InChatCommandsService - ${instance.id}`); - if (process.env.ZINC_URL) { + if (env.ZINC_URL) { this.zincSearch = new ZincSearch({ - url: process.env.ZINC_URL, - user: process.env.ZINC_USERNAME, - password: process.env.ZINC_PASSWORD, + url: env.ZINC_URL, + user: env.ZINC_USERNAME, + password: env.ZINC_PASSWORD, }); } }