import { Injectable } from '@angular/core'; import { CapacitorHttp, CapacitorCookies, HttpResponse } from '@capacitor/core'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { FileOpener } from '@capacitor-community/file-opener'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root' }) export class SiuService { constructor(private authService: AuthService) {} private readonly BASE_URL = 'https://autogestion.udelaciudad.edu.ar/caba'; async getAcademicHistory(dni: string, pass: string, carreraId: string = '30'): Promise { try { const authCookies = await this.authenticateSiu(dni, pass); const sessionCookies = await this.selectCareer(carreraId, authCookies); // 3. OBTENER PLAN DE ESTUDIO const { html: rawHtml } = await this.getHtml(`${this.BASE_URL}/plan_estudio`, sessionCookies); if (!rawHtml) { throw new Error('No se recibió contenido del plan de estudios.'); } const regex = /on_arrival\((\{.*?"id":"info_plan".*?\})\);/s; const match = rawHtml.match(regex); if (match && match[1]) { const jsonData = JSON.parse(match[1]); const contentHtml = jsonData.content; return this.parseHtmlSubjects(contentHtml); } if (rawHtml.includes('acceso?auth=form') || rawHtml.includes('id="login"')) { throw new Error('La sesión de SIU Guaraní no se pudo autenticar o expiró.'); } throw new Error('No se encontró el bloque de materias'); } catch (error) { console.error('Error SIU Academic History:', error); throw error; } } async getInscripcionesActivas(dni: string, pass: string, carreraId: string = '30'): Promise { try { const authCookies = await this.authenticateSiu(dni, pass); const sessionCookies = await this.selectCareer(carreraId, authCookies); // OBTENER PÁGINA DE CURSADA const { html: rawHtml } = await this.getHtml(`${this.BASE_URL}/cursada`, sessionCookies); if (!rawHtml) return []; // 1. Extraer directamente de "notificaciones" donde SIU provee el array JSON "asignaciones" const regexNotif = /on_arrival\((\{.*?"id":"notificaciones".*?\})\);/s; const matchNotif = rawHtml.match(regexNotif); if (matchNotif && matchNotif[1]) { try { const jsonNotif = JSON.parse(matchNotif[1]); const asignaciones = jsonNotif.info?.asignaciones; if (Array.isArray(asignaciones) && asignaciones.length > 0) { return asignaciones.map((asig: any) => ({ nombre: asig.NOMBRE_MATERIA || asig.MATERIA, codigo: asig.NOMBRE || asig.MATERIA, // ej: "ASIG00127-E1-TD" comision: asig.COMISION, dia: asig.DIA_SEMANA })); } } catch (e) { console.warn('No se pudieron leer asignaciones estructuradas:', e); } } // 2. Fallback: extraer del bloque HTML "info_materia" (tabla .js-mis-inscripciones) const regex = /on_arrival\((\{.*?"id":"info_materia".*?\})\);/s; const match = rawHtml.match(regex); if (match && match[1]) { const jsonData = JSON.parse(match[1]); const contentHtml = jsonData.content; return this.parseInscripcionesActivasHtml(contentHtml); } return []; } catch (error) { console.error('Error SIU Active Inscriptions:', error); return []; } } private async authenticateSiu(dni: string, pass: string): Promise { console.log('Autenticando en SIU...'); const loginData = `usuario=${encodeURIComponent(dni)}&password=${encodeURIComponent(pass)}&login=Ingresar`; const headers = this.getCommonHeaders(); headers['Content-Type'] = 'application/x-www-form-urlencoded'; const resLogin: HttpResponse = await CapacitorHttp.post({ url: `${this.BASE_URL}/acceso?auth=form`, data: loginData, headers }); let cookies = this.extractCookies(resLogin.headers); const stored = await this.getStoredCookies(); cookies = this.mergeCookieStrings(cookies, stored); const dataStr = this.decodeResponseData(resLogin.data); if (dataStr && (dataStr.includes('El usuario o la contraseña no son correctos') || dataStr.includes('error-login') || dataStr.includes('login_error'))) { throw new Error('DNI o Contraseña incorrectos en SIU Guaraní.'); } return cookies; } private async selectCareer(carreraId: string, currentCookies: string): Promise { console.log('Seleccionando carrera:', carreraId); const resCambio = await this.getHtml(`${this.BASE_URL}/acceso/cambiar_carrera?id=${carreraId}&op=inicio_alumno`, currentCookies); const resInicio = await this.getHtml(`${this.BASE_URL}/inicio_alumno`, resCambio.cookies); return resInicio.cookies; } private async getHtml(url: string, cookies: string): Promise<{ html: string; cookies: string }> { let updatedCookies = cookies; const res: HttpResponse = await CapacitorHttp.get({ url, headers: this.getCommonHeaders(updatedCookies) }); updatedCookies = this.mergeCookies(updatedCookies, res.headers); const stored = await this.getStoredCookies(); updatedCookies = this.mergeCookieStrings(updatedCookies, stored); let html = this.decodeResponseData(res.data); // Fallback: si html es nulo/vacío por codificación no-UTF8 (ej. ISO-8859-1 en SIU), solicitar arraybuffer y decodificar if (!html) { console.log(`Reintentando ${url} con arraybuffer para decodificación ISO-8859-1...`); const resBuffer: HttpResponse = await CapacitorHttp.get({ url, responseType: 'arraybuffer', headers: this.getCommonHeaders(updatedCookies) }); updatedCookies = this.mergeCookies(updatedCookies, resBuffer.headers); html = this.decodeBase64Response(resBuffer.data); } return { html, cookies: updatedCookies }; } private decodeResponseData(data: any): string { if (typeof data === 'string') { return data; } if (data && typeof data === 'object') { return JSON.stringify(data); } return ''; } private decodeBase64Response(base64: string): string { if (!base64 || typeof base64 !== 'string') return ''; try { const binaryString = window.atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } try { const utf8Decoder = new TextDecoder('utf-8', { fatal: true }); return utf8Decoder.decode(bytes); } catch { const latinDecoder = new TextDecoder('iso-8859-1'); return latinDecoder.decode(bytes); } } catch (e) { console.error('Error decoding base64 response:', e); return ''; } } private async getStoredCookies(): Promise { try { const cookieMap = await CapacitorCookies.getCookies({ url: this.BASE_URL }); if (cookieMap && typeof cookieMap === 'object') { return Object.entries(cookieMap) .filter(([k, v]) => k && v) .map(([k, v]) => `${k}=${v}`) .join('; '); } } catch (e) { // CapacitorCookies can fail gracefully without breaking flow } return ''; } private extractTobaContent(html: string, pageletId: string = 'pagelet'): string { const regexes = [ /kernel\.renderer\.on_arrival\((\{.*?\})\);/gs, /on_arrival\((\{.*?\})\);/gs ]; for (const regex of regexes) { let match; // Reset regex index for global searches regex.lastIndex = 0; while ((match = regex.exec(html)) !== null) { try { const jsonData = JSON.parse(match[1]); if (jsonData.id === pageletId || jsonData.info?.id === pageletId) { return jsonData.content || ''; } } catch (e) { continue; } } } return ''; } async downloadCertificate(dni: string, pass: string): Promise<{ base64: string, filename: string }> { console.log('Iniciando descarga de certificado para:', dni); try { const authCookies = await this.authenticateSiu(dni, pass); const sessionCookies = await this.selectCareer('30', authCookies); console.log('Login y selección de carrera (ID 30) listos'); // 1. VALIDAR SI EXISTE UNO VIGENTE (< 30 DÍAS) const { html: rawHtmlSolicitudes, cookies: solicitCookies } = await this.getHtml(`${this.BASE_URL}/solicitudes`, sessionCookies); console.log('Página de solicitudes obtenida'); const contentHtmlSolicitudes = this.extractTobaContent(rawHtmlSolicitudes, 'pagelet'); if (contentHtmlSolicitudes) { const existingCert = this.findValidCertificate(contentHtmlSolicitudes); if (existingCert) { console.log('Certificado vigente encontrado:', existingCert.filename); return await this.downloadFile(existingCert.url, existingCert.filename, solicitCookies); } } // 2. GENERAR NUEVA SOLICITUD (HEADLESS) console.log('No hay certificado vigente o no se pudo leer la tabla. Intentando generar uno nuevo...'); const { html: rawHtmlGestion, cookies: gestionCookies } = await this.getHtml(`${this.BASE_URL}/solicitudes/solicitud_gestion`, solicitCookies); const contentHtmlGestion = this.extractTobaContent(rawHtmlGestion, 'pagelet'); if (!contentHtmlGestion) { console.error('HTML completo de gestión no contiene on_arrival de pagelet. Muestra:', rawHtmlGestion.substring(0, 1000)); throw new Error('No se pudo localizar el generador (content missing)'); } const parser = new DOMParser(); const doc = parser.parseFromString(contentHtmlGestion, 'text/html'); const iframe = doc.querySelector('iframe#frameSolicitud') as HTMLIFrameElement; let iframeSrc = iframe?.getAttribute('src'); if (!iframeSrc) { throw new Error('No se pudo localizar el generador (iframe src missing)'); } console.log('Iframe localizado, src:', iframeSrc); // Asegurar que la URL del iframe es completa if (!iframeSrc.startsWith('http')) { const baseUrlParsed = new URL(this.BASE_URL); iframeSrc = `${baseUrlParsed.origin}${iframeSrc.startsWith('/') ? '' : '/'}${iframeSrc}`; } // Trigger the print event via URL parameters const printUrl = iframeSrc.includes('?') ? `${iframeSrc}&toba_ei_notificar=imprimir` : `${iframeSrc}?toba_ei_notificar=imprimir`; console.log('Solicitando generación de PDF:', printUrl); return await this.downloadFile(printUrl, `Certificado_Alumno_Regular_${new Date().getTime()}.pdf`, gestionCookies); } catch (error) { console.error('Error detallado en SIU Certificate:', error); throw error; } } private findValidCertificate(html: string): { url: string, filename: string } | null { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const rows = Array.from(doc.querySelectorAll('tr')); for (const row of rows) { const cells = Array.from(row.querySelectorAll('td')); if (cells.length >= 3) { const tipo = cells[0].textContent?.trim().toLowerCase() || ''; const presentarA = cells[2].textContent?.trim().toLowerCase() || ''; // Identificar constancia de alumno regular if (tipo.includes('alumno regular')) { const fechaStr = cells[1].textContent?.trim() || ''; console.log(`Revisando: ${tipo} | Para: ${presentarA} | Fecha: ${fechaStr}`); if (this.isWithin30Days(fechaStr)) { // El usuario indico que si dice "Quien corresponda" o similares (como "Google LLC") sirve. // Solo descartamos si es para un destinatario muy especifico que no sea el solicitado. const downloadLink = row.querySelector('a.descargar_cert, a[href*="descargar_comprobante"]') as HTMLAnchorElement; if (downloadLink) { return { url: downloadLink.getAttribute('href') || '', filename: `Certificado_SIU_${presentarA.replace(/\s+/g, '_')}_${fechaStr.replace(/\//g, '-')}.pdf` }; } } } } } return null; } private isWithin30Days(fechaStr: string): boolean { // Formato esperado: DD/MM/YYYY const parts = fechaStr.split('/'); if (parts.length !== 3) return false; const date = new Date(parseInt(parts[2]), parseInt(parts[1]) - 1, parseInt(parts[0])); const diff = new Date().getTime() - date.getTime(); const days = diff / (1000 * 60 * 60 * 24); return days < 30; } private async downloadFile(url: string, filename: string, cookies?: string): Promise<{ base64: string, filename: string }> { // Si la URL es relativa, completarla const fullUrl = url.startsWith('http') ? url : `${this.BASE_URL}/${url.startsWith('/') ? url.substring(1) : url}`; const response: HttpResponse = await CapacitorHttp.get({ url: fullUrl, responseType: 'blob', headers: this.getCommonHeaders(cookies) }); return { base64: response.data, // CapacitorHttp con responseType blob devuelve base64 string filename }; } private getCommonHeaders(cookies?: string): Record { const headers: Record = { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'es-AR,es;q=0.9' }; if (cookies) { headers['Cookie'] = cookies; } return headers; } private extractCookies(headers: Record | undefined): string { return this.mergeCookies('', headers); } private mergeCookies(existingCookies: string, newHeaders: Record | undefined): string { if (!newHeaders) return existingCookies; const rawSetCookie: string = newHeaders['set-cookie'] || newHeaders['Set-Cookie'] || ''; if (!rawSetCookie) return existingCookies; const cookieMap = new Map(); if (existingCookies) { existingCookies.split(';').forEach(c => { const [k, ...v] = c.trim().split('='); if (k) cookieMap.set(k.trim(), v.join('=')); }); } const newCookies = rawSetCookie.split(/,(?=[^ ])/); newCookies.forEach(c => { const cookiePart = c.trim().split(';')[0]; const [k, ...v] = cookiePart.split('='); if (k && v.length > 0) { cookieMap.set(k.trim(), v.join('=')); } }); return Array.from(cookieMap.entries()) .map(([k, v]) => `${k}=${v}`) .join('; '); } private mergeCookieStrings(cookies1: string, cookies2: string): string { if (!cookies1) return cookies2 || ''; if (!cookies2) return cookies1 || ''; const cookieMap = new Map(); cookies1.split(';').forEach(c => { const [k, ...v] = c.trim().split('='); if (k) cookieMap.set(k.trim(), v.join('=')); }); cookies2.split(';').forEach(c => { const [k, ...v] = c.trim().split('='); if (k) cookieMap.set(k.trim(), v.join('=')); }); return Array.from(cookieMap.entries()) .map(([k, v]) => `${k}=${v}`) .join('; '); } async saveAndOpenFile(base64Data: string, filename: string): Promise { try { const savedFile = await Filesystem.writeFile({ path: `Certificados/${filename}`, data: base64Data, directory: Directory.Documents, recursive: true }); await FileOpener.open({ filePath: savedFile.uri, contentType: 'application/pdf', }); } catch (error) { console.error('Error saving or opening file:', error); throw error; } } private parseInscripcionesActivasHtml(html: string): any[] { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // Buscamos los elementos que contienen las inscripciones activas const rows = Array.from(doc.querySelectorAll('.js-mis-inscripciones td, .inscripciones-lista td, .cuadro_materias td, table.tabla_cursada td')); const results: any[] = []; for (const td of rows) { const text = td.textContent?.trim() || ''; if (!text || text.length < 5) continue; // Caso 1: "Nombre Materia - ASIG00127-E1-TD" if (text.includes(' - ')) { const parts = text.split(' - '); const codigo = parts[parts.length - 1].trim(); const nombre = parts.slice(0, -1).join(' - ').trim(); if (codigo.toUpperCase().includes('ASIG') || codigo.length >= 4) { results.push({ nombre, codigo }); continue; } } // Caso 2: "Nombre Materia (ASIG00127-E1-TD)" const matchParen = text.match(/^(.*?)\s*\((ASIG[^\)]+)\)/i); if (matchParen) { results.push({ nombre: matchParen[1].trim(), codigo: matchParen[2].trim() }); continue; } // Caso 3: contiene código ASIG embebido const matchAsig = text.match(/(ASIG[A-Z0-9\-_]+)/i); if (matchAsig) { const codigo = matchAsig[1].trim(); const nombre = text.replace(codigo, '').replace(/[-–()]/g, '').trim(); results.push({ nombre, codigo }); } } return results; } private parseHtmlSubjects(html: string): any[] { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const rows = Array.from(doc.querySelectorAll('tr')); return rows.map(row => { const cols = row.querySelectorAll('td'); if (cols.length >= 6) { const nombreRaw = cols[0].textContent?.trim() || ''; return { nombre: nombreRaw.split(' (')[0], nota: cols[4].textContent?.trim(), origen: cols[5].textContent?.trim() }; } return null; }).filter(item => item && item.nombre); } }