Created
January 2, 2026 10:02
-
-
Save jsCommander/2078a56b9c449ec08967d6a58b31f0f3 to your computer and use it in GitHub Desktop.
gos uslugi slot checker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { exec } from 'child_process'; | |
| import { appendFileSync, mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs'; | |
| import { join, dirname } from 'path'; | |
| // --- CONFIGURATION --- | |
| const CONFIG = { | |
| COOKIES_FILE: 'data/cookies.json', | |
| SCHEDULE: { | |
| TICK_INTERVAL_SEC: 5, // Pulse of the runner | |
| MIN_INTERVAL_SEC: 10, // Global floor between requests | |
| NORMAL_INTERVAL_SEC: 120, // 2 minutes | |
| SESSION_CHECK_INTERVAL_SEC: 300, // 5 minutes | |
| IMPORTANT_RANGES: [ | |
| { start: '00:00', end: '00:10', intervalSec: 20 }, | |
| { start: '07:00', end: '09:30', intervalSec: 60 }, | |
| ] | |
| }, | |
| CHECK_SESSION_URL: 'https://www.gosuslugi.ru/auth-provider/check-session', | |
| URL: 'https://www.gosuslugi.ru/api/lk/v1/equeue/agg/slots', | |
| LOGS_DIR: 'logs', | |
| STATIC_HEADERS: { | |
| 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:146.0) Gecko/20100101 Firefox/146.0', | |
| 'Accept': 'application/json, text/plain, */*', | |
| 'Accept-Language': 'en-US,en;q=0.5', | |
| 'X-ORDER-ID': 'undefined', | |
| 'X-FORM-ID': '600300/1', | |
| 'Content-Type': 'application/json', | |
| }, | |
| PAYLOAD: { | |
| organizationId: ['10003871712'], | |
| serviceId: ['10001971213'], | |
| eserviceId: '10000000300', | |
| attributes: [], | |
| filter: null, | |
| }, | |
| }; | |
| let COOKIES: Record<string, string> = {}; | |
| // --- PERSISTENCE --- | |
| function saveCookies() { | |
| try { | |
| const dir = dirname(CONFIG.COOKIES_FILE); | |
| if (!existsSync(dir)) { | |
| mkdirSync(dir, { recursive: true }); | |
| } | |
| writeFileSync(CONFIG.COOKIES_FILE, JSON.stringify(COOKIES, null, 2)); | |
| } catch (error) { | |
| console.error(`Failed to save cookies: ${error}`); | |
| } | |
| } | |
| function loadCookies() { | |
| try { | |
| if (existsSync(CONFIG.COOKIES_FILE)) { | |
| const data = readFileSync(CONFIG.COOKIES_FILE, 'utf-8'); | |
| COOKIES = JSON.parse(data); | |
| } else { | |
| logger('ERROR', `Cookies file not found at ${CONFIG.COOKIES_FILE}. Please create it.`); | |
| process.exit(1); | |
| } | |
| } catch (error) { | |
| logger('ERROR', `Failed to load cookies: ${error}`); | |
| process.exit(1); | |
| } | |
| } | |
| loadCookies(); | |
| const stringifyCookies = (cookiesObj: Record<string, string>) => | |
| Object.entries(cookiesObj) | |
| .map(([key, value]) => `${key}=${value}`) | |
| .join('; '); | |
| const parseSetCookie = (setCookieHeaders: string[] | string | null): Record<string, string> => { | |
| if (!setCookieHeaders) return {}; | |
| const cookies: Record<string, string> = {}; | |
| const headers = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders]; | |
| headers.forEach(header => { | |
| const parts = header.split(';')[0].split('='); | |
| if (parts.length >= 2) { | |
| const key = parts[0].trim(); | |
| const value = parts.slice(1).join('=').trim(); | |
| cookies[key] = value; | |
| } | |
| }); | |
| return cookies; | |
| }; | |
| function updateCookies(setCookieHeader: string[] | string | null) { | |
| if (!setCookieHeader) return; | |
| const newCookies = parseSetCookie(setCookieHeader); | |
| const diff: Record<string, { old: string, new: string }> = {}; | |
| Object.entries(newCookies).forEach(([key, value]) => { | |
| if (COOKIES[key] !== value) { | |
| diff[key] = { old: COOKIES[key] || '(none)', new: value }; | |
| COOKIES[key] = value; // Merge/Update | |
| } | |
| }); | |
| if (Object.keys(diff).length > 0) { | |
| logger('INFO', 'Cookies Updated', diff); | |
| saveCookies(); | |
| } else { | |
| logger('INFO', 'No cookies were updated.'); | |
| } | |
| } | |
| // Ensure logs directory exists | |
| if (!existsSync(CONFIG.LOGS_DIR)) { | |
| mkdirSync(CONFIG.LOGS_DIR); | |
| } | |
| // --- STATE --- | |
| let lastCheckTimestamp = 0; | |
| let lastSessionCheckTimestamp = 0; | |
| let isChecking = false; | |
| // --- LOGGING --- | |
| function formatLocalISO(date: Date): string { | |
| const z = date.getTimezoneOffset() * 60000; | |
| return new Date(date.getTime() - z).toISOString().slice(0, -1); | |
| } | |
| function logger(level: 'INFO' | 'ERROR' | 'FOUND' | 'TICK', message: string, data?: any) { | |
| const now = new Date(); | |
| const timestamp = formatLocalISO(now); | |
| const dataStr = data ? ` | ${JSON.stringify(data)}` : ''; | |
| const logEntry = `[${timestamp}] [${level}] ${message}${dataStr}`; | |
| // 1. Console Output | |
| if (level === 'ERROR') { | |
| console.error(logEntry); | |
| } else { | |
| console.log(logEntry); | |
| } | |
| // 2. File Output | |
| const date = timestamp.split('T')[0]; | |
| const fileEntry = logEntry + '\n'; | |
| // Main log file (per date) | |
| const logFile = join(CONFIG.LOGS_DIR, `${date}.log`); | |
| appendFileSync(logFile, fileEntry); | |
| // Special logs | |
| if (level === 'FOUND') { | |
| appendFileSync(join(CONFIG.LOGS_DIR, 'success.log'), fileEntry); | |
| } | |
| if (level === 'ERROR') { | |
| appendFileSync(join(CONFIG.LOGS_DIR, 'error.log'), fileEntry); | |
| } | |
| } | |
| // --- MONITORING LOGIC --- | |
| function getActiveRange() { | |
| const now = new Date(); | |
| const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; | |
| return CONFIG.SCHEDULE.IMPORTANT_RANGES.find(range => { | |
| if (range.start <= range.end) { | |
| return currentTime >= range.start && currentTime <= range.end; | |
| } else { | |
| // Handle ranges crossing midnight (e.g., 23:50 - 00:10) | |
| return currentTime >= range.start || currentTime <= range.end; | |
| } | |
| }); | |
| } | |
| function getRequiredIntervalMs(): number { | |
| const range = getActiveRange(); | |
| const seconds = range ? range.intervalSec : CONFIG.SCHEDULE.NORMAL_INTERVAL_SEC; | |
| const finalSeconds = Math.max(seconds, CONFIG.SCHEDULE.MIN_INTERVAL_SEC); | |
| return finalSeconds * 1000; | |
| } | |
| async function checkSlots() { | |
| try { | |
| const response = await fetch(CONFIG.URL, { | |
| method: 'POST', | |
| headers: { | |
| ...CONFIG.STATIC_HEADERS, | |
| 'Cookie': stringifyCookies(COOKIES), | |
| }, | |
| body: JSON.stringify(CONFIG.PAYLOAD), | |
| }); | |
| const data = (await response.json()) as any; | |
| // Process and merge response cookies | |
| const newSetCookies = (response.headers as any).getSetCookie?.() || response.headers.get('set-cookie') | |
| updateCookies(newSetCookies); | |
| if (!response.ok) { | |
| const msg = `API request failed (${response.status})`; | |
| logger('ERROR', msg, data); | |
| notify('Gosuslugi Error', `${msg}: ${JSON.stringify(data)}`); | |
| process.exit(1); | |
| } | |
| const slots = Array.isArray(data) ? data : (data.slots || []); | |
| const hasSlots = slots.length > 0; | |
| if (hasSlots) { | |
| const msg = `Found ${slots.length} available slots.`; | |
| logger('FOUND', msg, data); | |
| notify('Gosuslugi Slots Found!', msg); | |
| } else { | |
| logger('INFO', 'Слотов пока нет.', data); | |
| } | |
| } catch (error) { | |
| const errorMsg = `Unexpected error: ${String(error)}`; | |
| logger('ERROR', errorMsg); | |
| notify('Gosuslugi Monitor Error', errorMsg); | |
| process.exit(1); | |
| } | |
| } | |
| function notify(title: string, message: string) { | |
| // Экранируем кавычки и спецсимволы для безопасной передачи в shell | |
| const escapedMessage = message.replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$'); | |
| const command = `notify-send "${title}" "${escapedMessage}" --urgency=critical`; | |
| exec(command, (error) => { | |
| if (error) { | |
| logger('ERROR', `Failed to send notification: ${error.message}`); | |
| } | |
| }); | |
| } | |
| // --- RUNNER --- | |
| async function tick() { | |
| if (isChecking) { | |
| return | |
| }; | |
| const now = Date.now(); | |
| const requiredIntervalMs = getRequiredIntervalMs(); | |
| const timeSinceLastCheck = now - lastCheckTimestamp; | |
| if (timeSinceLastCheck < requiredIntervalMs) { | |
| return | |
| }; | |
| const activeRange = getActiveRange(); | |
| logger('TICK', `Triggering check (Mode: ${activeRange ? 'IMPORTANT' : 'NORMAL'}, Interval: ${requiredIntervalMs / 1000}s)`); | |
| isChecking = true; | |
| try { | |
| await checkSlots(); | |
| lastCheckTimestamp = Date.now(); | |
| } finally { | |
| isChecking = false; | |
| } | |
| } | |
| setInterval(tick, CONFIG.SCHEDULE.TICK_INTERVAL_SEC * 1000); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment