Skip to content

Commit

Permalink
feat: 支持排序类型的过滤器
Browse files Browse the repository at this point in the history
  • Loading branch information
geekdada committed Nov 18, 2019
1 parent 128f648 commit db69447
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 115 deletions.
2 changes: 2 additions & 0 deletions lib/class/BlackSSLProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// istanbul ignore file

import Joi from '@hapi/joi';
import { BlackSSLProviderConfig } from '../types';
import { getBlackSSLConfig } from '../utils';
Expand Down
6 changes: 5 additions & 1 deletion lib/class/Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export default class Provider {
nodeFilter: Joi.function(),
netflixFilter: Joi.function(),
youtubePremiumFilter: Joi.function(),
customFilters: Joi.object().pattern(Joi.string(), Joi.function()),
customFilters: Joi.object()
.pattern(
Joi.string(),
Joi.any().allow(Joi.function(), Joi.object({ filter: Joi.function(), supportSort: Joi.boolean() }))
),
addFlag: Joi.boolean(),
startPort: Joi.number().integer().min(1024).max(65535),
})
Expand Down
11 changes: 8 additions & 3 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface CommandConfig {
readonly proxyTestUrl?: string;
readonly proxyTestInterval?: number;
readonly customFilters?: {
readonly [name: string]: NodeNameFilterType;
readonly [name: string]: NodeNameFilterType|SortedNodeNameFilterType;
};
}

Expand Down Expand Up @@ -79,7 +79,7 @@ export interface ProviderConfig {
readonly youtubePremiumFilter?: NodeNameFilterType;
readonly startPort?: number;
readonly customFilters?: {
readonly [name: string]: NodeNameFilterType;
readonly [name: string]: NodeNameFilterType|SortedNodeNameFilterType;
};
readonly addFlag?: boolean;
readonly tfo?: boolean;
Expand Down Expand Up @@ -189,9 +189,14 @@ export type NodeFilterType = (nodeConfig: PossibleNodeConfigType) => boolean;

export type NodeNameFilterType = (simpleNodeConfig: SimpleNodeConfig) => boolean;

export interface SortedNodeNameFilterType {
readonly filter: <T>(nodeList: ReadonlyArray<T & SimpleNodeConfig>) => ReadonlyArray<T & SimpleNodeConfig>;
readonly supportSort?: boolean;
}

export type PossibleNodeConfigType = HttpsNodeConfig|ShadowsocksNodeConfig|ShadowsocksrNodeConfig|SnellNodeConfig|VmessNodeConfig;

export type ProxyGroupModifier = (nodeList: ReadonlyArray<PossibleNodeConfigType>, filters: PlainObjectOf<NodeNameFilterType>) => ReadonlyArray<{
export type ProxyGroupModifier = (nodeList: ReadonlyArray<PossibleNodeConfigType>, filters: PlainObjectOf<NodeNameFilterType|SortedNodeNameFilterType>) => ReadonlyArray<{
readonly name: string;
readonly type: 'select'|'url-test'|'fallback'|'load-balance';
readonly proxies?: ReadonlyArray<string>;
Expand Down
6 changes: 5 additions & 1 deletion lib/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ export const validateConfig = (userConfig: Partial<CommandConfig>): void => {
],
}),
proxyTestInterval: Joi.number(),
customFilters: Joi.object().pattern(Joi.string(), Joi.function()),
customFilters: Joi.object()
.pattern(
Joi.string(),
Joi.any().allow(Joi.function(), Joi.object({ filter: Joi.function(), supportSort: Joi.boolean() }))
),
})
.unknown();

Expand Down
85 changes: 82 additions & 3 deletions lib/utils/filter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,66 @@
import _ from 'lodash';

import { NodeNameFilterType, SimpleNodeConfig } from '../types';
import { NodeNameFilterType, SimpleNodeConfig, SortedNodeNameFilterType } from '../types';

