refactor: 用 zod parse 环境变量

This commit is contained in:
Clansty 2024-01-15 17:58:48 +08:00
parent df9fd35a32
commit 6b80afca58
No known key found for this signature in database
GPG Key ID: 3A6BE8BAF2EDE134
14 changed files with 142 additions and 76 deletions

View File

@ -48,7 +48,8 @@
"telegram": "^2.19.10", "telegram": "^2.19.10",
"tmp-promise": "^3.0.3", "tmp-promise": "^3.0.3",
"undici": "^6.3.0", "undici": "^6.3.0",
"zincsearch-node": "^2.1.0" "zincsearch-node": "^2.1.0",
"zod": "^3.22.4"
}, },
"engines": { "engines": {
"node": "^14.13.1 || >=16.0.0" "node": "^14.13.1 || >=16.0.0"

View File

@ -80,6 +80,9 @@ dependencies:
zincsearch-node: zincsearch-node:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.1(undici@6.3.0) version: 2.1.1(undici@6.3.0)
zod:
specifier: ^3.22.4
version: 3.22.4
devDependencies: devDependencies:
'@types/cli-progress': '@types/cli-progress':
@ -3721,6 +3724,10 @@ packages:
undici: 6.3.0 undici: 6.3.0
dev: false 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': '@github.com/Clansty/quote-api/archive/37a0e48a434b94bb04c04c7d86d9f0d2295df869.tar.gz':
resolution: {tarball: https://github.com/Clansty/quote-api/archive/37a0e48a434b94bb04c04c7d86d9f0d2295df869.tar.gz} resolution: {tarball: https://github.com/Clansty/quote-api/archive/37a0e48a434b94bb04c04c7d86d9f0d2295df869.tar.gz}
name: quote-api name: quote-api

View File

@ -20,8 +20,9 @@ import { Converter, Image, rand2uuid } from 'icqq/lib/message';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { escapeXml, gzip, timestamp } from 'icqq/lib/common'; import { escapeXml, gzip, timestamp } from 'icqq/lib/common';
import { pb } from 'icqq/lib/core'; 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<boolean | void> type MessageHandler = (event: PrivateMessageEvent | GroupMessageEvent) => Promise<boolean | void>
@ -123,10 +124,10 @@ export default class OicqClient extends Client {
platform: params.platform, platform: params.platform,
data_dir: dataPath(params.uin.toString()), data_dir: dataPath(params.uin.toString()),
log_level: LOG_LEVEL, log_level: LOG_LEVEL,
ffmpeg_path: process.env.FFMPEG_PATH, ffmpeg_path: env.FFMPEG_PATH,
ffprobe_path: process.env.FFPROBE_PATH, ffprobe_path: env.FFPROBE_PATH,
sign_api_addr: params.signApi || process.env.SIGN_API, sign_api_addr: params.signApi || env.SIGN_API,
ver: params.signVer || process.env.SIGN_VER, ver: params.signVer || env.SIGN_VER,
}, params.signDockerId); }, params.signDockerId);
client.on('system.login.device', loginDeviceHandler); client.on('system.login.device', loginDeviceHandler);
client.on('system.login.slider', loginSliderHandler); client.on('system.login.slider', loginSliderHandler);

View File

