From a94eeab89b0a0c3432af2f9796d44e700084b93c Mon Sep 17 00:00:00 2001 From: Roy Li Date: Tue, 25 Feb 2020 21:29:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=B5=81=E9=87=8F=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/command/check.ts | 7 +- lib/command/generate.ts | 8 +- lib/command/subscriptions.ts | 113 ++++++++++++++++++ lib/command/upload.ts | 11 +- lib/index.ts | 13 +- lib/provider/ClashProvider.ts | 4 +- lib/provider/ShadowsocksSubscribeProvider.ts | 3 +- lib/provider/ShadowsocksrSubscribeProvider.ts | 12 +- lib/types.ts | 7 ++ lib/utils/index.ts | 20 ---- lib/utils/subscription.ts | 44 +++++++ package.json | 2 + yarn.lock | 10 ++ 13 files changed, 219 insertions(+), 35 deletions(-) create mode 100644 lib/command/subscriptions.ts create mode 100644 lib/utils/subscription.ts diff --git a/lib/command/check.ts b/lib/command/check.ts index f6041ed86..ac3dd9d6c 100644 --- a/lib/command/check.ts +++ b/lib/command/check.ts @@ -17,9 +17,12 @@ class CheckCommand extends Command { super(rawArgv); this.usage = '使用方法: surgio check [provider]'; this.options = { - config: { - alias: 'c', + c: { + alias: 'config', + demandOption: false, + describe: 'Surgio 配置文件', default: './surgio.conf.js', + type: 'string', }, }; } diff --git a/lib/command/generate.ts b/lib/command/generate.ts index c9587d9d4..4410341b6 100644 --- a/lib/command/generate.ts +++ b/lib/command/generate.ts @@ -12,13 +12,13 @@ class GenerateCommand extends Command { super(rawArgv); this.usage = '使用方法: surgio generate'; this.options = { - output: { + o: { type: 'string', - alias: 'o', + alias: 'output', description: '生成规则的目录', }, - config: { - alias: 'c', + c: { + alias: 'config', default: './surgio.conf.js', }, }; diff --git a/lib/command/subscriptions.ts b/lib/command/subscriptions.ts new file mode 100644 index 000000000..57f7a8001 --- /dev/null +++ b/lib/command/subscriptions.ts @@ -0,0 +1,113 @@ +// istanbul ignore file +import Command from 'common-bin'; +import { promises as fsp } from 'fs'; +import { basename, join } from 'path'; +import { createLogger } from '@surgio/logger'; +import BlackSSLProvider from '../provider/BlackSSLProvider'; +import ClashProvider from '../provider/ClashProvider'; +import CustomProvider from '../provider/CustomProvider'; +import ShadowsocksJsonSubscribeProvider from '../provider/ShadowsocksJsonSubscribeProvider'; +import ShadowsocksrSubscribeProvider from '../provider/ShadowsocksrSubscribeProvider'; +import ShadowsocksSubscribeProvider from '../provider/ShadowsocksSubscribeProvider'; +import V2rayNSubscribeProvider from '../provider/V2rayNSubscribeProvider'; + +import { CommandConfig } from '../types'; +import { + loadConfig +} from '../utils/config'; +import getProvider from '../utils/get-provider'; +import { errorHandler } from '../utils/error-helper'; +import { formatSubscriptionUserInfo } from '../utils/subscription'; + +const logger = createLogger({ + service: 'surgio:SubscriptionsCommand', +}); +type PossibleProviderType = BlackSSLProvider & ShadowsocksJsonSubscribeProvider & ShadowsocksSubscribeProvider & CustomProvider & V2rayNSubscribeProvider & ShadowsocksrSubscribeProvider & ClashProvider; + +class SubscriptionsCommand extends Command { + private options: object; + private config: CommandConfig; + + constructor(rawArgv) { + super(rawArgv); + this.usage = '使用方法: surgio subscriptions'; + this.options = { + c: { + alias: 'config', + demandOption: false, + describe: 'Surgio 配置文件', + default: './surgio.conf.js', + type: 'string', + }, + }; + } + + public async run(ctx): Promise { + this.config = loadConfig(ctx.cwd, ctx.argv.config); + + const providerList = await this.listProviders(); + + for (const provider of providerList) { + if (provider.getSubscriptionUserInfo) { + const userInfo = await provider.getSubscriptionUserInfo(); + + if (userInfo) { + const format = formatSubscriptionUserInfo(userInfo); + console.log('🤟 %s 已用流量:%s 剩余流量:%s 有效期至:%s', provider.name, format.used, format.left, format.expire); + } else { + console.log('⚠️ 无法查询 %s 的流量信息', provider.name); + } + } else { + console.log('⚠️ 无法查询 %s 的流量信息', provider.name); + } + } + } + + public get description(): string { + return '查询订阅流量'; + } + + public errorHandler(err): void { + errorHandler.call(this, err); + } + + private async listProviders(): Promise> { + const files = await fsp.readdir(this.config.providerDir, { + encoding: 'utf8', + }); + const providerList: PossibleProviderType[] = []; + + async function readProvider(path): Promise { + let provider; + + try { + const providerName = basename(path, '.js'); + + logger.debug('read %s %s', providerName, path); + provider = getProvider(providerName, require(path)); + } catch (err) { + logger.debug(`${path} 不是一个合法的模块`); + return undefined; + } + + if (!provider?.type) { + logger.debug(`${path} 不是一个 Provider`); + return undefined; + } + + logger.debug('got provider %j', provider); + return provider; + } + + for (const file of files) { + const result = await readProvider(join(this.config.providerDir, file)); + if (result) { + providerList.push(result); + } + } + + return providerList; + } +} + +export = SubscriptionsCommand; diff --git a/lib/command/upload.ts b/lib/command/upload.ts index afd5c2218..a927cb709 100644 --- a/lib/command/upload.ts +++ b/lib/command/upload.ts @@ -18,14 +18,17 @@ class GenerateCommand extends Command { this.usage = '使用方法: surgio upload'; this.spinner = ora(); this.options = { - output: { + o: { type: 'string', - alias: 'o', + alias: 'output', description: '生成规则的目录', }, - config: { - alias: 'c', + c: { + alias: 'config', + demandOption: false, + describe: 'Surgio 配置文件', default: './surgio.conf.js', + type: 'string', }, }; } diff --git a/lib/index.ts b/lib/index.ts index 940f443b8..1b12fb491 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -6,6 +6,7 @@ import fs from 'fs'; import env2 from 'env2'; import path from 'path'; import updateNotifier from 'update-notifier'; +import { transports } from '@surgio/logger'; import GenerateCommand from './command/generate'; import UploadCommand from './command/upload'; @@ -30,8 +31,18 @@ export class SurgioCommand extends Command { updateNotifier({ pkg: require('../package.json') }).notify(); this.usage = '使用方法: surgio [options]'; + this.yargs.option('V', { + alias: 'verbose', + demandOption: false, + describe: '打印调试日志', + type: 'boolean', + }); + this.load(path.join(__dirname, './command')); - this.yargs.alias('v', 'version'); + + if (this.yargs.argv.verbose) { + transports.console.level = 'debug'; + } } public errorHandler(err): void { diff --git a/lib/provider/ClashProvider.ts b/lib/provider/ClashProvider.ts index e915a2696..e8e052c14 100644 --- a/lib/provider/ClashProvider.ts +++ b/lib/provider/ClashProvider.ts @@ -15,8 +15,8 @@ import { SnellNodeConfig, SubscriptionUserinfo, VmessNodeConfig, } from '../types'; -import { parseSubscriptionUserInfo } from '../utils'; -import { ConfigCache, SubsciptionCacheItem, SubscriptionCache } from '../utils/cache'; +import { parseSubscriptionUserInfo } from '../utils/subscription'; +import { SubsciptionCacheItem, SubscriptionCache } from '../utils/cache'; import { NETWORK_TIMEOUT } from '../utils/constant'; import Provider from './Provider'; diff --git a/lib/provider/ShadowsocksSubscribeProvider.ts b/lib/provider/ShadowsocksSubscribeProvider.ts index a788fab6c..a27569069 100644 --- a/lib/provider/ShadowsocksSubscribeProvider.ts +++ b/lib/provider/ShadowsocksSubscribeProvider.ts @@ -10,7 +10,8 @@ import { ShadowsocksSubscribeProviderConfig, SubscriptionUserinfo, } from '../types'; -import { decodeStringList, fromBase64, fromUrlSafeBase64, parseSubscriptionUserInfo } from '../utils'; +import { decodeStringList, fromBase64, fromUrlSafeBase64 } from '../utils'; +import { parseSubscriptionUserInfo } from '../utils/subscription'; import { SubsciptionCacheItem, SubscriptionCache } from '../utils/cache'; import { NETWORK_TIMEOUT } from '../utils/constant'; import Provider from './Provider'; diff --git a/lib/provider/ShadowsocksrSubscribeProvider.ts b/lib/provider/ShadowsocksrSubscribeProvider.ts index 675be4bd5..d977989ee 100644 --- a/lib/provider/ShadowsocksrSubscribeProvider.ts +++ b/lib/provider/ShadowsocksrSubscribeProvider.ts @@ -3,7 +3,8 @@ import assert from 'assert'; import got from 'got'; import { ShadowsocksrNodeConfig, ShadowsocksrSubscribeProviderConfig, SubscriptionUserinfo } from '../types'; -import { fromBase64, parseSubscriptionUserInfo } from '../utils'; +import { fromBase64 } from '../utils'; +import { parseSubscriptionUserInfo } from '../utils/subscription'; import { SubsciptionCacheItem, SubscriptionCache } from '../utils/cache'; import { NETWORK_TIMEOUT } from '../utils/constant'; import { parseSSRUri } from '../utils/ssr'; @@ -40,6 +41,15 @@ export default class ShadowsocksrSubscribeProvider extends Provider { this.udpRelay = config.udpRelay; } + public async getSubscriptionUserInfo(): Promise { + const { subscriptionUserinfo } = await getShadowsocksrSubscription(this.url, this.udpRelay); + + if (subscriptionUserinfo) { + return subscriptionUserinfo; + } + return null; + } + public async getNodeList(): Promise> { const { nodeList } = await getShadowsocksrSubscription(this.url, this.udpRelay); diff --git a/lib/types.ts b/lib/types.ts index 15c091516..1eef0be51 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,11 @@ +import BlackSSLProvider from './provider/BlackSSLProvider'; +import ClashProvider from './provider/ClashProvider'; +import CustomProvider from './provider/CustomProvider'; import Provider from './provider/Provider'; +import ShadowsocksJsonSubscribeProvider from './provider/ShadowsocksJsonSubscribeProvider'; +import ShadowsocksrSubscribeProvider from './provider/ShadowsocksrSubscribeProvider'; +import ShadowsocksSubscribeProvider from './provider/ShadowsocksSubscribeProvider'; +import V2rayNSubscribeProvider from './provider/V2rayNSubscribeProvider'; export enum NodeTypeEnum { HTTPS = 'https', diff --git a/lib/utils/index.ts b/lib/utils/index.ts index fb8cbc96e..d2cdecd0c 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -1232,23 +1232,3 @@ export const applyFilter = ( return nodes; }; - -export const parseSubscriptionUserInfo = (str: string): SubscriptionUserinfo => { - const res = { - upload: 0, - download: 0, - total: 0, - expire: 0, - }; - - str.split(';').forEach(item => { - const pair = item.split('='); - const value = Number(pair[1].trim()); - - if (!Number.isNaN(value)) { - res[pair[0].trim()] = Number(pair[1].trim()) - } - }); - - return res; -}; diff --git a/lib/utils/subscription.ts b/lib/utils/subscription.ts new file mode 100644 index 000000000..7ddb41d03 --- /dev/null +++ b/lib/utils/subscription.ts @@ -0,0 +1,44 @@ +import filesize from 'filesize'; +import { format, formatDistanceToNow } from 'date-fns'; + +import { SubscriptionUserinfo } from '../types'; + +export const parseSubscriptionUserInfo = (str: string): SubscriptionUserinfo => { + const res = { + upload: 0, + download: 0, + total: 0, + expire: 0, + }; + + str.split(';').forEach(item => { + const pair = item.split('='); + const value = Number(pair[1].trim()); + + if (!Number.isNaN(value)) { + res[pair[0].trim()] = Number(pair[1].trim()) + } + }); + + return res; +}; + +export const formatSubscriptionUserInfo = (userInfo: SubscriptionUserinfo): { + readonly upload: string; + readonly download: string; + readonly used: string; + readonly left: string; + readonly total: string; + readonly expire: string; +} => { + return { + upload: filesize(userInfo.upload), + download: filesize(userInfo.download), + used: filesize(userInfo.upload + userInfo.download), + left: filesize(userInfo.total - userInfo.upload - userInfo.download), + total: filesize(userInfo.total), + expire: userInfo.expire + ? `${format(Date.now() + userInfo.expire, 'yyyy-MM-dd')} (${formatDistanceToNow(Date.now() + userInfo.expire)})` + : '无数据', + }; +}; diff --git a/package.json b/package.json index a82c9a949..8dcf1e6d3 100644 --- a/package.json +++ b/package.json @@ -83,10 +83,12 @@ "chalk": "^3.0.0", "common-bin": "^2.8.3", "cross-env": "^7.0.0", + "date-fns": "^2.10.0", "debug": "^4.1.1", "emoji-regex": "^8.0.0", "env2": "^2.2.2", "execa": "^4.0.0", + "filesize": "^6.1.0", "fs-extra": "^8.1.0", "get-port": "^5.1.0", "global-agent": "^2.1.7", diff --git a/yarn.lock b/yarn.lock index ac8fb16f3..023393358 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3810,6 +3810,11 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== +date-fns@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.10.0.tgz#abd10604d8bafb0bcbd2ba2e9b0563b922ae4b6b" + integrity sha512-EhfEKevYGWhWlZbNeplfhIU/+N+x0iCIx7VzKlXma2EdQyznVlZhCptXUY+BegNpPW2kjdx15Rvq503YcXXrcA== + date-time@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/date-time/-/date-time-2.1.0.tgz#0286d1b4c769633b3ca13e1e62558d2dbdc2eba2" @@ -4926,6 +4931,11 @@ file-uri-to-path@1, file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +filesize@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00" + integrity sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg== + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"