mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 05:03:56 +08:00
🤞 Sync 2026-09-30 23:44:05
This commit is contained in:
1 parent
e3fde93105
commit
44bb740ab2
206 files changed
+5736
-2799
No files matched your search
@@ -158,7 +158,8 @@ var callListTemplates = rpc.declare({ object: 'luci.clashoo', method: 'list_temp
|
||||
var callUploadTemplate= rpc.declare({ object: 'luci.clashoo', method: 'upload_template', params: ['name', 'content'], expect: {} });
|
||||
var callApplyRewrite = rpc.declare({ object: 'luci.clashoo', method: 'apply_rewrite', params: ['base_type','base_name','rewrite_type','rewrite_name','output_name','set_active'], expect: {} });
|
||||
var callFetchUrl = rpc.declare({ object: 'luci.clashoo', method: 'fetch_rewrite_url', params: ['url','name'], expect: {} });
|
||||
var callApplyTplUrl = rpc.declare({ object: 'luci.clashoo', method: 'apply_template_with_url', params: ['template_source','sub_url','output_name','set_active'], expect: {} });
|
||||
var callApplyTplUrl = rpc.declare({ object: 'luci.clashoo', method: 'apply_template_with_url', params: ['template_source','sub_pairs','output_name','set_active'], expect: {} });
|
||||
var callRegenerate = rpc.declare({ object: 'luci.clashoo', method: 'regenerate_output', params: ['name'], expect: {} });
|
||||
var callMigrateSbProfile = rpc.declare({ object: 'luci.clashoo', method: 'migrate_singbox_profile', params: ['name'], expect: {} });
|
||||
var callSmartFlushCache = rpc.declare({ object: 'luci.clashoo', method: 'smart_flush_cache', expect: {} });
|
||||
var callDetectPrimaryGroup = rpc.declare({ object: 'luci.clashoo', method: 'detect_primary_group', expect: {} });
|
||||
@@ -596,22 +597,24 @@ function createConfigEditor(mode) {
|
||||
return {
|
||||
el: wrap,
|
||||
textarea: ta,
|
||||
setValue: function (content) {
|
||||
setValue: function (content, readOnly) {
|
||||
content = (content == null) ? '' : String(content);
|
||||
ta.readOnly = !!readOnly;
|
||||
if (content.length > LARGE) { showTextarea(content); return; }
|
||||
if (cm) { showCM(content); return; }
|
||||
if (cm) { cm.setOption('readOnly', !!readOnly); showCM(content); return; }
|
||||
loadCodeMirror().then(function () {
|
||||
if (!cm && window.CodeMirror) {
|
||||
cm = window.CodeMirror(host, {
|
||||
value: '', mode: mode,
|
||||
lineNumbers: true, matchBrackets: true,
|
||||
lineWrapping: true, tabSize: 2, indentUnit: 2,
|
||||
viewportMargin: 60
|
||||
viewportMargin: 60,
|
||||
readOnly: !!readOnly
|
||||
});
|
||||
if (getThemeClass().indexOf('dark') >= 0)
|
||||
host.classList.add('cl-cm-dark');
|
||||
}
|
||||
if (cm) showCM(content); else showTextarea(content);
|
||||
if (cm) { cm.setOption('readOnly', !!readOnly); showCM(content); } else showTextarea(content);
|
||||
}).catch(function () { showTextarea(content); });
|
||||
},
|
||||
getValue: function () {
|
||||
@@ -945,7 +948,66 @@ return view.extend({
|
||||
});
|
||||
|
||||
var tplUrlIn = E('input', { type: 'text', 'class': 'cl-sub-url', placeholder: _("Enter remote template URL, e.g. https://raw.githubusercontent.com/…/Clash.yaml") });
|
||||
var subUrlIn = E('input', { type: 'text', 'class': 'cl-sub-url', placeholder: _("Enter subscription URL (injected into template proxy-providers)") });
|
||||
/* ── 多 provider pairs 输入 ── */
|
||||
var pairs = [];
|
||||
var pairsBox = E('div', { 'class': 'cl-pairs-box' });
|
||||
|
||||
function renderPairs() {
|
||||
pairsBox.innerHTML = '';
|
||||
pairs.forEach(function (p, idx) {
|
||||
var nameIn = E('input', {
|
||||
type: 'text', 'class': 'cl-sub-url',
|
||||
style: 'width:120px;flex-shrink:0',
|
||||
placeholder: _('Provider name'), value: p.name || ''
|
||||
});
|
||||
nameIn.addEventListener('input', function () { pairs[idx].name = nameIn.value; });
|
||||
|
||||
var urlIn = E('input', {
|
||||
type: 'text', 'class': 'cl-sub-url',
|
||||
style: 'flex:1;min-width:0',
|
||||
placeholder: _('Subscription URL'), value: p.url || ''
|
||||
});
|
||||
urlIn.addEventListener('input', function () { pairs[idx].url = urlIn.value; });
|
||||
|
||||
var children = [nameIn, urlIn];
|
||||
|
||||
if (idx === 0) {
|
||||
var addBtn = E('button', {
|
||||
'class': 'btn cbi-button cl-btn-sm',
|
||||
style: 'flex-shrink:0'
|
||||
}, '+');
|
||||
if (pairs.length >= 5) {
|
||||
addBtn.setAttribute('disabled', 'disabled');
|
||||
addBtn.style.opacity = '0.5';
|
||||
addBtn.style.cursor = 'not-allowed';
|
||||
}
|
||||
addBtn.addEventListener('click', function () {
|
||||
if (pairs.length >= 5) return;
|
||||
pairs.push({ name: '', url: '' });
|
||||
renderPairs();
|
||||
});
|
||||
children.push(addBtn);
|
||||
} else {
|
||||
var delBtn = E('button', {
|
||||
'class': 'btn cbi-button cl-btn-sm',
|
||||
style: 'flex-shrink:0'
|
||||
}, '−');
|
||||
delBtn.addEventListener('click', function () {
|
||||
pairs.splice(idx, 1);
|
||||
renderPairs();
|
||||
});
|
||||
children.push(delBtn);
|
||||
}
|
||||
|
||||
pairsBox.appendChild(E('div', {
|
||||
'class': 'cl-pair-row',
|
||||
style: 'display:flex;gap:6px;margin-bottom:6px;align-items:center'
|
||||
}, children));
|
||||
});
|
||||
}
|
||||
|
||||
pairs.push({ name: '', url: '' });
|
||||
renderPairs();
|
||||
var outNameIn = E('input', { type: 'text', 'class': 'cl-sub-url', placeholder: _("Output file name (without extension, blank to auto-fill)") });
|
||||
var rwMode = 'local';
|
||||
|
||||
@@ -990,12 +1052,13 @@ return view.extend({
|
||||
|
||||
var rwApply = function (setActive) {
|
||||
var tplSrc = rwMode === 'local' ? tplSel.value : tplUrlIn.value.trim();
|
||||
var subUrl = subUrlIn.value.trim();
|
||||
var out = outNameIn.value.trim();
|
||||
if (!tplSrc) { ui.addNotification(null, E('p', rwMode === 'local' ? _("Please select a local template file") : _("Please enter a remote template URL"))); return; }
|
||||
if (!subUrl) { ui.addNotification(null, E('p', _("Please enter a subscription URL"))); return; }
|
||||
var validPairs = pairs.filter(function (p) { return p.name.trim() && p.url.trim(); });
|
||||
if (validPairs.length === 0) { ui.addNotification(null, E('p', _("Please fill at least one name + URL pair"))); return; }
|
||||
if (!out) { ui.addNotification(null, E('p', _("Please enter an output file name"))); return; }
|
||||
L.resolveDefault(callApplyTplUrl(tplSrc, subUrl, out, setActive ? '1' : '0'), {}).then(function (r) {
|
||||
var pairsJson = JSON.stringify(validPairs);
|
||||
L.resolveDefault(callApplyTplUrl(tplSrc, pairsJson, out, setActive ? '1' : '0'), {}).then(function (r) {
|
||||
ui.addNotification(null, E('p', r && r.success ? (r.message || _("Generated: ") + r.output_name) : (_("Generate failed: ") + (r && r.message || _("Unknown error")))));
|
||||
if (r && r.success) location.reload();
|
||||
});
|
||||
@@ -1019,7 +1082,7 @@ return view.extend({
|
||||
}
|
||||
}, _("Save"));
|
||||
|
||||
var otherEditorBox = E('div', { 'class': 'cl-section cl-card cl-sb-editor' }, [
|
||||
var otherEditorBox = E('div', { 'class': 'cl-section cl-card cl-sb-editor cl-inline-editor' }, [
|
||||
otherEditorTitle,
|
||||
otherEd.el,
|
||||
E('div', { 'class': 'cl-actions cl-sb-row-actions cl-sb-editor-actions' }, [
|
||||
@@ -1027,93 +1090,194 @@ return view.extend({
|
||||
E('span', { 'class': 'cl-hint' }, _("Click Save after editing; switching configuration automatically restarts the service"))
|
||||
])
|
||||
]);
|
||||
var otherEditorHint = otherEditorBox.querySelector('.cl-hint');
|
||||
|
||||
function loadOtherEditor(name, type) {
|
||||
otherEditorTitle.textContent = _("Editing: ") + name;
|
||||
otherSaveBtn.removeAttribute('disabled');
|
||||
|
||||
function loadOtherEditor(name, type, readOnly) {
|
||||
otherEditorTitle.textContent = (readOnly ? _("Viewing: ") : _("Editing: ")) + name;
|
||||
if (readOnly) {
|
||||
otherSaveBtn.style.display = "none";
|
||||
} else {
|
||||
otherSaveBtn.style.display = "";
|
||||
otherSaveBtn.removeAttribute('disabled');
|
||||
}
|
||||
if (otherEditorHint) {
|
||||
otherEditorHint.style.display = "";
|
||||
otherEditorHint.textContent = readOnly
|
||||
? _("Read-only preview; edit the template and click Update to regenerate")
|
||||
: _("Click Save after editing; switching configuration automatically restarts the service");
|
||||
}
|
||||
otherEd.textarea.dataset.name = name;
|
||||
otherEd.textarea.dataset.type = type;
|
||||
otherEd.setValue(_("Loading…"));
|
||||
otherEd.setValue(_("Loading…"), readOnly);
|
||||
L.resolveDefault(callReadOtherConfig(name, type), {}).then(function (r) {
|
||||
otherEd.setValue(r.content || '');
|
||||
otherEd.setValue(r.content || '', readOnly);
|
||||
});
|
||||
}
|
||||
|
||||
var currentEditorCard = null;
|
||||
var toggleOtherEditor = function (f, type, cardEl) {
|
||||
if (currentEditorCard === cardEl && otherEditorBox.parentNode) {
|
||||
otherEditorBox.parentNode.removeChild(otherEditorBox);
|
||||
currentEditorCard = null;
|
||||
return;
|
||||
}
|
||||
if (otherEditorBox.parentNode) otherEditorBox.parentNode.removeChild(otherEditorBox);
|
||||
loadOtherEditor(f.name, type, f.generated === true);
|
||||
cardEl.parentNode.insertBefore(otherEditorBox, cardEl.nextSibling);
|
||||
currentEditorCard = cardEl;
|
||||
setTimeout(function(){
|
||||
var listWrap = cardEl.closest(".cl-file-list") || cardEl.parentNode;
|
||||
var section = listWrap.closest(".cl-section") || listWrap.parentNode;
|
||||
if (listWrap && section) {
|
||||
var cs = getComputedStyle(section);
|
||||
var innerW = section.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
|
||||
var listW = listWrap.clientWidth;
|
||||
var diff = Math.floor((innerW - listW) / 2);
|
||||
if (diff > 0 && innerW > 0) {
|
||||
otherEditorBox.style.width = innerW + "px";
|
||||
otherEditorBox.style.maxWidth = "none";
|
||||
otherEditorBox.style.marginLeft = "-" + diff + "px";
|
||||
otherEditorBox.style.marginRight = "-" + diff + "px";
|
||||
otherEditorBox.style.boxSizing = "border-box";
|
||||
otherEditorBox.style.alignSelf = "stretch";
|
||||
}
|
||||
}
|
||||
try { otherEditorBox.scrollIntoView({behavior:"smooth", block:"nearest"}); } catch(e) {}
|
||||
}, 60);
|
||||
};
|
||||
|
||||
var makeOtherCards = function (files, type) {
|
||||
return files.map(function (f) {
|
||||
var cardEl;
|
||||
var nameNodes = [];
|
||||
if (f.active) nameNodes.push(E('span', { 'class': 'cl-active-badge' }, _("Active")));
|
||||
nameNodes.push(E('span', { 'class': 'cl-file-name-text' }, safeText(f.name)));
|
||||
|
||||
return E('div', { 'class': 'cl-file-item' + (f.active ? ' is-active' : '') }, [
|
||||
var actions = [];
|
||||
|
||||
if (f.generated === true) {
|
||||
actions.push(E('button', {
|
||||
'class': 'btn cbi-button-action cl-btn-sm cl-btn-update-output',
|
||||
disabled: f.active ? 'disabled' : null,
|
||||
style: f.active ? 'opacity:.5;cursor:not-allowed' : '',
|
||||
title: f.active ? _('Active config cannot be updated; switch to another first') : '',
|
||||
click: function (ev) {
|
||||
var btn = ev.target;
|
||||
if (f.active || btn.disabled || clKernelBusy()) return;
|
||||
clKernelLock();
|
||||
btn.disabled = true;
|
||||
btn.textContent = _("Updating…");
|
||||
L.resolveDefault(callRegenerate(f.name), {}).then(function (r) {
|
||||
if (!r || !r.success) {
|
||||
ui.addNotification(null, E('p', _("Update failed: ") + ((r && (r.message || r.error)) || '')));
|
||||
btn.disabled = false;
|
||||
btn.textContent = _("Update");
|
||||
clKernelUnlock();
|
||||
return;
|
||||
}
|
||||
if (!r.sub_pairs || r.sub_pairs.length === 0) {
|
||||
ui.addNotification(null, E('p', _("Update failed: this config has no proxy-providers")));
|
||||
btn.disabled = false;
|
||||
btn.textContent = _("Update");
|
||||
clKernelUnlock();
|
||||
return;
|
||||
}
|
||||
var pairsJson = JSON.stringify(r.sub_pairs);
|
||||
L.resolveDefault(callApplyTplUrl(r.template_source, pairsJson, f.name, f.active ? '1' : '0'), {}).then(function (r2) {
|
||||
ui.addNotification(null, E('p', (r2 && r2.success) ? f.name + _(" updated") : _("Update failed")));
|
||||
location.reload();
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _("Update");
|
||||
clKernelUnlock();
|
||||
});
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _("Update");
|
||||
clKernelUnlock();
|
||||
});
|
||||
}
|
||||
}, _("Update")));
|
||||
}
|
||||
|
||||
actions.push(E('button', {
|
||||
'class': 'btn cbi-button cl-btn-sm cl-btn-edit',
|
||||
click: function (ev) {
|
||||
var btn = ev.target;
|
||||
if (btn.disabled) return;
|
||||
toggleOtherEditor(f, type, cardEl);
|
||||
}
|
||||
}, f.generated === true ? _("Show/Hide") : _("Edit")));
|
||||
|
||||
actions.push(E('button', {
|
||||
'class': 'btn cbi-button cl-btn-sm cl-btn-switch',
|
||||
click: function (ev) {
|
||||
var btn = ev.target;
|
||||
if (btn.disabled || clKernelBusy()) return;
|
||||
clKernelLock();
|
||||
btn.disabled = true;
|
||||
btn.textContent = _("Loading…");
|
||||
L.resolveDefault(callSetConfig(f.name), {}).then(function () {
|
||||
location.reload();
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _("Switch Profile");
|
||||
clKernelUnlock();
|
||||
});
|
||||
}
|
||||
}, _("Switch Profile")));
|
||||
|
||||
actions.push(E('button', {
|
||||
'class': 'btn cbi-button cl-btn-sm cl-btn-delete',
|
||||
click: function (ev) {
|
||||
var btn = ev.target;
|
||||
if (btn.disabled || clKernelBusy()) return;
|
||||
if (!confirm(_("Delete ") + f.name + '?')) return;
|
||||
clKernelLock();
|
||||
btn.disabled = true;
|
||||
btn.textContent = _("Loading…");
|
||||
L.resolveDefault(callDeleteCfg(f.name, type), {}).then(function () {
|
||||
location.reload();
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _("Delete");
|
||||
clKernelUnlock();
|
||||
});
|
||||
}
|
||||
}, _("Delete")));
|
||||
|
||||
cardEl = E('div', { 'class': 'cl-file-item' + (f.active ? ' is-active' : '') }, [
|
||||
E('div', { 'class': 'cl-file-meta' }, [
|
||||
E('div', { 'class': 'cl-file-name' }, nameNodes),
|
||||
E('div', { 'class': 'cl-file-size' }, safeText(f.size))
|
||||
]),
|
||||
E('div', { 'class': 'cl-file-actions' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cl-btn-sm cl-btn-edit',
|
||||
click: function (ev) {
|
||||
var btn = ev.target;
|
||||
if (btn.disabled) return;
|
||||
btn.disabled = true;
|
||||
var orig = btn.textContent;
|
||||
btn.textContent = _("Loading…");
|
||||
Promise.resolve(loadOtherEditor(f.name, type)).then(function () {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
});
|
||||
}
|
||||
}, _("Edit")),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cl-btn-sm cl-btn-switch',
|
||||
click: function (ev) {
|
||||
var btn = ev.target;
|
||||
if (btn.disabled || clKernelBusy()) return;
|
||||
clKernelLock();
|
||||
btn.disabled = true;
|
||||
btn.textContent = _("Loading…");
|
||||
L.resolveDefault(callSetConfig(f.name), {}).then(function () {
|
||||
location.reload();
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _("Switch Profile");
|
||||
clKernelUnlock();
|
||||
});
|
||||
}
|
||||
}, _("Switch Profile")),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cl-btn-sm cl-btn-delete',
|
||||
click: function (ev) {
|
||||
var btn = ev.target;
|
||||
if (btn.disabled || clKernelBusy()) return;
|
||||
if (!confirm(_("Delete ") + f.name + '?')) return;
|
||||
clKernelLock();
|
||||
btn.disabled = true;
|
||||
btn.textContent = _("Loading…");
|
||||
L.resolveDefault(callDeleteCfg(f.name, type), {}).then(function () {
|
||||
location.reload();
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _("Delete");
|
||||
clKernelUnlock();
|
||||
});
|
||||
}
|
||||
}, _("Delete"))
|
||||
])
|
||||
E('div', { 'class': 'cl-file-actions' }, actions)
|
||||
]);
|
||||
return cardEl;
|
||||
});
|
||||
};
|
||||
|
||||
var otherFiles = (upFiles || []).map(function(f){ return {f:f, t:'2'}; })
|
||||
.concat((customFiles || []).map(function(f){ return {f:f, t:'3'}; }));
|
||||
var tplOnlyFiles = (customFiles || []).filter(function(f){ return f.generated !== true; });
|
||||
var genCustomFiles = (customFiles || []).filter(function(f){ return f.generated === true; });
|
||||
|
||||
var otherCards = otherFiles.map(function(o) {
|
||||
return makeOtherCards([o.f], o.t)[0];
|
||||
});
|
||||
var tplCards = tplOnlyFiles.map(function(f){ return makeOtherCards([f], '3')[0]; });
|
||||
var genCards = (upFiles || []).map(function(f){ return makeOtherCards([f], '2')[0]; })
|
||||
.concat(genCustomFiles.map(function(f){ return makeOtherCards([f], '3')[0]; }));
|
||||
|
||||
var tplListBox = tplCards.length ? E('div', { 'class': 'cl-section cl-card' }, [
|
||||
E("h4", {}, _("Template Selection")),
|
||||
E('div', { 'class': 'cl-fixed-600' }, [
|
||||
E('div', { 'class': 'cl-file-list' }, tplCards)
|
||||
])
|
||||
]) : null;
|
||||
|
||||
var genListBox = genCards.length ? E('div', { 'class': 'cl-section cl-card' }, [
|
||||
E('h4', {}, _("Other Configuration Files (upload / overwrite output)")),
|
||||
E('div', { 'class': 'cl-fixed-600' }, [
|
||||
E('div', { 'class': 'cl-file-list' }, genCards)
|
||||
])
|
||||
]) : null;
|
||||
|
||||
var sections = [
|
||||
E('div', { 'class': 'cl-section cl-card' }, [
|
||||
@@ -1138,15 +1302,8 @@ return view.extend({
|
||||
])
|
||||
];
|
||||
|
||||
if (otherFiles.length) {
|
||||
sections.push(E('div', { 'class': 'cl-section cl-card' }, [
|
||||
E('h4', {}, _("Other Configuration Files (upload / overwrite output)")),
|
||||
E('div', { 'class': 'cl-fixed-600' }, [
|
||||
E('div', { 'class': 'cl-file-list' }, otherCards)
|
||||
])
|
||||
]));
|
||||
sections.push(otherEditorBox);
|
||||
}
|
||||
if (genListBox) sections.push(genListBox);
|
||||
if (tplListBox) sections.push(tplListBox);
|
||||
|
||||
sections.push(
|
||||
E('div', { 'class': 'cl-section cl-card' }, [
|
||||
@@ -1161,8 +1318,8 @@ return view.extend({
|
||||
E('div', { 'class': 'cl-rw-divider' }),
|
||||
E('div', { 'class': 'cl-rewrite-group cl-rewrite-group-input' }, [
|
||||
E('div', { 'class': 'cl-rewrite-group-title' }, _("Information Input")),
|
||||
subUrlIn,
|
||||
outNameIn,
|
||||
pairsBox,
|
||||
E('div', { 'class': 'cl-actions cl-rewrite-actions' }, [
|
||||
E('button', { 'class': 'btn cbi-button cl-btn-sm', click: function(){ rwApply(false); } }, _("Generate Configuration")),
|
||||
E('button', { 'class': 'btn cbi-button-action cl-btn-sm cl-btn-generate-switch', click: function(){ rwApply(true); } }, _("Apply Configuration"))
|
||||
@@ -1414,7 +1571,7 @@ return view.extend({
|
||||
|
||||
o = s.option(form.DynamicList, 'fake_ip_filter', _("Fake-IP Filter Domains"));
|
||||
o.placeholder = '+.lan / geosite:cn / RULE-SET,cn_domain,real-ip';
|
||||
o.description = _("Blocklist mode: enter domains or shorthand like <code>geosite:cn</code>. Rule mode: entries are converted to rule syntax automatically, such as <code>GEOSITE,cn,real-ip</code> / <code>RULE-SET,xxx,real-ip</code>; usually add <code>MATCH,fake-ip</code> at the end as fallback. <code>geosite:cn</code> automatically uses built-in cn.mrs acceleration and avoids loading the 10MB geosite.dat. Allowlist mode drops the shipped defaults (they mean the opposite there), so enter your own domains.");
|
||||
o.description = _("Blocklist mode: enter domains or shorthand like <code>geosite:cn</code>. Rule mode: entries are converted to rule syntax automatically, such as <code>RULE-SET,xxx,real-ip</code> / <code>DOMAIN-SUFFIX,xxx,real-ip</code>; usually add <code>MATCH,fake-ip</code> at the end as fallback. <code>geosite:cn</code> becomes <code>RULE-SET,cn_domain,real-ip</code> via built-in cn.mrs acceleration (other <code>geosite:xxx</code> becomes <code>GEOSITE,xxx,real-ip</code>). Allowlist mode drops the shipped defaults (they mean the opposite there), so enter your own domains.");
|
||||
o.depends('enhanced_mode', 'fake-ip');
|
||||
o.remove = function () {};
|
||||
o = s.option(form.DynamicList, 'default_nameserver', 'Bootstrap DNS');
|
||||
|
||||
Reference in new issue
Block a user