mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-26 07:44:59 +03:00
5e15120cec
* feat(happ): make ad blocking optional in routing presets Add an independent AdBlock toggle for Iran, China, and global presets, applied only when generating routing rules. Update the China preset to Bypass-CN and cover preset behavior and localized controls. * feat: add visual routing editor with JSON support and localization updates - Implemented a new modal for editing routing profiles with basic and advanced tabs. - Added functionality to load, parse, and generate routing profiles in JSON format. - Enhanced user experience with validation and error handling for JSON input. - Updated translations for Russian, Turkish, Ukrainian, Vietnamese, Chinese (Simplified and Traditional) to include new routing editor terms. - Created helper functions for managing routing profiles and generating deep links. - Added unit tests for routing editor functionalities and JSON handling. * feat: update routing editor to preserve null lists in profiles and enhance validation messages
91 lines
2.9 KiB
TypeScript
91 lines
2.9 KiB
TypeScript
import { HappRoutingProfileSchema, type HappRoutingProfile } from '@/schemas/happRouting';
|
|
import { toBase64Utf8 } from './happPresets';
|
|
import { isRemoteRoutingSource } from './subscriptionShared';
|
|
|
|
export type HappRoutingMode = 'add' | 'onadd';
|
|
export type HappRoutingListKey =
|
|
| 'DirectSites'
|
|
| 'DirectIp'
|
|
| 'ProxySites'
|
|
| 'ProxyIp'
|
|
| 'BlockSites'
|
|
| 'BlockIp';
|
|
|
|
export type HappRoutingLoadResult =
|
|
| { success: true; profile: HappRoutingProfile; mode: HappRoutingMode; isNew: boolean }
|
|
| { success: false; error: 'off' | 'remote' | 'invalid' };
|
|
|
|
export function parseHappRoutingJson(input: string): HappRoutingProfile | null {
|
|
try {
|
|
const value: unknown = JSON.parse(input);
|
|
const result = HappRoutingProfileSchema.safeParse(value);
|
|
// Keep the validated input object; schema output can discard some unknown extension keys.
|
|
return result.success ? (value as HappRoutingProfile) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function loadHappRouting(input: string): HappRoutingLoadResult {
|
|
const source = input.trim();
|
|
if (!source) {
|
|
return {
|
|
success: true,
|
|
profile: {
|
|
Name: 'Custom Rules',
|
|
GlobalProxy: 'true',
|
|
DirectSites: [],
|
|
DirectIp: [],
|
|
ProxySites: [],
|
|
ProxyIp: [],
|
|
BlockSites: [],
|
|
BlockIp: [],
|
|
DomainStrategy: 'IPIfNonMatch',
|
|
},
|
|
mode: 'onadd',
|
|
isNew: true,
|
|
};
|
|
}
|
|
if (source === 'happ://routing/off') return { success: false, error: 'off' };
|
|
if (isRemoteRoutingSource(source)) return { success: false, error: 'remote' };
|
|
|
|
if (source.startsWith('{')) {
|
|
const profile = parseHappRoutingJson(source);
|
|
return profile
|
|
? { success: true, profile, mode: 'onadd', isNew: false }
|
|
: { success: false, error: 'invalid' };
|
|
}
|
|
|
|
const match = /^happ:\/\/routing\/(onadd|add)\/([A-Za-z0-9+/_-]+={0,2})$/.exec(source);
|
|
if (!match) return { success: false, error: 'invalid' };
|
|
|
|
try {
|
|
const binary = atob(match[2].replace(/-/g, '+').replace(/_/g, '/'));
|
|
// Decode UTF-8 strictly so malformed bytes cannot silently change profile names or rules.
|
|
const json = new TextDecoder('utf-8', { fatal: true }).decode(
|
|
Uint8Array.from(binary, (character) => character.charCodeAt(0)),
|
|
);
|
|
const profile = parseHappRoutingJson(json);
|
|
return profile
|
|
? { success: true, profile, mode: match[1] as HappRoutingMode, isNew: false }
|
|
: { success: false, error: 'invalid' };
|
|
} catch {
|
|
return { success: false, error: 'invalid' };
|
|
}
|
|
}
|
|
|
|
export function buildHappRoutingDeeplink(
|
|
profile: HappRoutingProfile,
|
|
mode: HappRoutingMode = 'onadd',
|
|
): string {
|
|
return `happ://routing/${mode}/` + toBase64Utf8(JSON.stringify(profile));
|
|
}
|
|
|
|
export function parseHappRoutingList(input: string): string[] {
|
|
// Commas can belong to regexp rules, so the editor uses one rule per line.
|
|
return input
|
|
.split(/\r?\n/)
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean);
|
|
}
|