mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-21 15:22:20 +02:00
Merge pull request #239 from euzu/feature/ui_refactor_user_view
ui user view refactor, added status filter fixed grace tiem for connections
This commit is contained in:
@@ -111,7 +111,9 @@
|
||||
"PATTERN_IPV6": "Pattern IPv6",
|
||||
"IPv4": "IPv4",
|
||||
"IPv6": "IPv6",
|
||||
"IP": "IP"
|
||||
"IP": "IP",
|
||||
"TARGET": "Target",
|
||||
"TARGETS": "Targets"
|
||||
},
|
||||
"TITLE": {
|
||||
"USER_BOUQUET_EDITOR": "User group editor"
|
||||
@@ -120,6 +122,7 @@
|
||||
"NO_CONTENT": "No content",
|
||||
"INVALID_REGEXP": "Invalid regexp",
|
||||
"INVALID_FILETYPE": "Invalid filetype",
|
||||
"TARGET_NOT_EXISTS": "Target does not exist",
|
||||
"DOWNLOAD": {
|
||||
"SUCCESS": "Successfully downloaded",
|
||||
"FAIL": "Failed to download!",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
color: var(--app-header-color);
|
||||
align-content: center;
|
||||
padding: 10px;
|
||||
max-width: var(--max-view-width) !important;
|
||||
@media (max-width: common.$media-size-mobile) {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
flex: 0 0 auto;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
max-width: var(--max-view-width);
|
||||
background-color: inherit;
|
||||
|
||||
.source-selector {
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
@include common.container_horz_no_padding();
|
||||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
max-width: var(--max-view-width) !important;
|
||||
|
||||
&__content {
|
||||
@include common.container_horz_nowrap(flex-start, 0);
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
&__panels {
|
||||
|
||||
@@ -34,10 +34,17 @@ export default function StatusView() {
|
||||
}, [services])
|
||||
|
||||
useEffect(() => {
|
||||
const sub = interval(REQUEST_INTERVAL).subscribe(() => services.status().getServerStatus().pipe(first()).subscribe((data: any) => setStatus(data)));
|
||||
const fetchData = () => services.status().getServerStatus().pipe(first()).subscribe({
|
||||
next: (data: any) => setStatus(data),
|
||||
error: () => {
|
||||
}
|
||||
});
|
||||
fetchData();
|
||||
const sub = interval(REQUEST_INTERVAL).subscribe(fetchData);
|
||||
checkIp();
|
||||
return () => sub.unsubscribe();
|
||||
}, [services, checkIp]);
|
||||
}, [services, checkIp])
|
||||
|
||||
|
||||
return <div className={'status-view'}>
|
||||
{ipCheck && <>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Credentials} from "../../model/server-config";
|
||||
import useTranslator from "../../hook/use-translator";
|
||||
import FormView, {FormFieldType} from "../form-view/from-view";
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
export const STATUS_OPTIONS = [
|
||||
{value: 'Active', label: 'Active'},
|
||||
{value: 'Expired', label: 'Expired'},
|
||||
{value: 'Banned', label: 'Banned'},
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@use '../../scss/common';
|
||||
@use '../../scss/preferences';
|
||||
|
||||
.label {
|
||||
color: var(--label-color);
|
||||
}
|
||||
|
||||
.proxy-type {
|
||||
&-reverse {
|
||||
}
|
||||
@@ -49,12 +53,8 @@
|
||||
}
|
||||
label {
|
||||
font-weight: bold;
|
||||
|
||||
&:not(.target-not-exists) {
|
||||
color: var(--label-color);
|
||||
}
|
||||
|
||||
&.target-not-exists {
|
||||
color: var(--label-color);
|
||||
span.target-not-exists {
|
||||
color: var(--attention-color);
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,10 @@
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
padding: 8px;
|
||||
gap: 12px;
|
||||
span {
|
||||
color: var(--label-color);
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
@@ -143,6 +147,9 @@
|
||||
&-col-header {
|
||||
padding: 8px;
|
||||
background-color: var(--background-color);
|
||||
&-tools {
|
||||
width: 64px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-col {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {JSX, ReactNode, useCallback, useEffect, useRef, useState} from "react";
|
||||
import React, {JSX, useCallback, useEffect, useRef, useState} from "react";
|
||||
import './user-view.scss';
|
||||
import ServerConfig, {Credentials, TargetUser} from "../../model/server-config";
|
||||
import {getIconByName} from "../../icons/icons";
|
||||
@@ -9,7 +9,7 @@ import TabSet, {TabSetTab} from "../tab-set/tab-set";
|
||||
import useTranslator from "../../hook/use-translator";
|
||||
import DateUtils from "../../utils/date-utils";
|
||||
import PlaylistFilter from "../playlist-filter/playlist-filter";
|
||||
import UserEditor from "../user-editor/user-editor";
|
||||
import UserEditor, {STATUS_OPTIONS} from "../user-editor/user-editor";
|
||||
import TextGenerator from "../../utils/text-generator";
|
||||
|
||||
const renderExpDate = (value: any, hidden?: boolean) => {
|
||||
@@ -20,7 +20,8 @@ const renderExpDate = (value: any, hidden?: boolean) => {
|
||||
};
|
||||
|
||||
const renderBool = (value: any, hidden?: boolean) => {
|
||||
return <span className={'checkbox-' + (value ? 'checked' : 'unchecked') }>{getIconByName(value ? 'CheckMark' : 'Clear' )}</span>;
|
||||
return <span
|
||||
className={'checkbox-' + (value ? 'checked' : 'unchecked')}>{getIconByName(value ? 'CheckMark' : 'Clear')}</span>;
|
||||
};
|
||||
|
||||
const renderMaxCon = (value: any, hidden?: boolean) => {
|
||||
@@ -79,8 +80,8 @@ const prepareTargetUserForSave = (targetUser: TargetUser[]): TargetUser[] => {
|
||||
return targetUser.map((user) => {
|
||||
let storeUser = {...user, credentials: user.credentials.map(c => ({...c}))};
|
||||
storeUser.credentials.forEach((credential) => {
|
||||
credential.exp_date = DateUtils.toUnixSeconds(credential.exp_date);
|
||||
credential.created_at = DateUtils.toUnixSeconds(credential.created_at);
|
||||
credential.exp_date = DateUtils.toUnixSeconds(credential.exp_date);
|
||||
credential.created_at = DateUtils.toUnixSeconds(credential.created_at);
|
||||
});
|
||||
return storeUser;
|
||||
});
|
||||
@@ -130,19 +131,19 @@ const createNewUser = (targets: TargetUser[]): Credentials => {
|
||||
}
|
||||
const created_at = Date.now();
|
||||
const exp_date = new Date();
|
||||
exp_date.setFullYear(exp_date.getFullYear()+1);
|
||||
exp_date.setFullYear(exp_date.getFullYear() + 1);
|
||||
return {
|
||||
username,
|
||||
password: TextGenerator.generatePassword(),
|
||||
token: TextGenerator.generatePassword(),
|
||||
proxy: 'reverse',
|
||||
created_at,
|
||||
exp_date: exp_date.getTime(),
|
||||
max_connections: 1,
|
||||
status: "Active",
|
||||
ui_enabled: true,
|
||||
// @ts-ignore
|
||||
_ref: undefined, // an indicator for new user
|
||||
username,
|
||||
password: TextGenerator.generatePassword(),
|
||||
token: TextGenerator.generatePassword(),
|
||||
proxy: 'reverse',
|
||||
created_at,
|
||||
exp_date: exp_date.getTime(),
|
||||
max_connections: 1,
|
||||
status: "Active",
|
||||
ui_enabled: true,
|
||||
// @ts-ignore
|
||||
_ref: undefined, // an indicator for new user
|
||||
};
|
||||
}
|
||||
|
||||
@@ -165,6 +166,7 @@ export default function UserView(props: UserViewProps) {
|
||||
const [filteredUser, setFilteredUser] = useState<Record<string, {
|
||||
filter: string,
|
||||
regexp: boolean,
|
||||
status: undefined,
|
||||
user: Credentials[]
|
||||
}>>({});
|
||||
|
||||
@@ -291,32 +293,44 @@ export default function UserView(props: UserViewProps) {
|
||||
setShowHiddenFields((hiddenFields: any) => ({...hiddenFields, [key]: !hiddenFields[key]}));
|
||||
}, []);
|
||||
|
||||
const handleFilter = useCallback((filter: string, regexp: boolean): void => {
|
||||
let filter_value = regexp ? filter : filter.toLowerCase();
|
||||
const target = targets.find(t => t.target === activeTarget);
|
||||
|
||||
const filterTarget = useCallback((target: TargetUser, filter: string, regexp: boolean, filterForStatus: string): void => {
|
||||
if (target) {
|
||||
if (filter_value?.length) {
|
||||
const filtered = target.credentials.filter((credential: Credentials) => {
|
||||
if (filter?.length) {
|
||||
let filtered = target.credentials.filter((credential: Credentials) => {
|
||||
if (regexp) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return credential.username.trim().match(filter_value) != undefined;
|
||||
return credential.username.trim().match(filter) != undefined;
|
||||
} else {
|
||||
return (credential.username.trim().toLowerCase().indexOf(filter_value) > -1);
|
||||
return (credential.username.trim().toLowerCase().indexOf(filter) > -1);
|
||||
}
|
||||
}) ?? [];
|
||||
if (filterForStatus) {
|
||||
filtered = filtered.filter((c: Credentials) => c.status === filterForStatus);
|
||||
}
|
||||
setFilteredUser((filteredUser: any) => ({
|
||||
...filteredUser,
|
||||
[activeTarget]: {filter, regexp, user: filtered}
|
||||
[target.target]: {filter, regexp, status: filterForStatus, user: filtered}
|
||||
}));
|
||||
} else {
|
||||
let filtered = undefined;
|
||||
if (filterForStatus) {
|
||||
filtered = {filter: undefined, regexp: false, status: filterForStatus, user: target.credentials.filter((c: Credentials) => c.status === filterForStatus)};
|
||||
}
|
||||
setFilteredUser((filteredUser: any) => ({
|
||||
...filteredUser, [activeTarget]: undefined
|
||||
...filteredUser, [target.target]: filtered
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
}, [activeTarget, targets]);
|
||||
}, []);
|
||||
|
||||
const handleFilter = useCallback((filter: string, regexp: boolean): void => {
|
||||
let filter_value = regexp ? filter : filter?.toLowerCase();
|
||||
const target = targets.find(t => t.target === activeTarget);
|
||||
if (target) {
|
||||
let currentFilter = filteredUser[target.target];
|
||||
filterTarget(target, filter_value, regexp, currentFilter?.status);
|
||||
}
|
||||
}, [activeTarget, targets, filterTarget, filteredUser]);
|
||||
|
||||
const handleUserEditorSubmit = useCallback((user: Credentials, target_name: string): boolean => {
|
||||
const userRef = (user as any)._ref;
|
||||
@@ -349,14 +363,25 @@ export default function UserView(props: UserViewProps) {
|
||||
const renderTargetOptions = useCallback((target: string): JSX.Element => {
|
||||
let options = targetOptions?.[target];
|
||||
if (options?.force_redirect?.length > 2) {
|
||||
return <div className={'user__target-target-options'}>{translate('HINT.CONFIG.USER.TARGET_FORCE_REDIRECT')} {options.force_redirect}</div>;
|
||||
return <div
|
||||
className={'user__target-target-options'}>{translate('HINT.CONFIG.USER.TARGET_FORCE_REDIRECT')} {options.force_redirect}</div>;
|
||||
}
|
||||
return <></>;
|
||||
}, [targetOptions, translate]);
|
||||
|
||||
return <div className={'user'}>
|
||||
const handleStatusFilter = useCallback((evt: any) => {
|
||||
const target_name = evt.target.dataset.target;
|
||||
let target = targets.find((t) => t.target === target_name);
|
||||
if (target) {
|
||||
let value = evt.target.value;
|
||||
value = value?.length ? value : undefined;
|
||||
let currentFilter = filteredUser[target_name];
|
||||
filterTarget(target, currentFilter?.filter, !!currentFilter?.regexp, value)
|
||||
}
|
||||
}, [filteredUser, filterTarget, targets]);
|
||||
|
||||
<div className={'user__toolbar'}><label>{translate('LABEL.USER')}</label>
|
||||
return <div className={'user'}>
|
||||
<div className={'user__toolbar'}><label>{translate('LABEL.TARGETS')} / {translate('LABEL.USER')}</label>
|
||||
<PlaylistFilter onFilter={handleFilter} options={filteredUser[activeTarget]}></PlaylistFilter>
|
||||
<button data-tooltip='LABEL.SAVE' onClick={handleSave}>{translate('LABEL.SAVE')}</button>
|
||||
</div>
|
||||
@@ -367,10 +392,18 @@ export default function UserView(props: UserViewProps) {
|
||||
targets?.map(target => <div key={target.target}
|
||||
className={'user__target' + (activeTarget !== target.target ? ' hidden' : '')}>
|
||||
<div className={'user__target-target'}>
|
||||
<label className={(target as any).src ? '' : 'target-not-exists'}>
|
||||
{target.target}
|
||||
<label>
|
||||
{!(target as any).src &&
|
||||
<span className={'target-not-exists'}>{translate('MESSAGES.TARGET_NOT_EXISTS')}</span>}
|
||||
{renderTargetOptions(target.target)}
|
||||
</label>
|
||||
|
||||
<div className={'label'}>{translate("LABEL.STATUS")}</div>
|
||||
<select data-target={target.target} onChange={handleStatusFilter} defaultValue={undefined}>
|
||||
<option value={undefined}></option>
|
||||
{STATUS_OPTIONS.map(l => <option key={target.target + l.value} value={l.value}>{l.label}</option>)}
|
||||
</select>
|
||||
|
||||
<div className={'user__target-target-toolbar'}>
|
||||
<button data-tooltip={'New User'} data-target={target.target}
|
||||
onClick={handleUserAdd}>{getIconByName('PersonAdd')}</button>
|
||||
@@ -380,14 +413,25 @@ export default function UserView(props: UserViewProps) {
|
||||
<div className={'user__target-user-table-container'}>
|
||||
<div className={'user__target-user-table'}>
|
||||
<div className={'user__target-user-row user__target-user-table-header'}>
|
||||
<div
|
||||
className={'user__target-user-col user__target-user-col-header user__target-user-col-header-tools'}></div>
|
||||
{COLUMNS.map(col => <div key={col.field}
|
||||
className={'user__target-user-col user__target-user-col-header'}>
|
||||
<label>{translate(col.label)}</label></div>)}
|
||||
<div className={'user__target-user-col user__target-user-col-header'}></div>
|
||||
</div>
|
||||
|
||||
{(filteredUser[target.target]?.user ?? target.credentials).map((usr, idx) =>
|
||||
<div key={'credential' + idx} className={'user__target-user-row'}>
|
||||
<div className={'user__target-user-col-toolbar'}>
|
||||
<span data-target={target.target} data-user={usr.username}
|
||||
onClick={handleUserRemove}>
|
||||
{getIconByName('PersonRemove')}
|
||||
</span>
|
||||
<span data-target={target.target} data-user={usr.username}
|
||||
onClick={handleUserEdit}>
|
||||
{getIconByName('Edit')}
|
||||
</span>
|
||||
</div>
|
||||
{COLUMNS.map(c => <div
|
||||
key={'target_' + target.target + '_' + c.field + '_' + usr.username}
|
||||
className={'user__target-user-col'}>
|
||||
@@ -402,16 +446,6 @@ export default function UserView(props: UserViewProps) {
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
<div className={'user__target-user-col-toolbar'}>
|
||||
<span data-target={target.target} data-user={usr.username}
|
||||
onClick={handleUserRemove}>
|
||||
{getIconByName('PersonRemove')}
|
||||
</span>
|
||||
<span data-target={target.target} data-user={usr.username}
|
||||
onClick={handleUserEdit}>
|
||||
{getIconByName('Edit')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -81,7 +81,7 @@ $media-size-mobile: 600px;
|
||||
border: 1px solid var(--text-button-border-color);
|
||||
font-size: 1.2rem;
|
||||
border-radius: var(--text-button-border-radius);
|
||||
padding: 8px 24px;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--text-button-background-color);
|
||||
color: var(--text-button-color);
|
||||
&:focus,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
:root {
|
||||
--max-view-width: 1920px;
|
||||
--media-size-mobile: 600px;
|
||||
--text-color: #ffffff;
|
||||
--hint-color: #888888;
|
||||
|
||||
@@ -9,14 +9,14 @@ use crate::api::model::provider_config::{ProviderConfig, ProviderConfigWrapper};
|
||||
use crate::utils::{default_grace_period_millis, default_grace_period_timeout_secs};
|
||||
|
||||
pub struct ProviderConnectionGuard {
|
||||
manager: Arc<ActiveProviderManager>,
|
||||
// manager: Arc<ActiveProviderManager>,
|
||||
allocation: ProviderAllocation,
|
||||
}
|
||||
|
||||
impl ProviderConnectionGuard {
|
||||
pub fn new(manager: Arc<ActiveProviderManager>, allocation: ProviderAllocation) -> Self {
|
||||
pub fn new(_manager: Arc<ActiveProviderManager>, allocation: ProviderAllocation) -> Self {
|
||||
Self {
|
||||
manager,
|
||||
// manager,
|
||||
allocation,
|
||||
}
|
||||
}
|
||||
@@ -54,10 +54,11 @@ impl Drop for ProviderConnectionGuard {
|
||||
ProviderAllocation::Exhausted => {}
|
||||
ProviderAllocation::Available(config) |
|
||||
ProviderAllocation::GracePeriod(config) => {
|
||||
let manager = self.manager.clone();
|
||||
// let manager = self.manager.clone();
|
||||
let provider_config = Arc::clone(config);
|
||||
tokio::spawn(async move {
|
||||
manager.release_connection(&provider_config.name).await;
|
||||
provider_config.release().await;
|
||||
// manager.release_connection(&provider_config.name).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
const USER_CON_TTL: u64 = 10_800; // 3 hours
|
||||
|
||||
pub struct UserConnectionGuard {
|
||||
manager: Arc<ActiveUserManager>,
|
||||
username: String,
|
||||
@@ -149,7 +151,12 @@ impl ActiveUserManager {
|
||||
}
|
||||
|
||||
pub async fn active_connections(&self) -> usize {
|
||||
self.user.read().await.values().map(|c| c.connections as usize).sum()
|
||||
Self::get_active_connections(&self.user).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn get_active_connections(user: &Arc<RwLock<HashMap<String, UserConnectionData>>>) -> usize {
|
||||
user.read().await.values().map(|c| c.connections as usize).sum()
|
||||
}
|
||||
|
||||
pub async fn add_connection(&self, username: &str, max_connections: u32) -> UserConnectionGuard {
|
||||
@@ -162,7 +169,7 @@ impl ActiveUserManager {
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
self.log_active_user().await;
|
||||
self.log_active_user();
|
||||
|
||||
UserConnectionGuard {
|
||||
manager: Arc::new(self.clone_inner()),
|
||||
@@ -176,19 +183,16 @@ impl ActiveUserManager {
|
||||
if connection_data.connections > 0 {
|
||||
connection_data.connections -= 1;
|
||||
}
|
||||
// DO NOT reset granted_grace or grace_ts here!
|
||||
// We must preserve the grace period state until connection_permission() checks it.
|
||||
|
||||
// if connection_data.connections == 0 {
|
||||
// lock.remove(username);
|
||||
// } else
|
||||
// if connection_data.connections < connection_data.max_connections {
|
||||
// connection_data.token = None;
|
||||
// }
|
||||
if connection_data.connections == 0 || connection_data.connections < connection_data.max_connections {
|
||||
// Grace timeout expired, reset grace counters
|
||||
connection_data.granted_grace = false;
|
||||
connection_data.grace_ts = 0;
|
||||
}
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
self.log_active_user().await;
|
||||
self.log_active_user();
|
||||
}
|
||||
|
||||
fn find_user_session(token: u32, sessions: &[UserSession]) -> Option<&UserSession> {
|
||||
@@ -257,23 +261,27 @@ impl ActiveUserManager {
|
||||
None
|
||||
}
|
||||
|
||||
async fn log_active_user(&self) {
|
||||
fn log_active_user(&self) {
|
||||
if self.log_active_user {
|
||||
let user_count = self.active_users().await;
|
||||
let user_connection_count = self.active_connections().await;
|
||||
info!("Active Users: {user_count}, Active User Connections: {user_connection_count}");
|
||||
let user = Arc::clone(&self.user);
|
||||
tokio::spawn(async move {
|
||||
let user_count = user.read().await.len();
|
||||
let user_connection_count = Self::get_active_connections(&user).await;
|
||||
info!("Active Users: {user_count}, Active User Connections: {user_connection_count}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn gc(&self) {
|
||||
if let Some(gc_ts) = &self.gc_ts {
|
||||
let ts = gc_ts.load(Ordering::SeqCst);
|
||||
if current_time_secs() - ts > 10_800 { // 3 hours
|
||||
let now = current_time_secs();
|
||||
if now - ts > USER_CON_TTL {
|
||||
let mut lock = self.user.write().await;
|
||||
for (_, connection_data) in lock.iter_mut() {
|
||||
connection_data.sessions.retain(|s| current_time_secs() - s.ts < 10_800);
|
||||
connection_data.sessions.retain(|s| now - s.ts < USER_CON_TTL);
|
||||
}
|
||||
gc_ts.store(current_time_secs(), Ordering::SeqCst);
|
||||
gc_ts.store(now, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,12 +182,15 @@ impl ProviderConfig {
|
||||
}
|
||||
|
||||
pub async fn release(&self) {
|
||||
// DO NOT reset granted_grace or grace_ts here!
|
||||
// We must preserve the grace period state until allocate() checks it.
|
||||
let mut guard = self.connection.write().await;
|
||||
if guard.current_connections > 0 {
|
||||
guard.current_connections -= 1;
|
||||
}
|
||||
|
||||
if guard.current_connections == 0 || guard.current_connections < self.max_connections {
|
||||
guard.granted_grace = false;
|
||||
guard.grace_ts = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -61,9 +61,10 @@ impl IpCheckConfig {
|
||||
return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "No url provided!".to_owned()));
|
||||
}
|
||||
|
||||
if self.url.is_some() && (self.url_ipv4.is_some() || self.url_ipv6.is_some()) {
|
||||
return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "url in combination with ipv4 and/or ipv6 url not allowed!".to_owned()));
|
||||
}
|
||||
// TODO allow or do not allow ?
|
||||
// if self.url.is_some() && (self.url_ipv4.is_some() || self.url_ipv6.is_some()) {
|
||||
// return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "url in combination with ipv4 and/or ipv6 url not allowed!".to_owned()));
|
||||
// }
|
||||
|
||||
if let Some(p4) = &self.pattern_ipv4 {
|
||||
self.t_pattern_ipv4 = Some(
|
||||
|
||||
@@ -53,7 +53,6 @@ async fn fetch_combined_ips(client: &Arc<Client>, config: &IpCheckConfig, url: &
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Fetch both IPv4 and IPv6 addresses, using separate or combined URL(s)
|
||||
pub async fn get_ips(client: &Arc<Client>, config: &IpCheckConfig) -> Result<(Option<String>, Option<String>), M3uFilterError> {
|
||||
match (&config.url_ipv4, &config.url_ipv6, &config.url) {
|
||||
|
||||
Reference in New Issue
Block a user