432 lines
15 KiB
JavaScript
432 lines
15 KiB
JavaScript
const settingsState = {
|
|
accounts: [],
|
|
activeAccount: null,
|
|
dashboard: null,
|
|
auth: null,
|
|
continuous: null,
|
|
};
|
|
|
|
const ACTIVE_ACCOUNT_STORAGE_KEY = 'telegramScraper.activeAccount';
|
|
|
|
async function api(path, options = {}) {
|
|
const response = await fetch(path, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
...options,
|
|
});
|
|
const data = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Request failed');
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
let root = document.getElementById('toast-root');
|
|
if (!root) {
|
|
root = document.createElement('div');
|
|
root.id = 'toast-root';
|
|
root.className = 'toast-root';
|
|
document.body.appendChild(root);
|
|
}
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.textContent = message;
|
|
root.appendChild(toast);
|
|
window.setTimeout(() => {
|
|
toast.classList.add('toast-hide');
|
|
window.setTimeout(() => toast.remove(), 250);
|
|
}, 3600);
|
|
}
|
|
|
|
function downloadJson(filename, payload) {
|
|
const blob = new Blob([JSON.stringify(payload, null, 2) + '\n'], {
|
|
type: 'application/json',
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function displayTime(value) {
|
|
if (!value) return '-';
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return String(value).replace('T', ' ').replace('+00:00', ' UTC');
|
|
return date.toLocaleString(undefined, {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
}
|
|
|
|
function isAccountAuthorized(auth) {
|
|
const status = auth?.auth_status || auth || {};
|
|
return (
|
|
auth?.phase === 'authorized' ||
|
|
auth?.status === 'authorized' ||
|
|
status.status === 'ready' ||
|
|
status.status === 'authorized'
|
|
);
|
|
}
|
|
|
|
function accountLabel(account) {
|
|
return account?.label || account?.id || 'No account';
|
|
}
|
|
|
|
function setActiveAccount(accountId) {
|
|
settingsState.activeAccount = accountId || null;
|
|
if (accountId) {
|
|
localStorage.setItem(ACTIVE_ACCOUNT_STORAGE_KEY, accountId);
|
|
} else {
|
|
localStorage.removeItem(ACTIVE_ACCOUNT_STORAGE_KEY);
|
|
}
|
|
}
|
|
|
|
function chooseInitialAccount() {
|
|
const requested = new URLSearchParams(window.location.search).get('account');
|
|
if (requested && settingsState.accounts.some((account) => account.id === requested)) {
|
|
return requested;
|
|
}
|
|
const saved = localStorage.getItem(ACTIVE_ACCOUNT_STORAGE_KEY);
|
|
if (saved && settingsState.accounts.some((account) => account.id === saved)) {
|
|
return saved;
|
|
}
|
|
return settingsState.accounts[0]?.id || null;
|
|
}
|
|
|
|
function renderAccountList() {
|
|
const root = document.getElementById('settings-account-list');
|
|
root.innerHTML = '';
|
|
|
|
if (!settingsState.accounts.length) {
|
|
root.innerHTML = '<div class="empty-state">No accounts yet.</div>';
|
|
return;
|
|
}
|
|
|
|
settingsState.accounts.forEach((account) => {
|
|
const button = document.createElement('button');
|
|
button.className = 'settings-account-button';
|
|
button.type = 'button';
|
|
button.classList.toggle('active', account.id === settingsState.activeAccount);
|
|
|
|
const name = document.createElement('span');
|
|
name.className = 'settings-account-name';
|
|
name.textContent = accountLabel(account);
|
|
|
|
const status = document.createElement('span');
|
|
status.className = isAccountAuthorized(account.auth) ? 'settings-account-status ok' : 'settings-account-status';
|
|
status.textContent = isAccountAuthorized(account.auth) ? 'Authorized' : 'Needs auth';
|
|
|
|
button.append(name, status);
|
|
button.addEventListener('click', () => loadAccount(account.id));
|
|
root.appendChild(button);
|
|
});
|
|
}
|
|
|
|
function renderEmptyState() {
|
|
const hasAccount = Boolean(settingsState.activeAccount);
|
|
document.getElementById('settings-empty').classList.toggle('hidden', hasAccount);
|
|
document.getElementById('settings-export-btn').disabled = !hasAccount;
|
|
document.getElementById('settings-delete-btn').disabled = !hasAccount;
|
|
document
|
|
.getElementById('settings-credentials-form')
|
|
.querySelectorAll('input, button')
|
|
.forEach((node) => {
|
|
node.disabled = !hasAccount;
|
|
});
|
|
document.getElementById('settings-start-qr-btn').disabled = !hasAccount;
|
|
document
|
|
.getElementById('settings-phone-form')
|
|
.querySelectorAll('input, button')
|
|
.forEach((node) => {
|
|
node.disabled = !hasAccount;
|
|
});
|
|
document.getElementById('settings-scrape-media-toggle').disabled = !hasAccount;
|
|
}
|
|
|
|
function renderAccountData() {
|
|
const account = settingsState.accounts.find((item) => item.id === settingsState.activeAccount);
|
|
const dashboard = settingsState.dashboard || {};
|
|
const health = dashboard.health || {};
|
|
const auth = settingsState.auth || account?.auth || {};
|
|
const continuous = settingsState.continuous || {};
|
|
const continuousStatus = continuous.status || {};
|
|
const continuousConfig = continuous.config || {};
|
|
|
|
document.getElementById('settings-title').textContent = account ? accountLabel(account) : 'No account selected';
|
|
document.getElementById('settings-subtitle').textContent = account
|
|
? `Account ID: ${account.id}`
|
|
: 'Create or import an account to manage settings.';
|
|
document.getElementById('settings-auth-label').textContent = account
|
|
? `${accountLabel(account)} auth status`
|
|
: 'No account selected.';
|
|
|
|
document.getElementById('health-credentials').textContent = health.api_credentials ? 'Saved' : 'Missing';
|
|
document.getElementById('health-credentials').style.color = health.api_credentials ? 'var(--ok)' : 'var(--warn)';
|
|
document.getElementById('health-session').textContent =
|
|
health.session_ready || isAccountAuthorized(auth) ? 'Ready' : 'Missing';
|
|
document.getElementById('health-session').style.color =
|
|
health.session_ready || isAccountAuthorized(auth) ? 'var(--ok)' : 'var(--warn)';
|
|
document.getElementById('health-continuous').textContent = continuousStatus.running
|
|
? 'Running'
|
|
: continuousConfig.enabled
|
|
? 'Enabled'
|
|
: 'Stopped';
|
|
document.getElementById('health-continuous').style.color = continuousStatus.running ? 'var(--ok)' : 'var(--dim)';
|
|
document.getElementById('health-last-scrape').textContent = displayTime(
|
|
health.last_scrape || continuousStatus.last_iteration_at,
|
|
);
|
|
|
|
document.getElementById('settings-scrape-media-toggle').checked = Boolean(dashboard.dashboard?.scrape_media);
|
|
document.getElementById('settings-channel-count').textContent = String(health.channel_count || 0);
|
|
document.getElementById('settings-message-count').textContent = String(health.message_count || 0);
|
|
document.getElementById('settings-media-count').textContent = String(health.media_count || 0);
|
|
renderEmptyState();
|
|
renderAccountList();
|
|
}
|
|
|
|
async function loadAccounts() {
|
|
const payload = await api('/api/accounts');
|
|
settingsState.accounts = payload.accounts || [];
|
|
setActiveAccount(chooseInitialAccount());
|
|
renderAccountList();
|
|
if (settingsState.activeAccount) {
|
|
await loadAccount(settingsState.activeAccount, { preserveUrl: true });
|
|
} else {
|
|
settingsState.dashboard = null;
|
|
settingsState.auth = null;
|
|
settingsState.continuous = null;
|
|
renderAccountData();
|
|
}
|
|
}
|
|
|
|
async function loadAccount(accountId, options = {}) {
|
|
setActiveAccount(accountId);
|
|
const [dashboard, auth, continuous] = await Promise.all([
|
|
api(`/api/accounts/${encodeURIComponent(accountId)}`),
|
|
api(`/api/accounts/${encodeURIComponent(accountId)}/auth`).catch(() => ({})),
|
|
api(`/api/accounts/${encodeURIComponent(accountId)}/continuous`).catch(() => ({})),
|
|
]);
|
|
settingsState.dashboard = dashboard;
|
|
settingsState.auth = auth;
|
|
settingsState.continuous = continuous;
|
|
if (!options.preserveUrl) {
|
|
history.replaceState(null, '', `/settings?account=${encodeURIComponent(accountId)}`);
|
|
}
|
|
renderAccountData();
|
|
}
|
|
|
|
async function createAccount(event) {
|
|
event.preventDefault();
|
|
const accountId = document.getElementById('settings-add-account-id').value.trim();
|
|
const label = document.getElementById('settings-add-account-label').value.trim();
|
|
const apiId = document.getElementById('settings-add-account-api-id').value.trim();
|
|
const apiHash = document.getElementById('settings-add-account-api-hash').value.trim();
|
|
if (!accountId) return;
|
|
|
|
await api('/api/accounts', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
account_id: accountId,
|
|
label: label || accountId,
|
|
api_id: apiId ? parseInt(apiId) : undefined,
|
|
api_hash: apiHash || undefined,
|
|
}),
|
|
});
|
|
event.currentTarget.reset();
|
|
await loadAccounts();
|
|
await loadAccount(accountId);
|
|
showToast(`Added ${accountId}.`, 'success');
|
|
}
|
|
|
|
async function importAccount(event) {
|
|
const file = event.currentTarget.files?.[0];
|
|
if (!file) return;
|
|
try {
|
|
const payload = JSON.parse(await file.text());
|
|
const accountId = payload.account_id || payload.id;
|
|
if (!accountId) throw new Error('account_id is missing in import file');
|
|
await api('/api/accounts/import', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
event.currentTarget.value = '';
|
|
await loadAccounts();
|
|
await loadAccount(accountId);
|
|
showToast(`Imported ${accountId}.`, 'success');
|
|
} catch (err) {
|
|
showToast(`Failed to import account: ${err.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function saveCredentials(event) {
|
|
event.preventDefault();
|
|
if (!settingsState.activeAccount) return;
|
|
const apiId = document.getElementById('settings-api-id-input').value.trim();
|
|
const apiHash = document.getElementById('settings-api-hash-input').value.trim();
|
|
if (!apiId || !apiHash) return;
|
|
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/credentials`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ api_id: apiId, api_hash: apiHash }),
|
|
});
|
|
await loadAccount(settingsState.activeAccount);
|
|
showToast('Credentials saved.', 'success');
|
|
}
|
|
|
|
async function startQrLogin() {
|
|
if (!settingsState.activeAccount) return;
|
|
const data = await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/qr/start`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({}),
|
|
});
|
|
if (data.qr_image) {
|
|
document.getElementById('settings-qr-image').src = data.qr_image;
|
|
document.getElementById('settings-qr-wrap').classList.remove('hidden');
|
|
}
|
|
await loadAccount(settingsState.activeAccount);
|
|
}
|
|
|
|
async function requestPhoneCode(event) {
|
|
event.preventDefault();
|
|
if (!settingsState.activeAccount) return;
|
|
const phone = document.getElementById('settings-phone-input').value.trim();
|
|
if (!phone) return;
|
|
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/phone/request`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ phone }),
|
|
});
|
|
document.getElementById('settings-code-form').classList.remove('hidden');
|
|
await loadAccount(settingsState.activeAccount);
|
|
}
|
|
|
|
async function submitPhoneCode(event) {
|
|
event.preventDefault();
|
|
if (!settingsState.activeAccount) return;
|
|
const code = document.getElementById('settings-code-input').value.trim();
|
|
if (!code) return;
|
|
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/phone/submit`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ code }),
|
|
});
|
|
document.getElementById('settings-code-input').value = '';
|
|
await loadAccount(settingsState.activeAccount);
|
|
}
|
|
|
|
async function submitPassword(event) {
|
|
event.preventDefault();
|
|
if (!settingsState.activeAccount) return;
|
|
const password = document.getElementById('settings-password-input').value.trim();
|
|
if (!password) return;
|
|
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/password`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ password }),
|
|
});
|
|
document.getElementById('settings-password-input').value = '';
|
|
await loadAccount(settingsState.activeAccount);
|
|
}
|
|
|
|
async function exportAccount() {
|
|
if (!settingsState.activeAccount) return;
|
|
const payload = await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/export`);
|
|
downloadJson(`telegram-scraper-account-${settingsState.activeAccount}.json`, payload);
|
|
showToast('Account exported.', 'success');
|
|
}
|
|
|
|
async function deleteAccount() {
|
|
if (!settingsState.activeAccount) return;
|
|
const account = settingsState.accounts.find((item) => item.id === settingsState.activeAccount);
|
|
if (!window.confirm(`Remove account "${accountLabel(account)}"? All its account data will be deleted.`)) return;
|
|
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}`, { method: 'DELETE' });
|
|
setActiveAccount(null);
|
|
await loadAccounts();
|
|
showToast('Account deleted.', 'success');
|
|
}
|
|
|
|
async function toggleMedia(event) {
|
|
if (!settingsState.activeAccount) return;
|
|
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/settings/media`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ value: event.currentTarget.checked }),
|
|
});
|
|
await loadAccount(settingsState.activeAccount);
|
|
}
|
|
|
|
function bindEvents() {
|
|
document.getElementById('settings-add-account-form').addEventListener('submit', async (event) => {
|
|
try {
|
|
await createAccount(event);
|
|
} catch (err) {
|
|
showToast(`Failed to add account: ${err.message}`, 'error');
|
|
}
|
|
});
|
|
document.getElementById('settings-import-account-file').addEventListener('change', importAccount);
|
|
document.getElementById('settings-credentials-form').addEventListener('submit', async (event) => {
|
|
try {
|
|
await saveCredentials(event);
|
|
} catch (err) {
|
|
showToast(`Failed to save credentials: ${err.message}`, 'error');
|
|
}
|
|
});
|
|
document.getElementById('settings-start-qr-btn').addEventListener('click', async () => {
|
|
try {
|
|
await startQrLogin();
|
|
} catch (err) {
|
|
showToast(`QR login failed: ${err.message}`, 'error');
|
|
}
|
|
});
|
|
document.getElementById('settings-phone-form').addEventListener('submit', async (event) => {
|
|
try {
|
|
await requestPhoneCode(event);
|
|
} catch (err) {
|
|
showToast(`Phone login failed: ${err.message}`, 'error');
|
|
}
|
|
});
|
|
document.getElementById('settings-code-form').addEventListener('submit', async (event) => {
|
|
try {
|
|
await submitPhoneCode(event);
|
|
} catch (err) {
|
|
showToast(`Code submit failed: ${err.message}`, 'error');
|
|
}
|
|
});
|
|
document.getElementById('settings-password-form').addEventListener('submit', async (event) => {
|
|
try {
|
|
await submitPassword(event);
|
|
} catch (err) {
|
|
showToast(`Password submit failed: ${err.message}`, 'error');
|
|
}
|
|
});
|
|
document.getElementById('settings-export-btn').addEventListener('click', async () => {
|
|
try {
|
|
await exportAccount();
|
|
} catch (err) {
|
|
showToast(`Export failed: ${err.message}`, 'error');
|
|
}
|
|
});
|
|
document.getElementById('settings-delete-btn').addEventListener('click', async () => {
|
|
try {
|
|
await deleteAccount();
|
|
} catch (err) {
|
|
showToast(`Delete failed: ${err.message}`, 'error');
|
|
}
|
|
});
|
|
document.getElementById('settings-scrape-media-toggle').addEventListener('change', async (event) => {
|
|
try {
|
|
await toggleMedia(event);
|
|
} catch (err) {
|
|
showToast(`Media setting failed: ${err.message}`, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
bindEvents();
|
|
loadAccounts().catch((err) => {
|
|
console.error(err);
|
|
showToast(`Failed to load settings: ${err.message}`, 'error');
|
|
});
|