@ -15,6 +15,7 @@ import { BigInteger } from 'big-integer';
import { EditMessageParams, IterMessagesParams } from 'telegram/client/messages'; import { EditMessageParams, IterMessagesParams } from 'telegram/client/messages';
import { PromisedNetSockets, PromisedWebSockets } from 'telegram/extensions'; import { PromisedNetSockets, PromisedWebSockets } from 'telegram/extensions';
import { ConnectionTCPFull, ConnectionTCPObfuscated } from 'telegram/network'; import { ConnectionTCPFull, ConnectionTCPObfuscated } from 'telegram/network';
import env from '../models/env';
type MessageHandler = (message: Api.Message) => Promise<boolean | void>; type MessageHandler = (message: Api.Message) => Promise<boolean | void>;
type ServiceMessageHandler = (message: Api.MessageService) => Promise<boolean | void>; type ServiceMessageHandler = (message: Api.MessageService) => Promise<boolean | void>;
@ -41,24 +42,24 @@ export default class Telegram {
private constructor(appName: string, sessionId?: number) { private constructor(appName: string, sessionId?: number) {
this.client = new TelegramClient( this.client = new TelegramClient(
new TelegramSession(sessionId), new TelegramSession(sessionId),
parseInt(process.env.TG_API_ID), env.TG_API_ID,
process.env.TG_API_HASH, env.TG_API_HASH,
{ {
connectionRetries: 20, connectionRetries: 20,
langCode: 'zh', langCode: 'zh',
deviceModel: `${appName} On ${os.hostname()}`, deviceModel: `${appName} On ${os.hostname()}`,
appVersion: 'rainbowcat', appVersion: 'rainbowcat',
useIPV6: !!process.env.IPV6, useIPV6: !!env.IPV6,
proxy: process.env.PROXY_IP ? { proxy: env.PROXY_IP ? {
socksType: 5, socksType: 5,
ip: process.env.PROXY_IP, ip: env.PROXY_IP,
port: parseInt(process.env.PROXY_PORT), port: env.PROXY_PORT,
...(process.env.PROXY_USERNAME && { username: process.env.PROXY_USERNAME }), username: env.PROXY_USERNAME,
...(process.env.PROXY_PASSWORD && { password: process.env.PROXY_PASSWORD }), password: env.PROXY_PASSWORD,
} : undefined, } : undefined,
autoReconnect: true, autoReconnect: true,
networkSocket: process.env.TG_CONNECTION === 'websocket' ? PromisedWebSockets : PromisedNetSockets, networkSocket: env.TG_CONNECTION === 'websocket' ? PromisedWebSockets : PromisedNetSockets,
connection: process.env.TG_CONNECTION === 'websocket' ? ConnectionTCPObfuscated : ConnectionTCPFull, connection: env.TG_CONNECTION === 'websocket' ? ConnectionTCPObfuscated : ConnectionTCPFull,
}, },
); );
// this.client.logger.setLevel(LogLevel.WARN); // this.client.logger.setLevel(LogLevel.WARN);

View File

@ -12,6 +12,7 @@ import BigInteger from 'big-integer';
import { getAvatarUrl } from '../utils/urls'; import { getAvatarUrl } from '../utils/urls';
import convert from '../helpers/convert'; import convert from '../helpers/convert';
import { Pair } from '../models/Pair'; import { Pair } from '../models/Pair';
import env from '../models/env';
export default class { export default class {
private readonly log: Logger; private readonly log: Logger;
@ -280,7 +281,7 @@ export default class {
throw new Error('不支持的消息类型'); throw new Error('不支持的消息类型');
} }
const res = await quotly({ const res = await quotly({
botToken: process.env.TG_BOT_TOKEN, botToken: env.TG_BOT_TOKEN,
type, type,
format, format,
backgroundColor, backgroundColor,

View File

@ -9,6 +9,7 @@ import { WorkMode } from '../types/definitions';
import OicqClient from '../client/OicqClient'; import OicqClient from '../client/OicqClient';
import { md5Hex } from '../utils/hashing'; import { md5Hex } from '../utils/hashing';
import Instance from '../models/Instance'; import Instance from '../models/Instance';
import env from '../models/env';
export default class SetupController { export default class SetupController {
private readonly setupService: SetupService; private readonly setupService: SetupService;
@ -106,7 +107,7 @@ export default class SetupController {
let signApi: string; let signApi: string;
if (!process.env.SIGN_API) { if (!env.SIGN_API) {
signApi = await this.setupService.waitForOwnerInput('请输入签名服务器地址', [ signApi = await this.setupService.waitForOwnerInput('请输入签名服务器地址', [
[Button.text('不需要签名服务器', true, true)], [Button.text('不需要签名服务器', true, true)],
]); ]);
@ -115,7 +116,7 @@ export default class SetupController {
let signVer: string; let signVer: string;
if (signApi && !process.env.SIGN_VER) { if (signApi && !env.SIGN_VER) {
signVer = await this.setupService.waitForOwnerInput('请输入签名服务器版本', [ signVer = await this.setupService.waitForOwnerInput('请输入签名服务器版本', [
[Button.text('8.9.63', true, true), [Button.text('8.9.63', true, true),
Button.text('8.9.68', true, true)], Button.text('8.9.68', true, true)],

View File

@ -1,8 +1,9 @@
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import env from '../models/env';
export default function tgsToGif(tgsPath: string) { export default function tgsToGif(tgsPath: string) {
return new Promise(resolve => { 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'); resolve(tgsPath + '.gif');
}); });
}); });

View File

@ -19,6 +19,7 @@ import axios from 'axios';
import { CustomFile } from 'telegram/client/uploads'; import { CustomFile } from 'telegram/client/uploads';
import fsP from 'fs/promises'; import fsP from 'fs/promises';
import { file } from 'tmp-promise'; import { file } from 'tmp-promise';
import env from '../models/env';
export default class { export default class {
private readonly log: Logger; private readonly log: Logger;
@ -197,21 +198,26 @@ export default class {
} }
break; break;
case 'forward': case 'forward':
try { if (env.CRV_API) {
const messages = await this.pair.qq.getForwardMsg(result.resId); try {
const hash = md5Hex(result.resId); const messages = await this.pair.qq.getForwardMsg(result.resId);
text += `转发的消息记录 ${process.env.CRV_API}/?hash=${hash}`; const hash = md5Hex(result.resId);
// 传到 Cloudflare text += `转发的消息记录 ${env.CRV_API}/?hash=${hash}`;
axios.post(`${process.env.CRV_API}/add`, { // 传到 Cloudflare
auth: process.env.CRV_KEY, axios.post(`${env.CRV_API}/add`, {
key: hash, auth: env.CRV_KEY,
data: messages, key: hash,
}) data: messages,
.then(data => this.log.trace('上传消息记录到 Cloudflare', data.data)) })
.catch(e => this.log.error('上传消息记录到 Cloudflare 失败', e)); .then(data => this.log.trace('上传消息记录到 Cloudflare', data.data))
.catch(e => this.log.error('上传消息记录到 Cloudflare 失败', e));
}
catch (e) {
text += '[转发多条消息(无法获取)]';
}
} }
catch (e) { else {
text += '[转发多条消息(无法获取)]'; text += '[转发多条消息]';
} }
break; break;
} }
@ -255,11 +261,11 @@ export default class {
ext: 'tgs', ext: 'tgs',
mime: 'application/x-tgsticker', mime: 'application/x-tgsticker',
} : await fileTypeFromFile(filePath); } : await fileTypeFromFile(filePath);
if(!type){ if (!type) {
type = { type = {
ext: 'bin', ext: 'bin',
mime: 'application/octet-stream', mime: 'application/octet-stream',
} };
} }
let media: Api.TypeInputMedia; let media: Api.TypeInputMedia;
if (['.webp', '.tgs'].includes(path.extname(filePath))) { if (['.webp', '.tgs'].includes(path.extname(filePath))) {

View File

@ -1,7 +1,6 @@
import path from 'path'; import path from 'path';
import env from '../models/env';
const DATA_DIR = process.env.DATA_DIR || path.resolve('./data');
// Wrap of path.join, add base DATA_DIR // Wrap of path.join, add base DATA_DIR
export default (...paths: string[]) => export default (...paths: string[]) =>
path.join(DATA_DIR, ...paths); path.join(env.DATA_DIR, ...paths);

View File

@ -24,6 +24,7 @@ import StatusReportController from '../controllers/StatusReportController';
import HugController from '../controllers/HugController'; import HugController from '../controllers/HugController';
import QuotLyController from '../controllers/QuotLyController'; import QuotLyController from '../controllers/QuotLyController';
import MiraiSkipFilterController from '../controllers/MiraiSkipFilterController'; import MiraiSkipFilterController from '../controllers/MiraiSkipFilterController';
import env from './env';
export default class Instance { export default class Instance {
private _owner = 0; private _owner = 0;
@ -96,7 +97,7 @@ export default class Instance {
this.tgBot = await Telegram.connect(this._botSessionId); this.tgBot = await Telegram.connect(this._botSessionId);
} }
else { else {
const token = this.id === 0 ? process.env.TG_BOT_TOKEN : botToken; const token = this.id === 0 ? env.TG_BOT_TOKEN : botToken;
if (!token) { if (!token) {
throw new Error('botToken 未指定'); throw new Error('botToken 未指定');
} }

View File

@ -2,6 +2,7 @@ import { MemorySession } from 'telegram/sessions';
import db from './db'; import db from './db';
import { AuthKey } from 'telegram/crypto/AuthKey'; import { AuthKey } from 'telegram/crypto/AuthKey';
import { getLogger, Logger } from 'log4js'; import { getLogger, Logger } from 'log4js';
import env from './env';
const PASS = () => 0; const PASS = () => 0;
@ -19,19 +20,19 @@ export default class TelegramSession extends MemorySession {
async load() { async load() {
this.log.trace('load'); this.log.trace('load');
if (process.env.TG_INITIAL_DCID) { if (env.TG_INITIAL_DCID) {
this._dcId = Number(process.env.TG_INITIAL_DCID); this._dcId = env.TG_INITIAL_DCID;
} }
if (process.env.TG_INITIAL_SERVER) { if (env.TG_INITIAL_SERVER) {
this._serverAddress = process.env.TG_INITIAL_SERVER; this._serverAddress = env.TG_INITIAL_SERVER;
} }
if (!this._dbId) { if (!this._dbId) {
this.log.debug('Session 不存在,创建'); this.log.debug('Session 不存在,创建');
// 创建并返回 // 创建并返回
const newDbEntry = await db.session.create({ const newDbEntry = await db.session.create({
data: { data: {
dcId: process.env.TG_INITIAL_DCID ? Number(process.env.TG_INITIAL_DCID) : null, dcId: env.TG_INITIAL_DCID,
serverAddress: process.env.TG_INITIAL_SERVER, serverAddress: env.TG_INITIAL_SERVER,
}, },
}); });
this._dbId = newDbEntry.id; this._dbId = newDbEntry.id;

39
src/models/env.ts Normal file
View File

@ -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;

View File

@ -41,6 +41,7 @@ import random from '../utils/random';
import { escapeXml } from 'icqq/lib/common'; import { escapeXml } from 'icqq/lib/common';
import Docker from 'dockerode'; import Docker from 'dockerode';
import ReplyKeyboardHide = Api.ReplyKeyboardHide; import ReplyKeyboardHide = Api.ReplyKeyboardHide;
import env from '../models/env';
const NOT_CHAINABLE_ELEMENTS = ['flash', 'record', 'video', 'location', 'share', 'json', 'xml', 'poke']; 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 tgBot: Telegram,
private readonly oicq: OicqClient) { private readonly oicq: OicqClient) {
this.log = getLogger(`ForwardService - ${instance.id}`); this.log = getLogger(`ForwardService - ${instance.id}`);
if (process.env.ZINC_URL) { if (env.ZINC_URL) {
this.zincSearch = new ZincSearch({ this.zincSearch = new ZincSearch({
url: process.env.ZINC_URL, url: env.ZINC_URL,
user: process.env.ZINC_USERNAME, user: env.ZINC_USERNAME,
password: process.env.ZINC_PASSWORD, password: env.ZINC_PASSWORD,
}); });
} }
if (process.env.BAIDU_APP_ID) { if (env.BAIDU_APP_ID) {
this.speechClient = new AipSpeechClient( this.speechClient = new AipSpeechClient(
process.env.BAIDU_APP_ID, env.BAIDU_APP_ID,
process.env.BAIDU_API_KEY, env.BAIDU_API_KEY,
process.env.BAIDU_SECRET_KEY, env.BAIDU_SECRET_KEY,
); );
} }
if (oicq.signDockerId) { if (oicq.signDockerId) {
@ -110,22 +111,27 @@ export default class ForwardService {
} }
}; };
const useForward = async (resId: string) => { const useForward = async (resId: string) => {
try { if(env.CRV_API) {
const messages = await pair.qq.getForwardMsg(resId); try {
message = helper.generateForwardBrief(messages); const messages = await pair.qq.getForwardMsg(resId);
const hash = md5Hex(resId); message = helper.generateForwardBrief(messages);
buttons.push(Button.url('📃查看', `${process.env.CRV_API}/?hash=${hash}`)); const hash = md5Hex(resId);
// 传到 Cloudflare buttons.push(Button.url('📃查看', `${env.CRV_API}/?hash=${hash}`));
axios.post(`${process.env.CRV_API}/add`, { // 传到 Cloudflare
auth: process.env.CRV_KEY, axios.post(`${env.CRV_API}/add`, {
key: hash, auth: env.CRV_KEY,
data: messages, key: hash,
}) data: messages,
.then(data => this.log.trace('上传消息记录到 Cloudflare', data.data)) })
.catch(e => this.log.error('上传消息记录到 Cloudflare 失败', e)); .then(data => this.log.trace('上传消息记录到 Cloudflare', data.data))
.catch(e => this.log.error('上传消息记录到 Cloudflare 失败', e));
}
catch (e) {
message = '[<i>转发多条消息(无法获取)</i>]';
}
} }
catch (e) { else {
message = '[<i>转发多条消息(无法获取)</i>]'; message = '[<i>转发多条消息(未配置</i>]';
} }
}; };
for (const elem of event.message) { for (const elem of event.message) {
@ -541,7 +547,7 @@ export default class ForwardService {
} }
} }
brief += '[文件]'; brief += '[文件]';
if (process.env.DISABLE_FILE_UPLOAD_TIP) { if (env.DISABLE_FILE_UPLOAD_TIP) {
chain = []; chain = [];
} }
} }
@ -713,10 +719,10 @@ export default class ForwardService {
nick: string, nick: string,
}) { }) {
if (!this.zincSearch) return; 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', method: 'HEAD',
headers: { 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) { if (existsReq.status === 404) {

View File

@ -11,6 +11,7 @@ import db from '../models/db';
import { Friend, Group } from 'icqq'; import { Friend, Group } from 'icqq';
import { format } from 'date-and-time'; import { format } from 'date-and-time';
import ZincSearch from 'zincsearch-node'; import ZincSearch from 'zincsearch-node';
import env from '../models/env';
export default class InChatCommandsService { export default class InChatCommandsService {
private readonly log: Logger; private readonly log: Logger;
@ -20,11 +21,11 @@ export default class InChatCommandsService {
private readonly tgBot: Telegram, private readonly tgBot: Telegram,
private readonly oicq: OicqClient) { private readonly oicq: OicqClient) {
this.log = getLogger(`InChatCommandsService - ${instance.id}`); this.log = getLogger(`InChatCommandsService - ${instance.id}`);
if (process.env.ZINC_URL) { if (env.ZINC_URL) {
this.zincSearch = new ZincSearch({ this.zincSearch = new ZincSearch({
url: process.env.ZINC_URL, url: env.ZINC_URL,
user: process.env.ZINC_USERNAME, user: env.ZINC_USERNAME,
password: process.env.ZINC_PASSWORD, password: env.ZINC_PASSWORD,
}); });
} }
} }