// tslint:disable-next-line:max-classes-per-file
export class SortFilterWithSortedFilters implements SortedNodeNameFilterType {
public supportSort = true;

constructor(public _filters: ReadonlyArray<NodeNameFilterType>) {
this.filter.bind(this);
}

public filter<T>(nodeList: ReadonlyArray<T & SimpleNodeConfig>): ReadonlyArray<T & SimpleNodeConfig> {
const result = [];

this._filters.forEach(filter => {
result.push(...nodeList.filter(filter));
});

return _.uniqBy(result, node => node.nodeName);
}
}

// tslint:disable-next-line:max-classes-per-file
export class SortFilterWithSortedKeywords implements SortedNodeNameFilterType {
public supportSort = true;

constructor(public _keywords: ReadonlyArray<string>) {
this.filter.bind(this);
}

public filter<T>(nodeList: ReadonlyArray<T & SimpleNodeConfig>): ReadonlyArray<T & SimpleNodeConfig> {
const result = [];

this._keywords.forEach(keyword => {
result.push(...nodeList.filter(node => node.nodeName.includes(keyword)));
});

return _.uniqBy(result, node => node.nodeName);
}
}

export const validateFilter = (filter: any): boolean => {
if (filter === null || filter === void 0) {
return false;
}
if (typeof filter === 'function') {
return true;
}
return typeof filter === 'object' && filter.supportSort && typeof filter.filter === 'function';
};

export const mergeFilters = (filters: ReadonlyArray<NodeNameFilterType>, isStrict?: boolean): NodeNameFilterType => {
filters.forEach(filter => {
if (filter.hasOwnProperty('supportSort') && (filter as any).supportSort) {
throw new Error('mergeFilters 不支持包含排序功能的过滤器');
}

if (typeof filter !== 'function') {
throw new Error('mergeFilters 传入了无效的过滤器');
}
});

return (item: SimpleNodeConfig) => {
return filters[isStrict ? 'every' : 'some'](filter => filter(item));
};
Expand All @@ -17,8 +75,6 @@ export const useKeywords = (keywords: ReadonlyArray<string>, isStrict?: boolean)
return item => keywords[isStrict ? 'every' : 'some'](keyword => item.nodeName.includes(keyword));
};

// export const useSortedKeywords = ()

