Skip to content

feat: add keyword filters for node subscription #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/API-doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ https://your-worker-domain.workers.dev
- **方法**: GET
- **参数**:
- `config` (必需): URL 编码的字符串,包含一个或多个代理配置
- `filters` (可选): 一个或多个关键词,用逗号分隔,用于过滤包含关键词的节点,不支持正则表达式
- `selectedRules` (可选): 预定义规则集名称或自定义规则的 JSON 数组
- `customRules` (可选): 自定义规则的 JSON 数组
- `pin` (可选): 布尔值,是否将自定义规则置于预定义规则之上
Expand Down
11 changes: 6 additions & 5 deletions src/BaseConfigBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ import { ProxyParser } from './ProxyParsers.js';
import { DeepCopy } from './utils.js';

export class BaseConfigBuilder {
constructor(inputString, baseConfig) {
constructor(inputString, filters, baseConfig) {
this.inputString = inputString;
this.config = DeepCopy(baseConfig);
this.customRules = [];
this.filters = filters;
}

async build() {
const customItems = await this.parseCustomItems();
const customItems = await this.parseCustomItems(this.filters);
this.addCustomItems(customItems);
this.addSelectors();
return this.formatConfig();
}

async parseCustomItems() {
async parseCustomItems(filters) {
const urls = this.inputString.split('\n').filter(url => url.trim() !== '');
const parsedItems = [];

Expand All @@ -24,11 +25,11 @@ export class BaseConfigBuilder {
if (Array.isArray(result)) {
for (const subUrl of result) {
const subResult = await ProxyParser.parse(subUrl);
if (subResult) {
if (subResult && !filters.some(filter => subResult.tag.includes(filter))) {
parsedItems.push(subResult);
}
}
} else if (result) {
} else if (result && !filters.some(filter => result.tag.includes(filter))) {
parsedItems.push(result);
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/ClashConfigBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { BaseConfigBuilder } from './BaseConfigBuilder.js';
import { DeepCopy } from './utils.js';

export class ClashConfigBuilder extends BaseConfigBuilder {
constructor(inputString, selectedRules, customRules, pin, baseConfig) {
constructor(inputString, filters, selectedRules, customRules, pin, baseConfig) {
if (!baseConfig) {
baseConfig = CLASH_CONFIG
}
super(inputString, baseConfig);
if (!filters) {
filters = [];
}
super(inputString, filters ,baseConfig);
this.selectedRules = selectedRules;
this.customRules = customRules;
this.pin = pin;
Expand Down
9 changes: 6 additions & 3 deletions src/SingboxConfigBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { BaseConfigBuilder } from './BaseConfigBuilder.js';
import { DeepCopy } from './utils.js';

export class ConfigBuilder extends BaseConfigBuilder {
constructor(inputString, selectedRules, customRules, pin, baseConfig) {
constructor(inputString, filters, selectedRules, customRules, pin, baseConfig) {
if (baseConfig === undefined) {
baseConfig = SING_BOX_CONFIG
baseConfig = SING_BOX_CONFIG;
}
super(inputString, baseConfig);
if (!filters) {
filters = [];
}
super(inputString, filters ,baseConfig);
this.selectedRules = selectedRules;
this.customRules = customRules;
this.pin = pin;
Expand Down
22 changes: 19 additions & 3 deletions src/htmlBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ const generateForm = () => `
<div class="form-section">
<div class="form-section-title">Share URLs</div>
<textarea class="form-control" id="inputTextarea" name="input" required placeholder="vmess://abcd..." rows="3"></textarea>
<div class="form-section-title" style="margin-top:1rem;">Filter Keywords</div>
<textarea class="form-control" id="inputFilter" name="filters" placeholder="剩余,官网,到期" rows="1"></textarea>
</div>

<div class="form-check form-switch mb-3">
Expand Down Expand Up @@ -468,9 +470,11 @@ const submitFormFunction = () => `
const form = event.target;
const formData = new FormData(form);
const inputString = formData.get('input');
const filters = formData.get('filters') ? formData.get('filters').split(',') : '';

// Save form data to localStorage
localStorage.setItem('inputTextarea', inputString);
localStorage.setItem('inputFilter', filters);
localStorage.setItem('advancedToggle', document.getElementById('advancedToggle').checked);
localStorage.setItem('crpinToggle', document.getElementById('crpinToggle').checked);

Expand Down Expand Up @@ -503,9 +507,14 @@ const submitFormFunction = () => `

const configParam = configId ? \`&configId=\${configId}\` : '';
const xrayUrl = \`\${window.location.origin}/xray?config=\${encodeURIComponent(inputString)}\${configParam}\`;
const singboxUrl = \`\${window.location.origin}/singbox?config=\${encodeURIComponent(inputString)}&selectedRules=\${encodeURIComponent(JSON.stringify(selectedRules))}&customRules=\${encodeURIComponent(JSON.stringify(customRules))}&pin=\${pin}\${configParam}\`;
const clashUrl = \`\${window.location.origin}/clash?config=\${encodeURIComponent(inputString)}&selectedRules=\${encodeURIComponent(JSON.stringify(selectedRules))}&customRules=\${encodeURIComponent(JSON.stringify(customRules))}&pin=\${pin}\${configParam}\`;

let singboxUrl, clashUrl;
if (filters.length > 0) {
singboxUrl = \`\${window.location.origin}/singbox?config=\${encodeURIComponent(inputString)}&filters=\${encodeURIComponent(JSON.stringify(filters))}&selectedRules=\${encodeURIComponent(JSON.stringify(selectedRules))}&customRules=\${encodeURIComponent(JSON.stringify(customRules))}&pin=\${pin}\${configParam}\`;
clashUrl = \`\${window.location.origin}/clash?config=\${encodeURIComponent(inputString)}&filters=\${encodeURIComponent(JSON.stringify(filters))}&selectedRules=\${encodeURIComponent(JSON.stringify(selectedRules))}&customRules=\${encodeURIComponent(JSON.stringify(customRules))}&pin=\${pin}\${configParam}\`;
} else {
singboxUrl = \`\${window.location.origin}/singbox?config=\${encodeURIComponent(inputString)}&selectedRules=\${encodeURIComponent(JSON.stringify(selectedRules))}&customRules=\${encodeURIComponent(JSON.stringify(customRules))}&pin=\${pin}\${configParam}\`;
clashUrl = \`\${window.location.origin}/clash?config=\${encodeURIComponent(inputString)}&selectedRules=\${encodeURIComponent(JSON.stringify(selectedRules))}&customRules=\${encodeURIComponent(JSON.stringify(customRules))}&pin=\${pin}\${configParam}\`;
}
document.getElementById('xrayLink').value = xrayUrl;
document.getElementById('singboxLink').value = singboxUrl;
document.getElementById('clashLink').value = clashUrl;
Expand All @@ -524,6 +533,11 @@ const submitFormFunction = () => `
if (savedInput) {
document.getElementById('inputTextarea').value = savedInput;
}

const savedFilter = localStorage.getItem('inputFilter');
if (savedFilter) {
document.getElementById('inputFilter').value = savedFilter;
}

const advancedToggle = localStorage.getItem('advancedToggle');
if (advancedToggle) {
Expand Down Expand Up @@ -579,13 +593,15 @@ const submitFormFunction = () => `

function clearFormData() {
localStorage.removeItem('inputTextarea');
localStorage.removeItem('inputFilter');
localStorage.removeItem('advancedToggle');
localStorage.removeItem('selectedRules');
localStorage.removeItem('predefinedRules');
localStorage.removeItem('configEditor'); // 添加清除 configEditor
localStorage.removeItem('configType'); // 添加清除 configType

document.getElementById('inputTextarea').value = '';
document.getElementById('inputFilter').value = '';
document.getElementById('advancedToggle').checked = false;
document.getElementById('advancedOptions').classList.remove('show');
document.getElementById('configEditor').value = '';
Expand Down
10 changes: 6 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async function handleRequest(request) {
} else if (request.method === 'POST' && url.pathname === '/') {
const formData = await request.formData();
const inputString = formData.get('input');
const filters = formData.getAll('filters[]');
const selectedRules = formData.getAll('selectedRules');
const customRuleDomains = formData.getAll('customRuleSite[]');
const customRuleIPs = formData.getAll('customRuleIP[]');
Expand All @@ -40,14 +41,15 @@ async function handleRequest(request) {
const rulesToUse = selectedRules.length > 0 ? selectedRules : ['广告拦截', '谷歌服务', '国外媒体', '电报消息'];

const xrayUrl = `${url.origin}/xray?config=${encodeURIComponent(inputString)}`;
const singboxUrl = `${url.origin}/singbox?config=${encodeURIComponent(inputString)}&selectedRules=${encodeURIComponent(JSON.stringify(rulesToUse))}&customRules=${encodeURIComponent(JSON.stringify(customRules))}pin=${pin}`;
const clashUrl = `${url.origin}/clash?config=${encodeURIComponent(inputString)}&selectedRules=${encodeURIComponent(JSON.stringify(rulesToUse))}&customRules=${encodeURIComponent(JSON.stringify(customRules))}pin=${pin}`;
const singboxUrl = `${url.origin}/singbox?config=${encodeURIComponent(inputString)}&filters=${encodeURIComponent(JSON.stringify(filters))}&selectedRules=${encodeURIComponent(JSON.stringify(rulesToUse))}&customRules=${encodeURIComponent(JSON.stringify(customRules))}pin=${pin}`;
const clashUrl = `${url.origin}/clash?config=${encodeURIComponent(inputString)}&filters=${encodeURIComponent(JSON.stringify(filters))}&selectedRules=${encodeURIComponent(JSON.stringify(rulesToUse))}&customRules=${encodeURIComponent(JSON.stringify(customRules))}pin=${pin}`;

return new Response(generateHtml(xrayUrl, singboxUrl, clashUrl), {
headers: { 'Content-Type': 'text/html' }
});
} else if (url.pathname.startsWith('/singbox') || url.pathname.startsWith('/clash')) {
const inputString = url.searchParams.get('config');
let filters = JSON.parse(url.searchParams.get('filters'));
let selectedRules = url.searchParams.get('selectedRules');
let customRules = url.searchParams.get('customRules');
let pin = url.searchParams.get('pin');
Expand Down Expand Up @@ -89,9 +91,9 @@ async function handleRequest(request) {
// Env pin is use to pin customRules to top
let configBuilder;
if (url.pathname.startsWith('/singbox')) {
configBuilder = new ConfigBuilder(inputString, selectedRules, customRules, pin, baseConfig);
configBuilder = new ConfigBuilder(inputString, filters, selectedRules, customRules, pin, baseConfig);
} else {
configBuilder = new ClashConfigBuilder(inputString, selectedRules, customRules, pin, baseConfig);
configBuilder = new ClashConfigBuilder(inputString, filters, selectedRules, customRules, pin, baseConfig);
}

const config = await configBuilder.build();
Expand Down