Skip to content

Commit

Permalink
refactor: fine
Browse files Browse the repository at this point in the history
  • Loading branch information
SharerMax committed Jan 9, 2024
1 parent cf0ee66 commit 6cd3e6c
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 140 deletions.
151 changes: 151 additions & 0 deletions src/convetor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import fs from 'node:fs/promises'
import { existsSync as fileExistsSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import Readline from 'node:readline/promises'
import debug from 'debug'
import { stringify as yamlStringify } from 'yaml'

const debugLogger = debug('v2ray-2-clash-rule:convetor')
const __dirname = path.dirname(fileURLToPath(import.meta.url))
function isFullDomainRule(rule: string) {
return rule.startsWith('full:')
}
function isKeywordRule(rule: string) {
return rule.startsWith('keyword:')
}
function isRegexDomainRule(rule: string) {
return rule.startsWith('regexp:')
}

function isIncludeRule(rule: string) {
return rule.startsWith('include:')
}

function isInvalidRule(rule?: string) {
return !rule || !rule.trim() || rule.startsWith('#') || rule.includes('@')
}

interface V2RayRules {
fullDomain: string[]
subdomain: string[]
keyword: string[]
}

interface ClashRule {
payload: string[]
type: 'classic' | 'domain'
}

async function parseV2rayRuleFile(v2rayRuleFilePath: string) {
const openedFile = await fs.open(path.resolve(v2rayRuleFilePath), 'r')
const fileReadStream = openedFile.createReadStream()
const rl = Readline.createInterface({
input: fileReadStream,
})
const result: V2RayRules = {
fullDomain: Array<string>(),
subdomain: Array<string>(),
keyword: Array<string>(),
}
for await (let rule of rl) {
const commentIndex = rule.indexOf('#')
if (commentIndex >= 0)
rule = rule.slice(0, commentIndex).trim()

if (isInvalidRule(rule)) {
debugLogger('ignore invalid rule: ', rule)
continue
}
else if (isFullDomainRule(rule)) {
result.fullDomain.push(rule.slice(5).trim())
}
else if (isKeywordRule(rule)) {
result.keyword.push(rule.slice(8).trim())
}
else if (isRegexDomainRule(rule)) {
debugLogger('ignore regex rule: ', rule)
}
else if (isIncludeRule(rule)) {
const includeFilePath = path.resolve(path.dirname(v2rayRuleFilePath), rule.slice(8).trim())
const includeResult = await parseV2rayRuleFile(includeFilePath)
result.fullDomain = result.fullDomain.concat(includeResult.fullDomain)
result.subdomain = result.subdomain.concat(includeResult.subdomain)
result.keyword = result.keyword.concat(includeResult.keyword)
}
else {
// Subdomain begins with `domain:`, followed by a valid domain name. The prefix `domain:` may be omitted.
if (rule.startsWith('domain:'))
result.subdomain.push(rule.slice(7).trim())
else
result.subdomain.push(rule.trim())
}
}
fileReadStream.close()
return result
}

function convertV2rayRuleToClashRule(v2rayRule: V2RayRules): ClashRule {
const result: ClashRule = {
payload: Array<string>(),
type: 'domain',
}
const domainSuffixRule = Array<string>()
const domainRule = Array<string>()
const domainKeywordRule = Array<string>()
const ruleType = domainKeywordRule.length > 0 ? 'classic' : 'domain'
result.type = ruleType
if (ruleType === 'classic') {
for (const fullDomain of v2rayRule.fullDomain)
domainRule.push(`DOMAIN,${fullDomain}`)

for (const subdomain of v2rayRule.subdomain)
domainSuffixRule.push(`DOMAIN-SUFFIX,${subdomain}`)

for (const keyword of v2rayRule.keyword)
domainKeywordRule.push(`DOMAIN-KEYWORD,${keyword}`)
}
else {
for (const fullDomain of v2rayRule.fullDomain)
domainRule.push(fullDomain)
for (const subdomain of v2rayRule.subdomain)
domainSuffixRule.push(`.${subdomain}`)
}
result.payload = result.payload.concat(domainSuffixRule)
result.payload = result.payload.concat(domainRule)
result.payload = result.payload.concat(domainKeywordRule)

return result
}

export async function generateRuleList(domainDataDir: string, rulesDistPath: string) {
const dataListFiles = await fs.readdir(domainDataDir, { withFileTypes: true })

if (!fileExistsSync(rulesDistPath))
await fs.mkdir(rulesDistPath, { recursive: true })

for (const file of dataListFiles) {
if (file.isFile()) {
const v2rayRules = await parseV2rayRuleFile(path.join(file.path, file.name))
const clashRules = convertV2rayRuleToClashRule(v2rayRules)
const yamlObj = {
payload: Array<string>(),
}
yamlObj.payload = clashRules.payload
const yamlFile = await fs.open(path.join(rulesDistPath, `${file.name}.yaml`), 'w')
const writeStream = yamlFile.createWriteStream()
writeStream.write('# Generated by v2ray2clashrule\n')
if (clashRules.type === 'domain')
writeStream.write('# type: domain\n')
else
writeStream.write('# type: classic\n')
writeStream.write(yamlStringify(yamlObj))
writeStream.end()
writeStream.close()
}
}
}

export default {
generateRuleList,
}
146 changes: 6 additions & 140 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,152 +1,18 @@
import fs from 'node:fs/promises'
import { existsSync as fileExistsSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import Readline from 'node:readline/promises'
import debug from 'debug'
import { stringify as yamlStringify } from 'yaml'

import { generateRuleList } from './convetor'
import { generateRulePage } from './page'

const debugLogger = debug('v2ray-2-clash-rule')
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const dataPath = path.resolve(__dirname, '../data')
const rulesDistPath = path.resolve(__dirname, '../dist/rules')
const pageDistPath = path.resolve(__dirname, '../dist/public')
function isFullDomainRule(rule: string) {
return rule.startsWith('full:')
}
function isKeywordRule(rule: string) {
return rule.startsWith('keyword:')
}
function isRegexDomainRule(rule: string) {
return rule.startsWith('regexp:')
}

function isIncludeRule(rule: string) {
return rule.startsWith('include:')
}

function isInvalidRule(rule?: string) {
return !rule || !rule.trim() || rule.startsWith('#') || rule.includes('@')
}

interface V2RayRules {
fullDomain: string[]
subdomain: string[]
keyword: string[]
}

interface ClashRule {
payload: string[]
type: 'classic' | 'domain'
}

async function parseV2rayRuleFile(fileFullPath: string) {
const openedFile = await fs.open(path.resolve(fileFullPath), 'r')
const fileReadStream = openedFile.createReadStream()
const rl = Readline.createInterface({
input: fileReadStream,
})
const result: V2RayRules = {
fullDomain: Array<string>(),
subdomain: Array<string>(),
keyword: Array<string>(),
}
for await (let rule of rl) {
const commentIndex = rule.indexOf('#')
if (commentIndex >= 0)
rule = rule.slice(0, commentIndex).trim()
const v2rayRuleDataPath = path.resolve(__dirname, '../data')

if (isInvalidRule(rule)) {
debugLogger('ignore invalid rule: ', rule)
continue
}
else if (isFullDomainRule(rule)) {
result.fullDomain.push(rule.slice(5).trim())
}
else if (isKeywordRule(rule)) {
result.keyword.push(rule.slice(8).trim())
}
else if (isRegexDomainRule(rule)) {
debugLogger('ignore regex rule: ', rule)
}
else if (isIncludeRule(rule)) {
const includeFilePath = path.resolve(dataPath, rule.slice(8).trim())
const includeResult = await parseV2rayRuleFile(includeFilePath)
result.fullDomain = result.fullDomain.concat(includeResult.fullDomain)
result.subdomain = result.subdomain.concat(includeResult.subdomain)
result.keyword = result.keyword.concat(includeResult.keyword)
}
else {
// Subdomain begins with `domain:`, followed by a valid domain name. The prefix `domain:` may be omitted.
if (rule.startsWith('domain:'))
result.subdomain.push(rule.slice(7).trim())
else
result.subdomain.push(rule.trim())
}
}
fileReadStream.close()
return result
}

function convertV2rayRuleToClashRule(v2rayRule: V2RayRules): ClashRule {
const result: ClashRule = {
payload: Array<string>(),
type: 'domain',
}
const domainSuffixRule = Array<string>()
const domainRule = Array<string>()
const domainKeywordRule = Array<string>()
const ruleType = domainKeywordRule.length > 0 ? 'classic' : 'domain'
result.type = ruleType
if (ruleType === 'classic') {
for (const fullDomain of v2rayRule.fullDomain)
domainRule.push(`DOMAIN,${fullDomain}`)

for (const subdomain of v2rayRule.subdomain)
domainSuffixRule.push(`DOMAIN-SUFFIX,${subdomain}`)

for (const keyword of v2rayRule.keyword)
domainKeywordRule.push(`DOMAIN-KEYWORD,${keyword}`)
}
else {
for (const fullDomain of v2rayRule.fullDomain)
domainRule.push(fullDomain)
for (const subdomain of v2rayRule.subdomain)
domainSuffixRule.push(`.${subdomain}`)
}
result.payload = result.payload.concat(domainSuffixRule)
result.payload = result.payload.concat(domainRule)
result.payload = result.payload.concat(domainKeywordRule)

return result
}

const dataListFiles = await fs.readdir(dataPath, { withFileTypes: true })

if (!fileExistsSync(rulesDistPath))
await fs.mkdir(rulesDistPath, { recursive: true })
const pageDistPath = path.resolve(__dirname, '../dist/public')
const rulesDistPath = path.resolve(__dirname, '../dist/rules')

for (const file of dataListFiles) {
if (file.isFile()) {
const v2rayRules = await parseV2rayRuleFile(path.join(file.path, file.name))
const clashRules = convertV2rayRuleToClashRule(v2rayRules)
const yamlObj = {
payload: Array<string>(),
}
yamlObj.payload = clashRules.payload
const yamlFile = await fs.open(path.join(rulesDistPath, `${file.name}.yaml`), 'w')
const writeStream = yamlFile.createWriteStream()
writeStream.write('# Generated by v2ray2clashrule\n')
if (clashRules.type === 'domain')
writeStream.write('# type: domain\n')
else
writeStream.write('# type: classic\n')
writeStream.write(yamlStringify(yamlObj))
writeStream.end()
writeStream.close()
}
}
await generateRuleList(v2rayRuleDataPath, rulesDistPath)
debugLogger('generate rule list done')
await generateRulePage(rulesDistPath, pageDistPath)
debugLogger('generate rule page done')

0 comments on commit 6cd3e6c

Please sign in to comment.