export const discardKeywords = (keywords: ReadonlyArray<string>, isStrict?: boolean): NodeNameFilterType => {
// istanbul ignore next
if (!Array.isArray(keywords)) {
Expand All @@ -37,6 +93,29 @@ export const useRegexp = (regexp: RegExp): NodeNameFilterType => {
return item => regexp.test(item.nodeName);
};

export const useSortedKeywords = (keywords: ReadonlyArray<string>): SortedNodeNameFilterType => {
// istanbul ignore next
if (!Array.isArray(keywords)) {
throw new Error('keywords 请使用数组');
}

return new SortFilterWithSortedKeywords(keywords);
};

export const mergeSortedFilters = (filters: ReadonlyArray<NodeNameFilterType>): SortedNodeNameFilterType => {
filters.forEach(filter => {
if (filter.hasOwnProperty('supportSort') && (filter as any).supportSort) {
throw new Error('mergeSortedFilters 不支持包含排序功能的过滤器');
}

if (typeof filter !== 'function') {
throw new Error('mergeSortedFilters 传入了无效的过滤器');
}
});

return new SortFilterWithSortedFilters(filters);
};

export const netflixFilter: NodeNameFilterType = item => {
return [
'netflix',
Expand Down
94 changes: 41 additions & 53 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ import {
ShadowsocksrNodeConfig,
SimpleNodeConfig,
SnellNodeConfig,
SortedNodeNameFilterType,
VmessNodeConfig,
} from '../types';
import { normalizeConfig, validateConfig } from './config';
import { validateFilter } from './filter';
import { parseSSRUri } from './ssr';
import { OBFS_UA, NETWORK_TIMEOUT, NETWORK_CONCURRENCY, PROXY_TEST_URL, PROXY_TEST_INTERVAL } from './constant';
import { formatVmessUri } from './v2ray';
Expand Down Expand Up @@ -302,13 +303,10 @@ export const getV2rayNSubscription = async (

export const getSurgeNodes = (
list: ReadonlyArray<HttpsNodeConfig|ShadowsocksNodeConfig|SnellNodeConfig|ShadowsocksrNodeConfig|VmessNodeConfig>,
filter?: NodeFilterType,
filter?: NodeFilterType|SortedNodeNameFilterType,
): string => {
const result: string[] = list
.filter(item => filter ? filter(item) : true)
const result: string[] = applyFilter(list, filter)
.map<string>(nodeConfig => {
if (nodeConfig.enable === false) { return null; }

switch (nodeConfig.type) {
case NodeTypeEnum.Shadowsocks: {
const config = nodeConfig as ShadowsocksNodeConfig;
Expand Down Expand Up @@ -483,10 +481,8 @@ export const getSurgeNodes = (

export const getClashNodes = (
list: ReadonlyArray<PossibleNodeConfigType>,
filter?: NodeFilterType
): ReadonlyArray<any> => {
return list
.filter(item => filter ? filter(item) : true)
.map(nodeConfig => {
if (nodeConfig.enable === false) { return null; }

Expand Down Expand Up @@ -568,13 +564,10 @@ export const getClashNodes = (

export const getMellowNodes = (
list: ReadonlyArray<VmessNodeConfig>,
filter?: NodeFilterType
filter?: NodeFilterType|SortedNodeNameFilterType
): string => {
const result = list
.filter(item => filter ? filter(item) : true)
const result = applyFilter(list, filter)
.map(nodeConfig => {
if (nodeConfig.enable === false) { return null; }

switch (nodeConfig.type) {
case NodeTypeEnum.Vmess: {
const uri = formatVmessUri(nodeConfig);
Expand Down Expand Up @@ -747,7 +740,7 @@ export const getV2rayNNodes = (list: ReadonlyArray<VmessNodeConfig>): string =>
export const getQuantumultNodes = (
list: ReadonlyArray<ShadowsocksNodeConfig|VmessNodeConfig|ShadowsocksrNodeConfig|HttpsNodeConfig>,
groupName: string = 'Surgio',
filter?: NodeNameFilterType,
filter?: NodeNameFilterType|SortedNodeNameFilterType,
): string => {
function getHeader(
host: string,
Expand All @@ -759,13 +752,7 @@ export const getQuantumultNodes = (
].join('[Rr][Nn]');
}

const result: ReadonlyArray<string> = list
.filter(item => {
if (filter) {
return filter(item) && item.enable !== false;
}
return item.enable !== false;
})
const result: ReadonlyArray<string> = applyFilter(list, filter)
.map<string>(nodeConfig => {
switch (nodeConfig.type) {
case NodeTypeEnum.Vmess: {
Expand Down Expand Up @@ -829,15 +816,9 @@ export const getQuantumultNodes = (
*/
export const getQuantumultXNodes = (
list: ReadonlyArray<ShadowsocksNodeConfig|VmessNodeConfig|ShadowsocksrNodeConfig|HttpsNodeConfig>,
filter?: NodeNameFilterType,
filter?: NodeNameFilterType|SortedNodeNameFilterType,
): string => {
const result: ReadonlyArray<string> = list
.filter(item => {
if (filter) {
return filter(item) && item.enable !== false;
}
return item.enable !== false;
})
const result: ReadonlyArray<string> = applyFilter(list, filter)
.map<string>(nodeConfig => {
switch (nodeConfig.type) {
case NodeTypeEnum.Vmess: {
Expand Down Expand Up @@ -989,28 +970,18 @@ export const getShadowsocksNodesJSON = (list: ReadonlyArray<ShadowsocksNodeConfi

export const getNodeNames = (
list: ReadonlyArray<SimpleNodeConfig>,
filter?: NodeNameFilterType,
filter?: NodeNameFilterType|SortedNodeNameFilterType,
separator?: string,
): string => {
const nodes = list.filter(item => {
const result = item.enable !== false;

if (filter) {
return filter(item) && result;
}

return result;
});

return nodes.map(item => item.nodeName).join(separator || ', ');
return applyFilter(list, filter).map(item => item.nodeName).join(separator || ', ');
};

export const getClashNodeNames = (
ruleName: string,
ruleType: 'select'|'url-test'|'fallback'|'load-balance',
nodeNameList: ReadonlyArray<SimpleNodeConfig>,
options: {
readonly filter?: NodeNameFilterType,
readonly filter?: NodeNameFilterType|SortedNodeNameFilterType,
readonly existingProxies?: ReadonlyArray<string>,
readonly proxyTestUrl?: string,
readonly proxyTestInterval?: number,
Expand All @@ -1025,15 +996,7 @@ export const getClashNodeNames = (
readonly url?: string;
readonly interval?: number;
} => {
const nodes = nodeNameList.filter(item => {
const result = item.enable !== false;

if (options.filter) {
return options.filter(item) && result;
}

return result;
});
const nodes = applyFilter(nodeNameList, options.filter);
const proxies = options.existingProxies ?
[].concat(options.existingProxies, nodes.map(item => item.nodeName)) :
nodes.map(item => item.nodeName);
Expand Down Expand Up @@ -1072,7 +1035,7 @@ export const decodeStringList = <T = object>(stringList: ReadonlyArray<string>):

export const normalizeClashProxyGroupConfig = (
nodeList: ReadonlyArray<PossibleNodeConfigType>,
customFilters: PlainObjectOf<NodeNameFilterType>,
customFilters: PlainObjectOf<NodeNameFilterType|SortedNodeNameFilterType>,
proxyGroupModifier: ProxyGroupModifier,
options: {
readonly proxyTestUrl?: string,
Expand All @@ -1084,7 +1047,7 @@ export const normalizeClashProxyGroupConfig = (
return proxyGroup.map<any>(item => {
if (item.hasOwnProperty('filter')) {
// istanbul ignore next
if (!item.filter || typeof item.filter !== 'function') {
if (!item.filter || !validateFilter(item.filter)) {
throw new Error(`过滤器 ${item.filter} 无效,请检查 proxyGroupModifier`);
}

Expand Down Expand Up @@ -1248,3 +1211,28 @@ export const formatV2rayConfig = (localPort: string|number, nodeConfig: VmessNod

return config;
};

export const applyFilter = <T extends SimpleNodeConfig>(
nodeList: ReadonlyArray<T>,
filter?: NodeNameFilterType|SortedNodeNameFilterType
): ReadonlyArray<T> => {
if (filter && !validateFilter(filter)) {
throw new Error(`使用了无效的过滤器 ${filter}`);
}

let nodes: ReadonlyArray<T> = nodeList.filter(item => {
const result = item.enable !== false;

if (filter && typeof filter === 'function') {
return filter(item) && result;
}

return result;
});

if (filter && typeof filter === 'object' && typeof filter.filter === 'function') {
nodes = filter.filter(nodes);
}

return nodes;
};
5 changes: 2 additions & 3 deletions test/fixture/custom-filter/provider/ss.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
'use strict';

const sinon = require('sinon');
const { utils } = require('../../../../');

exports.keywordFilter = sinon.spy(utils.useKeywords(['US 1', 'US 2']));
exports.strictKeywordFilter = sinon.spy(utils.useKeywords(['US', 'Netflix'], true));
exports.keywordFilter = utils.useKeywords(['US 1', 'US 2']);
exports.strictKeywordFilter = utils.useKeywords(['US', 'Netflix'], true);

module.exports = {
url: 'http://example.com/test-ss-sub.txt',
Expand Down
Loading

0 comments on commit db69447

Please sign in to comment.