788 lines
28 KiB
JavaScript
788 lines
28 KiB
JavaScript
const els = {
|
|
adminStatus: document.querySelector('#adminStatus'),
|
|
credentialsSource: document.querySelector('#credentialsSource'),
|
|
tokenStatus: document.querySelector('#tokenStatus'),
|
|
steamStatus: document.querySelector('#steamStatus'),
|
|
gcStatus: document.querySelector('#gcStatus'),
|
|
loginPanel: document.querySelector('#loginPanel'),
|
|
notificationPanel: document.querySelector('#notificationPanel'),
|
|
telegramPanel: document.querySelector('#telegramPanel'),
|
|
lobbyPanel: document.querySelector('#lobbyPanel'),
|
|
credentialsPanel: document.querySelector('#credentialsPanel'),
|
|
guardPanel: document.querySelector('#guardPanel'),
|
|
tokenPanel: document.querySelector('#tokenPanel'),
|
|
adminPassword: document.querySelector('#adminPassword'),
|
|
unlockBtn: document.querySelector('#unlockBtn'),
|
|
adminError: document.querySelector('#adminError'),
|
|
adminMessage: document.querySelector('#adminMessage'),
|
|
steamAccount: document.querySelector('#steamAccount'),
|
|
steamPassword: document.querySelector('#steamPassword'),
|
|
clearToken: document.querySelector('#clearToken'),
|
|
reloadBtn: document.querySelector('#reloadBtn'),
|
|
saveBtn: document.querySelector('#saveBtn'),
|
|
steamGuardCode: document.querySelector('#steamGuardCode'),
|
|
submitGuardBtn: document.querySelector('#submitGuardBtn'),
|
|
guardHint: document.querySelector('#guardHint'),
|
|
tokenSteamId: document.querySelector('#tokenSteamId'),
|
|
tokenAccount: document.querySelector('#tokenAccount'),
|
|
tokenUpdated: document.querySelector('#tokenUpdated'),
|
|
tokenExpires: document.querySelector('#tokenExpires'),
|
|
clearTokenBtn: document.querySelector('#clearTokenBtn'),
|
|
enableNotificationsBtn: document.querySelector('#enableNotificationsBtn'),
|
|
disableNotificationsBtn: document.querySelector('#disableNotificationsBtn'),
|
|
testNotificationBtn: document.querySelector('#testNotificationBtn'),
|
|
notificationStatus: document.querySelector('#notificationStatus'),
|
|
telegramToken: document.querySelector('#telegramToken'),
|
|
telegramRecipientIds: document.querySelector('#telegramRecipientIds'),
|
|
saveTelegramBtn: document.querySelector('#saveTelegramBtn'),
|
|
testTelegramBtn: document.querySelector('#testTelegramBtn'),
|
|
disableTelegramBtn: document.querySelector('#disableTelegramBtn'),
|
|
telegramStatus: document.querySelector('#telegramStatus'),
|
|
savedSteamAccount: document.querySelector('#savedSteamAccount'),
|
|
activateAccountBtn: document.querySelector('#activateAccountBtn'),
|
|
lobbyName: document.querySelector('#lobbyName'),
|
|
lobbyRegion: document.querySelector('#lobbyRegion'),
|
|
customGameId: document.querySelector('#customGameId'),
|
|
customGameMode: document.querySelector('#customGameMode'),
|
|
customMapName: document.querySelector('#customMapName'),
|
|
lobbyPassKey: document.querySelector('#lobbyPassKey'),
|
|
customMinPlayers: document.querySelector('#customMinPlayers'),
|
|
customMaxPlayers: document.querySelector('#customMaxPlayers'),
|
|
notifyAtPlayers: document.querySelector('#notifyAtPlayers'),
|
|
leaveAtPlayers: document.querySelector('#leaveAtPlayers'),
|
|
adminNames: document.querySelector('#adminNames'),
|
|
customGameCrc: document.querySelector('#customGameCrc'),
|
|
customGameTimestamp: document.querySelector('#customGameTimestamp'),
|
|
allowSpectating: document.querySelector('#allowSpectating'),
|
|
saveLobbySettingsBtn: document.querySelector('#saveLobbySettingsBtn'),
|
|
createLobbyBtn: document.querySelector('#createLobbyBtn'),
|
|
stopLobbyBtn: document.querySelector('#stopLobbyBtn'),
|
|
automationPhase: document.querySelector('#automationPhase'),
|
|
activeLobbyId: document.querySelector('#activeLobbyId'),
|
|
activePlayerCount: document.querySelector('#activePlayerCount'),
|
|
publicationStatus: document.querySelector('#publicationStatus'),
|
|
arcadePublicationStatus: document.querySelector('#arcadePublicationStatus'),
|
|
lobbyProtocolStatus: document.querySelector('#lobbyProtocolStatus'),
|
|
dotaClientVersion: document.querySelector('#dotaClientVersion'),
|
|
leaveReason: document.querySelector('#leaveReason'),
|
|
lobbyMembers: document.querySelector('#lobbyMembers'),
|
|
};
|
|
|
|
let adminPassword = sessionStorage.getItem('dokaAdminPassword') || '';
|
|
let credentialsDirty = false;
|
|
let lobbySettingsDirty = false;
|
|
let telegramDirty = false;
|
|
let regionsReady = false;
|
|
let lastAdminState = null;
|
|
|
|
function readPendingLobbyTemplate() {
|
|
try {
|
|
const saved = sessionStorage.getItem('dokaLobbyTemplate');
|
|
if (saved) {
|
|
const parsed = JSON.parse(saved);
|
|
if (parsed && typeof parsed === 'object') return parsed;
|
|
}
|
|
} catch {
|
|
// Fall through to the query-string compatibility path.
|
|
}
|
|
|
|
const query = new URLSearchParams(window.location.search);
|
|
return query.get('template') === 'lobby' ? Object.fromEntries(query.entries()) : null;
|
|
}
|
|
|
|
const pendingLobbyTemplate = readPendingLobbyTemplate();
|
|
let lobbyTemplateApplied = false;
|
|
|
|
const dateTimeFormat = new Intl.DateTimeFormat('en-GB', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
});
|
|
|
|
function formatDate(iso) {
|
|
if (!iso) return '-';
|
|
const date = new Date(iso);
|
|
if (Number.isNaN(date.getTime())) return '-';
|
|
return dateTimeFormat.format(date);
|
|
}
|
|
|
|
function setBadge(label, klass) {
|
|
els.adminStatus.textContent = label;
|
|
els.adminStatus.className = `badge ${klass}`;
|
|
}
|
|
|
|
function showError(message) {
|
|
els.adminError.hidden = !message;
|
|
els.adminError.textContent = message || '';
|
|
}
|
|
|
|
function showMessage(message) {
|
|
els.adminMessage.hidden = !message;
|
|
els.adminMessage.textContent = message || '';
|
|
}
|
|
|
|
async function adminFetch(url, options = {}) {
|
|
const res = await fetch(url, {
|
|
...options,
|
|
cache: 'no-store',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Admin-Password': adminPassword,
|
|
...(options.headers || {}),
|
|
},
|
|
});
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
return data;
|
|
}
|
|
|
|
function unlocked() {
|
|
return Boolean(adminPassword);
|
|
}
|
|
|
|
function initRegions(regions) {
|
|
if (regionsReady || !regions) return;
|
|
for (const [id, name] of Object.entries(regions).sort(([a], [b]) => Number(a) - Number(b))) {
|
|
const option = document.createElement('option');
|
|
option.value = id;
|
|
option.textContent = `${name} (${id})`;
|
|
els.lobbyRegion.append(option);
|
|
}
|
|
regionsReady = true;
|
|
}
|
|
|
|
function setLobbyInputs(settings) {
|
|
els.lobbyName.value = settings.lobbyName || '';
|
|
els.lobbyRegion.value = String(settings.serverRegion ?? 3);
|
|
els.customGameId.value = settings.customGameId || '';
|
|
els.customGameMode.value = settings.customGameMode || '';
|
|
els.customMapName.value = settings.customMapName || '';
|
|
els.lobbyPassKey.value = settings.passKey || '';
|
|
els.customMinPlayers.value = String(settings.customMinPlayers ?? 2);
|
|
els.customMaxPlayers.value = String(settings.customMaxPlayers ?? 10);
|
|
els.notifyAtPlayers.value = String(settings.notifyAtPlayers ?? 8);
|
|
els.leaveAtPlayers.value = String(settings.leaveAtPlayers ?? 9);
|
|
els.adminNames.value = (settings.adminNames || []).join(', ');
|
|
els.customGameCrc.value = settings.customGameCrc || '';
|
|
els.customGameTimestamp.value = settings.customGameTimestamp || '';
|
|
els.allowSpectating.checked = settings.allowSpectating !== false;
|
|
lobbySettingsDirty = false;
|
|
}
|
|
|
|
async function resolveMissingTemplateMetadata() {
|
|
if (els.customGameMode.value || !pendingLobbyTemplate?.sourceLobbyId) return;
|
|
const controller = new AbortController();
|
|
let timedOut = false;
|
|
const timeout = setTimeout(() => {
|
|
timedOut = true;
|
|
controller.abort();
|
|
}, 15000);
|
|
try {
|
|
showMessage('Resolving the internal addon name (local Workshop first, Dota GC fallback; up to 15 seconds)...');
|
|
const data = await adminFetch('/api/admin/lobby-template', {
|
|
method: 'POST',
|
|
signal: controller.signal,
|
|
body: JSON.stringify({
|
|
lobbyId: pendingLobbyTemplate.sourceLobbyId,
|
|
serverRegion: Number(pendingLobbyTemplate.serverRegion || 0),
|
|
customGameId: pendingLobbyTemplate.customGameId,
|
|
}),
|
|
});
|
|
els.customGameMode.value = data.templateMetadata?.customGameMode || '';
|
|
if (!els.customGameTimestamp.value && data.templateMetadata?.customGameTimestamp) {
|
|
els.customGameTimestamp.value = data.templateMetadata.customGameTimestamp;
|
|
}
|
|
if (!els.customGameMode.value) throw new Error('The source lobby has no internal addon name.');
|
|
lobbySettingsDirty = true;
|
|
showError('');
|
|
showMessage(`Lobby template is complete: Workshop ${els.customGameId.value}, addon ${els.customGameMode.value}, map ${els.customMapName.value}. Review the thresholds, then create it.`);
|
|
} catch (err) {
|
|
showError(`Could not complete lobby template: ${timedOut ? 'metadata lookup timed out after 15 seconds' : err.message}`);
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
function applyLobbyTemplate() {
|
|
if (!pendingLobbyTemplate || lobbyTemplateApplied) return;
|
|
|
|
const setFromTemplate = (element, key) => {
|
|
const value = pendingLobbyTemplate[key];
|
|
if (value !== undefined && value !== null) element.value = String(value);
|
|
};
|
|
setFromTemplate(els.lobbyName, 'lobbyName');
|
|
setFromTemplate(els.lobbyRegion, 'serverRegion');
|
|
setFromTemplate(els.customGameId, 'customGameId');
|
|
setFromTemplate(els.customGameMode, 'customGameMode');
|
|
setFromTemplate(els.customMapName, 'customMapName');
|
|
setFromTemplate(els.customMinPlayers, 'customMinPlayers');
|
|
setFromTemplate(els.customMaxPlayers, 'customMaxPlayers');
|
|
setFromTemplate(els.customGameCrc, 'customGameCrc');
|
|
setFromTemplate(els.customGameTimestamp, 'customGameTimestamp');
|
|
|
|
// The source feed never exposes the lobby password. Clear any stale value.
|
|
els.lobbyPassKey.value = '';
|
|
|
|
const maxPlayers = Number(els.customMaxPlayers.value);
|
|
if (Number.isInteger(maxPlayers) && maxPlayers > 0) {
|
|
const highestSafeThreshold = Math.max(1, maxPlayers - 1);
|
|
const leaveAt = Math.min(Number(els.leaveAtPlayers.value) || highestSafeThreshold, highestSafeThreshold);
|
|
els.leaveAtPlayers.value = String(leaveAt);
|
|
els.notifyAtPlayers.value = String(Math.min(Number(els.notifyAtPlayers.value) || leaveAt, leaveAt));
|
|
}
|
|
|
|
lobbyTemplateApplied = true;
|
|
lobbySettingsDirty = true;
|
|
try {
|
|
sessionStorage.removeItem('dokaLobbyTemplate');
|
|
} catch {
|
|
// The query-string fallback is still removed below.
|
|
}
|
|
window.history.replaceState({}, '', '/admin');
|
|
showMessage(`Lobby template loaded: Workshop ${els.customGameId.value}, map ${els.customMapName.value}. Review the notification and leave thresholds, then create it.`);
|
|
if (!els.customGameMode.value) {
|
|
showError('The GC did not return the internal addon name for this row. Refresh the lobby list and create the template again; an incomplete template cannot be published.');
|
|
resolveMissingTemplateMetadata();
|
|
}
|
|
}
|
|
|
|
function lobbyPayload() {
|
|
return {
|
|
lobbyName: els.lobbyName.value,
|
|
serverRegion: Number(els.lobbyRegion.value),
|
|
customGameId: els.customGameId.value,
|
|
customGameMode: els.customGameMode.value,
|
|
customMapName: els.customMapName.value,
|
|
passKey: els.lobbyPassKey.value,
|
|
customMinPlayers: Number(els.customMinPlayers.value),
|
|
customMaxPlayers: Number(els.customMaxPlayers.value),
|
|
notifyAtPlayers: Number(els.notifyAtPlayers.value),
|
|
leaveAtPlayers: Number(els.leaveAtPlayers.value),
|
|
adminNames: els.adminNames.value,
|
|
customGameCrc: els.customGameCrc.value,
|
|
customGameTimestamp: els.customGameTimestamp.value,
|
|
allowSpectating: els.allowSpectating.checked,
|
|
};
|
|
}
|
|
|
|
function renderLobbyAutomation(automation) {
|
|
const status = automation.status || {};
|
|
const settings = automation.settings || {};
|
|
const busy = ['creating', 'leaving'].includes(status.phase);
|
|
const hasLobby = Boolean(status.lobbyId);
|
|
|
|
if (!lobbySettingsDirty && !busy) setLobbyInputs(settings);
|
|
if (unlocked() && regionsReady && automation.settings && !busy) applyLobbyTemplate();
|
|
els.automationPhase.textContent = status.phase || 'idle';
|
|
els.activeLobbyId.textContent = status.lobbyId || '-';
|
|
els.activePlayerCount.textContent = String(status.humanCount ?? 0);
|
|
els.publicationStatus.textContent = status.practicePublished === true
|
|
? 'listed'
|
|
: status.practicePublished === false
|
|
? 'not listed'
|
|
: status.armed
|
|
? 'checking'
|
|
: '-';
|
|
els.arcadePublicationStatus.textContent = status.arcadePublished === true
|
|
? 'listed'
|
|
: status.arcadePublished === false
|
|
? 'not listed'
|
|
: status.armed
|
|
? 'checking'
|
|
: '-';
|
|
const protocol = status.lobbyProtocol;
|
|
els.lobbyProtocolStatus.textContent = protocol
|
|
? `type=${protocol.lobbyType}, visibility=${protocol.visibility}, auto=${protocol.autoCreated ? 'yes' : 'no'}, bot team=${protocol.botTeam ?? '-'}`
|
|
: '-';
|
|
els.leaveReason.textContent = status.leaveReason || '-';
|
|
els.createLobbyBtn.disabled = busy || hasLobby || Boolean(status.armed);
|
|
els.stopLobbyBtn.disabled = busy || (!hasLobby && !status.armed);
|
|
els.saveLobbySettingsBtn.disabled = busy;
|
|
|
|
const members = status.members || [];
|
|
if (members.length) {
|
|
els.lobbyMembers.textContent = members
|
|
.map((member) => member.name ? `${member.name} (${member.steamId})` : member.steamId)
|
|
.join(', ');
|
|
} else {
|
|
els.lobbyMembers.textContent = 'No real players in the active lobby.';
|
|
}
|
|
|
|
if (status.lastError) showError(status.lastError);
|
|
}
|
|
|
|
function pushSupported() {
|
|
return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;
|
|
}
|
|
|
|
async function currentPushSubscription() {
|
|
if (!pushSupported()) return null;
|
|
const registration = await navigator.serviceWorker.register('/sw.js');
|
|
return registration.pushManager.getSubscription();
|
|
}
|
|
|
|
async function updateNotificationStatus() {
|
|
if (!pushSupported()) {
|
|
els.notificationStatus.textContent = 'not supported';
|
|
els.enableNotificationsBtn.disabled = true;
|
|
els.disableNotificationsBtn.disabled = true;
|
|
els.testNotificationBtn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const subscription = await currentPushSubscription();
|
|
els.notificationStatus.textContent = subscription && Notification.permission === 'granted'
|
|
? 'enabled on this browser'
|
|
: Notification.permission;
|
|
els.disableNotificationsBtn.disabled = !subscription;
|
|
els.testNotificationBtn.disabled = !subscription;
|
|
} catch (err) {
|
|
els.notificationStatus.textContent = err.message;
|
|
}
|
|
}
|
|
|
|
function urlBase64ToUint8Array(value) {
|
|
const padding = '='.repeat((4 - (value.length % 4)) % 4);
|
|
const base64 = (value + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
const raw = atob(base64);
|
|
return Uint8Array.from([...raw].map((char) => char.charCodeAt(0)));
|
|
}
|
|
|
|
function renderSavedAccounts(credentials) {
|
|
const accounts = Array.isArray(credentials.savedAccounts) ? credentials.savedAccounts : [];
|
|
const currentSelection = els.savedSteamAccount.value;
|
|
els.savedSteamAccount.replaceChildren();
|
|
if (!accounts.length) {
|
|
const option = document.createElement('option');
|
|
option.value = '';
|
|
option.textContent = 'No saved accounts';
|
|
els.savedSteamAccount.append(option);
|
|
els.savedSteamAccount.disabled = true;
|
|
els.activateAccountBtn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
for (const account of accounts) {
|
|
const option = document.createElement('option');
|
|
option.value = account.accountName;
|
|
option.textContent = account.accountName === credentials.activeAccountName
|
|
? `${account.accountName} (active)`
|
|
: account.accountName;
|
|
els.savedSteamAccount.append(option);
|
|
}
|
|
els.savedSteamAccount.disabled = false;
|
|
els.activateAccountBtn.disabled = false;
|
|
const preferred = accounts.some((account) => account.accountName === currentSelection)
|
|
? currentSelection
|
|
: credentials.activeAccountName;
|
|
els.savedSteamAccount.value = preferred || accounts[0].accountName;
|
|
}
|
|
|
|
function renderTelegram(telegram = {}) {
|
|
const bot = telegram.botUsername ? `@${telegram.botUsername}` : 'bot';
|
|
if (telegram.configured) {
|
|
els.telegramStatus.textContent = `${bot}, ${telegram.recipientCount || 0} recipient(s)${telegram.lastError ? `; error: ${telegram.lastError}` : ''}`;
|
|
} else {
|
|
els.telegramStatus.textContent = 'not configured';
|
|
}
|
|
els.testTelegramBtn.disabled = !telegram.configured;
|
|
els.disableTelegramBtn.disabled = !telegram.configured;
|
|
|
|
const editing = document.activeElement === els.telegramToken
|
|
|| document.activeElement === els.telegramRecipientIds
|
|
|| telegramDirty;
|
|
if (!editing) {
|
|
els.telegramToken.value = '';
|
|
els.telegramToken.placeholder = telegram.configured
|
|
? 'configured; leave blank to keep saved token'
|
|
: 'paste token from @BotFather';
|
|
els.telegramRecipientIds.value = (telegram.recipientIds || []).join(', ');
|
|
telegramDirty = false;
|
|
}
|
|
}
|
|
|
|
function render(data, options = {}) {
|
|
lastAdminState = data;
|
|
const preserveEditedInputs = options.preserveEditedInputs !== false;
|
|
const connection = data.connection || {};
|
|
const credentials = data.credentials || {};
|
|
const token = data.refreshToken || {};
|
|
const steamGuard = connection.steamGuard || {};
|
|
|
|
els.loginPanel.hidden = unlocked();
|
|
els.notificationPanel.hidden = !unlocked();
|
|
els.telegramPanel.hidden = !unlocked();
|
|
els.lobbyPanel.hidden = !unlocked();
|
|
els.credentialsPanel.hidden = !unlocked();
|
|
els.tokenPanel.hidden = !unlocked();
|
|
els.guardPanel.hidden = !unlocked() || !steamGuard.required;
|
|
|
|
setBadge(unlocked() ? 'unlocked' : 'locked', unlocked() ? 'badge-ok' : 'badge-muted');
|
|
els.credentialsSource.textContent = credentials.source || '-';
|
|
els.tokenStatus.textContent = token.exists ? 'saved' : 'missing';
|
|
els.steamStatus.textContent = connection.steam || '-';
|
|
els.gcStatus.textContent = connection.gc || '-';
|
|
els.dotaClientVersion.textContent = connection.dotaClientVersion
|
|
? `${connection.dotaClientVersion}${connection.dotaClientVersionSource ? ` (${connection.dotaClientVersionSource})` : ''}`
|
|
: 'not detected; set DOTA_CLIENT_VERSION';
|
|
initRegions(data.regions);
|
|
renderLobbyAutomation(data.lobbyAutomation || {});
|
|
renderTelegram(data.lobbyAutomation?.telegram || {});
|
|
renderSavedAccounts(credentials);
|
|
|
|
const editingCredentials = document.activeElement === els.steamAccount
|
|
|| document.activeElement === els.steamPassword
|
|
|| credentialsDirty;
|
|
|
|
if (!preserveEditedInputs || !editingCredentials) {
|
|
els.steamAccount.value = credentials.accountName || '';
|
|
els.steamPassword.value = '';
|
|
els.steamPassword.placeholder = credentials.passwordSaved
|
|
? 'saved; leave blank to keep the saved password'
|
|
: 'password required for a new account';
|
|
credentialsDirty = false;
|
|
}
|
|
|
|
els.tokenSteamId.textContent = token.steamID || '-';
|
|
els.tokenAccount.textContent = token.accountName || '-';
|
|
els.tokenUpdated.textContent = formatDate(token.updatedAt);
|
|
els.tokenExpires.textContent = formatDate(token.expiresAt);
|
|
|
|
if (steamGuard.required) {
|
|
const where = steamGuard.domain ? `Email domain: ${steamGuard.domain}` : 'Mobile authenticator code required';
|
|
els.guardHint.textContent = steamGuard.lastCodeWrong ? `Last code was rejected. ${where}.` : where;
|
|
} else {
|
|
els.guardHint.textContent = '';
|
|
}
|
|
}
|
|
|
|
async function loadAuth(options = {}) {
|
|
if (!unlocked()) {
|
|
render({});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showError('');
|
|
const data = await adminFetch('/api/admin/auth');
|
|
render(data, options);
|
|
} catch (err) {
|
|
showError(err.message);
|
|
if (err.message.includes('Invalid admin password')) {
|
|
adminPassword = '';
|
|
sessionStorage.removeItem('dokaAdminPassword');
|
|
}
|
|
render({});
|
|
}
|
|
}
|
|
|
|
async function saveCredentials() {
|
|
try {
|
|
showError('');
|
|
showMessage('');
|
|
const data = await adminFetch('/api/admin/auth', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
accountName: els.steamAccount.value,
|
|
password: els.steamPassword.value,
|
|
clearRefreshToken: els.clearToken.checked,
|
|
}),
|
|
});
|
|
credentialsDirty = false;
|
|
render(data);
|
|
showMessage('Credentials saved. Reconnect started.');
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
async function submitSteamGuard() {
|
|
try {
|
|
showError('');
|
|
showMessage('');
|
|
const data = await adminFetch('/api/admin/steam-guard', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ code: els.steamGuardCode.value }),
|
|
});
|
|
els.steamGuardCode.value = '';
|
|
render(data);
|
|
showMessage('Steam Guard code submitted.');
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
async function clearRefreshToken() {
|
|
try {
|
|
showError('');
|
|
showMessage('');
|
|
const data = await adminFetch('/api/admin/clear-token', { method: 'POST', body: '{}' });
|
|
render(data);
|
|
showMessage('Refresh token cleared.');
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
async function saveLobbySettings() {
|
|
try {
|
|
showError('');
|
|
showMessage('');
|
|
const data = await adminFetch('/api/admin/lobby-settings', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(lobbyPayload()),
|
|
});
|
|
lobbySettingsDirty = false;
|
|
render(data);
|
|
showMessage('Lobby settings saved.');
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
async function createLobby() {
|
|
try {
|
|
showError('');
|
|
showMessage('Creating lobby and waiting for the Dota GC snapshot...');
|
|
els.createLobbyBtn.disabled = true;
|
|
const data = await adminFetch('/api/admin/lobby', {
|
|
method: 'POST',
|
|
body: JSON.stringify(lobbyPayload()),
|
|
});
|
|
lobbySettingsDirty = false;
|
|
render(data);
|
|
showMessage('Lobby created. Player monitoring and safe auto-leave are active.');
|
|
} catch (err) {
|
|
showError(err.message);
|
|
showMessage('');
|
|
} finally {
|
|
if (!lastAdminState?.lobbyAutomation?.status?.armed) els.createLobbyBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function stopLobby() {
|
|
try {
|
|
showError('');
|
|
showMessage('Leaving lobby...');
|
|
els.stopLobbyBtn.disabled = true;
|
|
const data = await adminFetch('/api/admin/lobby', {
|
|
method: 'DELETE',
|
|
body: '{}',
|
|
});
|
|
render(data);
|
|
showMessage('The bot left the lobby.');
|
|
} catch (err) {
|
|
showError(err.message);
|
|
showMessage('');
|
|
}
|
|
}
|
|
|
|
async function enableNotifications() {
|
|
try {
|
|
showError('');
|
|
if (!pushSupported()) throw new Error('Push notifications are not supported by this browser.');
|
|
const permission = await Notification.requestPermission();
|
|
if (permission !== 'granted') throw new Error(`Notification permission is ${permission}.`);
|
|
const registration = await navigator.serviceWorker.register('/sw.js');
|
|
let subscription = await registration.pushManager.getSubscription();
|
|
if (!subscription) {
|
|
const publicKey = lastAdminState?.lobbyAutomation?.push?.publicKey;
|
|
if (!publicKey) throw new Error('Web Push public key is unavailable.');
|
|
subscription = await registration.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: urlBase64ToUint8Array(publicKey),
|
|
});
|
|
}
|
|
const data = await adminFetch('/api/admin/push-subscription', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ subscription: subscription.toJSON() }),
|
|
});
|
|
render(data);
|
|
await updateNotificationStatus();
|
|
showMessage('Browser push notifications enabled.');
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
async function disableNotifications() {
|
|
try {
|
|
showError('');
|
|
const subscription = await currentPushSubscription();
|
|
if (subscription) {
|
|
await adminFetch('/api/admin/push-subscription', {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({ endpoint: subscription.endpoint }),
|
|
});
|
|
await subscription.unsubscribe();
|
|
}
|
|
await updateNotificationStatus();
|
|
showMessage('Browser push notifications disabled on this browser.');
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
async function testNotification() {
|
|
try {
|
|
showError('');
|
|
const data = await adminFetch('/api/admin/test-notification', { method: 'POST', body: '{}' });
|
|
const result = data.notificationResult || {};
|
|
showMessage(`Test notification sent: ${result.delivered || 0} delivered, ${result.failed || 0} failed.`);
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
async function saveTelegram() {
|
|
try {
|
|
showError('');
|
|
showMessage('Checking the Telegram bot and saving recipients...');
|
|
const data = await adminFetch('/api/admin/telegram', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
token: els.telegramToken.value,
|
|
recipientIds: els.telegramRecipientIds.value,
|
|
}),
|
|
});
|
|
telegramDirty = false;
|
|
render(data);
|
|
showMessage('Telegram notifications saved. Use Send test to verify every recipient.');
|
|
} catch (err) {
|
|
showMessage('');
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
async function disableTelegram() {
|
|
try {
|
|
showError('');
|
|
const data = await adminFetch('/api/admin/telegram', { method: 'DELETE', body: '{}' });
|
|
telegramDirty = false;
|
|
render(data);
|
|
showMessage('Telegram notifications disabled and the saved bot token removed.');
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
async function testTelegram() {
|
|
try {
|
|
showError('');
|
|
const data = await adminFetch('/api/admin/test-telegram', { method: 'POST', body: '{}' });
|
|
const result = data.telegramResult || {};
|
|
render(data);
|
|
const errors = result.errors?.length ? ` ${result.errors.join('; ')}` : '';
|
|
showMessage(`Telegram test: ${result.delivered || 0} delivered, ${result.failed || 0} failed.${errors}`);
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
async function activateSavedAccount() {
|
|
try {
|
|
showError('');
|
|
showMessage('Switching Steam account and reconnecting Dota GC...');
|
|
const data = await adminFetch('/api/admin/steam-account/activate', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ accountName: els.savedSteamAccount.value }),
|
|
});
|
|
credentialsDirty = false;
|
|
render(data);
|
|
showMessage('Steam account activated. Reconnection continues in the background.');
|
|
} catch (err) {
|
|
showMessage('');
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
els.unlockBtn.addEventListener('click', () => {
|
|
adminPassword = els.adminPassword.value;
|
|
sessionStorage.setItem('dokaAdminPassword', adminPassword);
|
|
loadAuth({ preserveEditedInputs: false });
|
|
});
|
|
|
|
els.adminPassword.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter') els.unlockBtn.click();
|
|
});
|
|
|
|
els.reloadBtn.addEventListener('click', () => {
|
|
credentialsDirty = false;
|
|
loadAuth({ preserveEditedInputs: false });
|
|
});
|
|
els.saveBtn.addEventListener('click', saveCredentials);
|
|
els.submitGuardBtn.addEventListener('click', submitSteamGuard);
|
|
els.clearTokenBtn.addEventListener('click', clearRefreshToken);
|
|
els.saveLobbySettingsBtn.addEventListener('click', saveLobbySettings);
|
|
els.createLobbyBtn.addEventListener('click', createLobby);
|
|
els.stopLobbyBtn.addEventListener('click', stopLobby);
|
|
els.enableNotificationsBtn.addEventListener('click', enableNotifications);
|
|
els.disableNotificationsBtn.addEventListener('click', disableNotifications);
|
|
els.testNotificationBtn.addEventListener('click', testNotification);
|
|
els.saveTelegramBtn.addEventListener('click', saveTelegram);
|
|
els.testTelegramBtn.addEventListener('click', testTelegram);
|
|
els.disableTelegramBtn.addEventListener('click', disableTelegram);
|
|
els.activateAccountBtn.addEventListener('click', activateSavedAccount);
|
|
|
|
els.steamAccount.addEventListener('input', () => {
|
|
credentialsDirty = true;
|
|
});
|
|
|
|
els.steamPassword.addEventListener('input', () => {
|
|
credentialsDirty = true;
|
|
});
|
|
|
|
els.savedSteamAccount.addEventListener('change', () => {
|
|
if (!els.savedSteamAccount.value) return;
|
|
els.steamAccount.value = els.savedSteamAccount.value;
|
|
els.steamPassword.value = '';
|
|
credentialsDirty = true;
|
|
});
|
|
|
|
[els.telegramToken, els.telegramRecipientIds].forEach((element) => {
|
|
element.addEventListener('input', () => {
|
|
telegramDirty = true;
|
|
});
|
|
});
|
|
|
|
[
|
|
els.lobbyName,
|
|
els.lobbyRegion,
|
|
els.customGameId,
|
|
els.customGameMode,
|
|
els.customMapName,
|
|
els.lobbyPassKey,
|
|
els.customMinPlayers,
|
|
els.customMaxPlayers,
|
|
els.notifyAtPlayers,
|
|
els.leaveAtPlayers,
|
|
els.adminNames,
|
|
els.customGameCrc,
|
|
els.customGameTimestamp,
|
|
els.allowSpectating,
|
|
].forEach((element) => {
|
|
element.addEventListener('input', () => {
|
|
lobbySettingsDirty = true;
|
|
});
|
|
element.addEventListener('change', () => {
|
|
lobbySettingsDirty = true;
|
|
});
|
|
});
|
|
|
|
els.steamGuardCode.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter') els.submitGuardBtn.click();
|
|
});
|
|
|
|
if (adminPassword) {
|
|
els.adminPassword.value = adminPassword;
|
|
}
|
|
|
|
loadAuth({ preserveEditedInputs: false });
|
|
updateNotificationStatus();
|
|
setInterval(loadAuth, 5000);
|