const express = require('express'); const axios = require('axios'); const cors = require('cors'); const crypto = require('crypto'); const app = express(); const PORT = 3000; // Configuration const ZYBOOKS_CONFIG = { BASE_URL: 'https://zyserver.zybooks.com/v1', TIME_URL: 'https://zyserver2.zybooks.com/v1', LEARN_URL: 'https://learn.zybooks.com', HEADERS: { 'Host': 'zyserver.zybooks.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'en-US,en;q=0.5', 'Content-Type': 'application/json', 'Origin': 'https://learn.zybooks.com', 'Referer': 'https://learn.zybooks.com/' } }; // Middleware app.use(cors()); app.use(express.json()); app.use(express.static('public')); // Global error handlers process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); }); // ==================== UTILITY FUNCTIONS ==================== function generateTimestamp(timeSpent = 0) { const now = new Date(); const future = new Date(now.getTime() + timeSpent * 1000); const ms = String(Math.floor(Math.random() * 1000)).padStart(3, '0'); return future.toISOString().replace(/\.\d{3}Z$/, `${ms}Z`); } function generateChecksum(actId, timestamp, auth, part, buildkey) { const data = `content_resource/${actId}/activity${timestamp}${auth}${actId}${part}true${buildkey}`; return crypto.createHash('md5').update(data).digest('hex'); } async function getBuildkey(axiosInstance) { const response = await axiosInstance.get(ZYBOOKS_CONFIG.LEARN_URL); const match = response.data.match(/zybooks-web\/config\/environment[^>]*content="([^"]*)/); const decoded = decodeURIComponent(match[1]); const config = JSON.parse(decoded); return config.APP.BUILDKEY; } async function calculateSectionParts(bookCode, chapterNumber, sectionNumber, authToken, axiosInstance) { try { const problemsResponse = await axiosInstance.get( `${ZYBOOKS_CONFIG.BASE_URL}/zybook/${bookCode}/chapter/${chapterNumber}/section/${sectionNumber}?auth_token=${authToken}` ); if (!problemsResponse.data || !problemsResponse.data.section) { throw new Error('Invalid response: missing section data'); } const problems = problemsResponse.data.section.content_resources; if (!problems || !Array.isArray(problems)) { throw new Error('Invalid response: missing or invalid content_resources'); } const totalParts = problems.reduce((total, problem) => total + Math.max(problem.parts, 1), 0); return { problems, totalParts }; } catch (error) { console.error('Error in calculateSectionParts:', error.message); throw error; } } // Error response helper function sendError(res, statusCode, message) { console.error(`Error [${statusCode}]:`, message); res.status(statusCode).json({ success: false, message }); } // SSE message helpers function sendSSEMessage(res, type, data) { res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); } // Core function to solve a single part async function solvePart(actId, secId, auth, part, code, axiosInstance, timeSpent = 0) { // Simulate time spent const spentTime = Math.floor(Math.random() * 60) + 1; const newTimeSpent = timeSpent + spentTime; // Send time record await axiosInstance.post(`${ZYBOOKS_CONFIG.TIME_URL}/zybook/${code}/time_spent`, { time_spent_records: [{ canonical_section_id: secId, content_resource_id: actId, part: part, time_spent: spentTime, timestamp: generateTimestamp(newTimeSpent) }], auth_token: auth }); // Get buildkey and generate checksum const buildkey = await getBuildkey(axiosInstance); const timestamp = generateTimestamp(newTimeSpent); const checksum = generateChecksum(actId, timestamp, auth, part, buildkey); // Solve the problem const response = await axiosInstance.post( `${ZYBOOKS_CONFIG.BASE_URL}/content_resource/${actId}/activity`, { part: part, complete: true, metadata: "{}", zybook_code: code, auth_token: auth, timestamp: timestamp, __cs__: checksum }, { headers: ZYBOOKS_CONFIG.HEADERS } ); return { success: response.data.success || false, timeSpent: newTimeSpent }; } // ==================== API ROUTES ==================== // Login app.post('/api/auth/login', async (req, res) => { try { const { email, password } = req.body; if (!email || !password) { return sendError(res, 400, 'Email and password are required'); } const tempSession = axios.create(); const response = await tempSession.post(`${ZYBOOKS_CONFIG.BASE_URL}/signin`, { email, password }); if (!response.data.success || !response.data.session) { return sendError(res, 401, 'Invalid credentials'); } res.json({ success: true, auth_token: response.data.session.auth_token, user_id: response.data.session.user_id }); } catch (error) { sendError(res, 500, 'Login failed - Server error'); } }); // Get books app.get('/api/books/:userId', async (req, res) => { try { const { userId } = req.params; const { auth_token } = req.query; if (!userId || !auth_token) { return sendError(res, 400, 'User ID and auth token are required'); } const axiosInstance = axios.create(); const response = await axiosInstance.get( `${ZYBOOKS_CONFIG.BASE_URL}/user/${userId}/items?items=%5B%22zybooks%22%5D&auth_token=${auth_token}` ); const books = response.data.items.zybooks.filter(book => !book.autosubscribe); res.json({ success: true, books }); } catch (error) { sendError(res, 500, 'Failed to get books'); } }); // Get chapters app.get('/api/chapters/:bookCode', async (req, res) => { try { const { bookCode } = req.params; const { auth_token } = req.query; if (!bookCode || !auth_token) { return sendError(res, 400, 'Book code and auth token are required'); } const axiosInstance = axios.create(); const response = await axiosInstance.get( `${ZYBOOKS_CONFIG.BASE_URL}/zybooks?zybooks=%5B%22${bookCode}%22%5D&auth_token=${auth_token}` ); res.json({ success: true, chapters: response.data.zybooks[0].chapters }); } catch (error) { sendError(res, 500, 'Failed to get chapters'); } }); // Get section parts count app.get('/api/section-parts/:bookCode/:chapterNumber/:sectionNumber', async (req, res) => { try { const { bookCode, chapterNumber, sectionNumber } = req.params; const { auth_token } = req.query; if (!bookCode || !chapterNumber || !sectionNumber || !auth_token) { return sendError(res, 400, 'Missing required parameters'); } const axiosInstance = axios.create(); const { totalParts } = await calculateSectionParts(bookCode, chapterNumber, sectionNumber, auth_token, axiosInstance); res.json({ success: true, totalParts }); } catch (error) { sendError(res, 500, 'Failed to get section parts'); } }); // Solve section with SSE app.post('/api/solve/section', async (req, res) => { // Set SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' }); try { const { bookCode, chapter, section, auth, timeSpent = 0 } = req.body; if (!bookCode || !chapter || !section || !auth) { sendSSEMessage(res, 'error', { message: 'Missing required parameters' }); return res.end(); } console.log(`Solving section: ${chapter.number}.${section.number}`); const axiosInstance = axios.create(); let currentTimeSpent = timeSpent; // Send start message sendSSEMessage(res, 'start', { message: `Starting section: ${chapter.number}.${section.number} - ${section.title}` }); // Get problems and total parts const { problems, totalParts } = await calculateSectionParts(bookCode, chapter.number, section.number, auth, axiosInstance); sendSSEMessage(res, 'total', { total: totalParts }); let currentPart = 0; let results = []; // Process each problem for (let i = 0; i < problems.length; i++) { const problem = problems[i]; const parts = Math.max(problem.parts, 1); // Process each part for (let partIndex = 0; partIndex < parts; partIndex++) { currentPart++; const partNum = partIndex + 1; const actualPartIndex = problem.parts > 0 ? partIndex : 0; // Send progress const progressMessage = parts > 1 ? `Solving problem ${i + 1} part ${partNum}...` : `Solving problem ${i + 1}...`; sendSSEMessage(res, 'progress', { current: currentPart, total: totalParts, message: progressMessage }); // Solve the part const result = await solvePart(problem.id, section.canonical_section_id, auth, actualPartIndex, bookCode, axiosInstance, currentTimeSpent); currentTimeSpent = result.timeSpent; // Update time spent results.push({ problem: i + 1, part: partNum, success: result.success }); // Send result sendSSEMessage(res, 'result', { current: currentPart, total: totalParts, problem: i + 1, part: partNum, success: result.success, timeSpent: currentTimeSpent, message: result.success ? `✅ Problem ${i + 1} part ${partNum} solved successfully` : `❌ Problem ${i + 1} part ${partNum} failed` }); console.log(`Problem ${i + 1}, Part ${partNum}: ${result.success ? 'Success' : 'Failed'}`); await new Promise(resolve => setTimeout(resolve, 500)); } } // Send completion const successCount = results.filter(r => r.success).length; sendSSEMessage(res, 'complete', { results, successCount, totalCount: results.length, finalTimeSpent: currentTimeSpent, message: `Completed! ${successCount}/${results.length} problems solved successfully` }); res.end(); } catch (error) { console.error('Section solving error:', error.message); sendSSEMessage(res, 'error', { message: `Error solving section: ${error.message}` }); res.end(); } }); // Start server app.listen(PORT, () => { console.log(`🚀 ZyBook Auto Server running on http://localhost:${PORT}`); });