{
  "name": "Aulas Sync",
  "id": "aulasSync24680",
  "active": true,
  "nodes": [
    {
      "parameters": {},
      "id": "trigger_aulas",
      "name": "Execute Workflow Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.UTFPR_PORTAL_BASE_URL + '/auth' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { username: $env.UTFPR_PORTAL_USERNAME, password: $env.UTFPR_PORTAL_PASSWORD } }}",
        "options": {},
        "method": "POST"
      },
      "id": "utfpr_auth",
      "name": "UTFPR Auth",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "onError": "continueRegularOutput",
      "position": [
        200,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.UTFPR_PORTAL_BASE_URL + '/dados' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $json.token }}"
            }
          ]
        },
        "options": {},
        "method": "GET"
      },
      "id": "utfpr_dados",
      "name": "UTFPR Dados",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "onError": "continueRegularOutput",
      "position": [
        400,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// UTFPR Aulas \u2014 Select student course and preserve auth token\n\nconst authPayload = $items('UTFPR Auth')[0]?.json || {};\nconst dadosPayload = $input.first().json || {};\n\nconst token = String(authPayload.token || '').trim();\nif (!token) {\n  return [{\n    json: {\n      ok: false,\n      error: 'auth_failed',\n      detail: String(authPayload.message || authPayload.error || 'Token ausente no auth').trim()\n    }\n  }];\n}\n\nconst cursosRoot = Array.isArray(dadosPayload?.cursos) ? dadosPayload.cursos : [];\nconst cursosAluno = Array.isArray(dadosPayload?.aluno?.cursos) ? dadosPayload.aluno.cursos : [];\nconst cursos = cursosRoot.length > 0 ? cursosRoot : cursosAluno;\nconst preferredCod = String($env.UTFPR_COD_ALUNO || '').trim();\n\nlet selected = null;\nif (preferredCod) {\n  selected = cursos.find(course => String(course?.alCuIdVc || '') === preferredCod) || null;\n}\nif (!selected) {\n  selected = cursos[0] || null;\n}\n\nif (!selected || !selected.alCuIdVc) {\n  return [{\n    json: {\n      ok: false,\n      error: 'course_not_found',\n      detail: 'Nao foi possivel determinar codAluno em /dados para consultar horario',\n      token\n    }\n  }];\n}\n\nreturn [{\n  json: {\n    ok: true,\n    token,\n    codAluno: String(selected.alCuIdVc),\n    cursoNome: String(selected.cursNomeVc || selected.curso || ''),\n    nomeAluno: String(dadosPayload.pessNomeVc || dadosPayload.nome || ''),\n    ra: String(dadosPayload.login || dadosPayload.ra || ''),\n    preferredCod\n  }\n}];"
      },
      "id": "select_utfpr_course",
      "name": "Select UTFPR Course",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        600,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.UTFPR_PORTAL_BASE_URL + '/' + encodeURIComponent($json.codAluno || '') + '/horario' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $json.token }}"
            }
          ]
        },
        "options": {},
        "method": "GET"
      },
      "id": "utfpr_horario",
      "name": "UTFPR Horario",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "onError": "continueRegularOutput",
      "position": [
        800,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.UTFPR_PORTAL_BASE_URL + '/' + encodeURIComponent(($items('Select UTFPR Course')[0] && $items('Select UTFPR Course')[0].json && $items('Select UTFPR Course')[0].json.codAluno) || '') + '/boletim' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + (($items('Select UTFPR Course')[0] && $items('Select UTFPR Course')[0].json && $items('Select UTFPR Course')[0].json.token) || '') }}"
            }
          ]
        },
        "options": {},
        "method": "GET"
      },
      "id": "utfpr_boletim",
      "name": "UTFPR Boletim",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "onError": "continueRegularOutput",
      "position": [
        1000,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// UTFPR Aulas \u2014 Parse auth/dados/horario/boletim payloads into reusable data\n\nconst TZ = 'America/Sao_Paulo';\nconst DAY_LABELS = {\n  1: 'DOM',\n  2: 'SEG',\n  3: 'TER',\n  4: 'QUA',\n  5: 'QUI',\n  6: 'SEX',\n  7: 'SAB'\n};\n\nconst TURN_ORDER = { M: 0, T: 1, N: 2 };\nconst SLOT_TIMES = {\n  M: {\n    1: ['07:30', '08:20'],\n    2: ['08:20', '09:10'],\n    3: ['09:10', '10:00'],\n    4: ['10:20', '11:10'],\n    5: ['11:10', '12:00'],\n    6: ['12:00', '12:50']\n  },\n  T: {\n    1: ['13:00', '13:50'],\n    2: ['13:50', '14:40'],\n    3: ['14:40', '15:30'],\n    4: ['15:50', '16:40'],\n    5: ['16:40', '17:30'],\n    6: ['17:30', '18:20']\n  },\n  N: {\n    1: ['18:40', '19:30'],\n    2: ['19:30', '20:20'],\n    3: ['20:20', '21:10'],\n    4: ['21:20', '22:10'],\n    5: ['22:10', '23:00']\n  }\n};\n\nfunction safeNumber(v, fallback = 0) {\n  const n = Number(v);\n  return Number.isFinite(n) ? n : fallback;\n}\n\nfunction toMinutes(hhmm) {\n  const [hh, mm] = String(hhmm || '00:00').split(':').map(Number);\n  if (!Number.isFinite(hh) || !Number.isFinite(mm)) return 0;\n  return hh * 60 + mm;\n}\n\nfunction countSlotsInBlock(block) {\n  const start = Number(block?.slotStart);\n  const end = Number(block?.slotEnd);\n  if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {\n    return end - start + 1;\n  }\n  return 1;\n}\n\nfunction shiftHHmm(hhmm, deltaMinutes) {\n  const total = ((toMinutes(hhmm) + deltaMinutes) % 1440 + 1440) % 1440;\n  const hh = String(Math.floor(total / 60)).padStart(2, '0');\n  const mm = String(total % 60).padStart(2, '0');\n  return `${hh}:${mm}`;\n}\n\nfunction nowInfoInTz() {\n  const now = new Date();\n  const weekdayShort = now.toLocaleDateString('en-US', {\n    timeZone: TZ,\n    weekday: 'short'\n  });\n\n  const weekdayMap = {\n    Sun: 1,\n    Mon: 2,\n    Tue: 3,\n    Wed: 4,\n    Thu: 5,\n    Fri: 6,\n    Sat: 7\n  };\n\n  const todayDayNumber = weekdayMap[weekdayShort] || 1;\n\n  const timeParts = new Intl.DateTimeFormat('pt-BR', {\n    timeZone: TZ,\n    hour: '2-digit',\n    minute: '2-digit'\n  }).formatToParts(now);\n\n  const hh = Number(timeParts.find(part => part.type === 'hour')?.value || 0);\n  const mm = Number(timeParts.find(part => part.type === 'minute')?.value || 0);\n  return {\n    todayDayNumber,\n    nowMinutes: hh * 60 + mm\n  };\n}\n\nfunction parseHoraDescr(code) {\n  const raw = String(code || '').trim().toUpperCase();\n  const match = raw.match(/^(\\d)([MTN])(\\d)$/);\n  if (!match) return null;\n\n  const dia = Number(match[1]);\n  const turno = match[2];\n  const slot = Number(match[3]);\n\n  if (!DAY_LABELS[dia]) return null;\n  if (!SLOT_TIMES[turno] || !SLOT_TIMES[turno][slot]) return null;\n\n  return { dia, turno, slot };\n}\n\nfunction parseHorarioPayload(payload) {\n  const rows = Array.isArray(payload) ? payload : [];\n  const slots = [];\n\n  rows.forEach(row => {\n    const disciplina = String(row?.discNomeVc || '').trim();\n    const cod = String(row?.discCodVelhoVc || '').trim();\n    const horarios = Array.isArray(row?.horarios) ? row.horarios : [];\n    const professores = Array.isArray(row?.professores) ? row.professores : [];\n    const professor = String(professores[0]?.pessNomeVc || '').trim();\n\n    horarios.forEach(h => {\n      const parsed = parseHoraDescr(h?.horaDescrVc);\n      if (!parsed) return;\n\n      const slotWindow = SLOT_TIMES[parsed.turno][parsed.slot];\n      const sala = String(h?.ambienteNomeVc || '').trim();\n\n      slots.push({\n        dia: parsed.dia,\n        dayLabel: DAY_LABELS[parsed.dia],\n        turno: parsed.turno,\n        slot: parsed.slot,\n        inicio: slotWindow[0],\n        fim: slotWindow[1],\n        disciplina,\n        cod,\n        sala,\n        professor\n      });\n    });\n  });\n\n  slots.sort((a, b) => {\n    if (a.dia !== b.dia) return a.dia - b.dia;\n    if (a.turno !== b.turno) return TURN_ORDER[a.turno] - TURN_ORDER[b.turno];\n    return a.slot - b.slot;\n  });\n\n  return slots;\n}\n\nfunction mergeConsecutiveSlots(slots) {\n  const merged = [];\n\n  slots.forEach(slot => {\n    const prev = merged[merged.length - 1];\n    const canMerge =\n      prev &&\n      prev.dia === slot.dia &&\n      prev.turno === slot.turno &&\n      prev.disciplina === slot.disciplina &&\n      prev.cod === slot.cod &&\n      prev.slotEnd + 1 === slot.slot;\n\n    if (!canMerge) {\n      merged.push({\n        dia: slot.dia,\n        dayLabel: slot.dayLabel,\n        turno: slot.turno,\n        slotStart: slot.slot,\n        slotEnd: slot.slot,\n        inicio: slot.inicio,\n        fim: slot.fim,\n        disciplina: slot.disciplina,\n        cod: slot.cod,\n        salas: slot.sala ? [slot.sala] : [],\n        professor: slot.professor\n      });\n      return;\n    }\n\n    prev.slotEnd = slot.slot;\n    prev.fim = slot.fim;\n\n    if (slot.sala && !prev.salas.includes(slot.sala)) {\n      prev.salas.push(slot.sala);\n    }\n\n    if (!prev.professor && slot.professor) {\n      prev.professor = slot.professor;\n    }\n  });\n\n  return merged.map(item => ({\n    dia: item.dia,\n    dayLabel: item.dayLabel,\n    dayOffset: 0,\n    turno: item.turno,\n    slotStart: item.slotStart,\n    slotEnd: item.slotEnd,\n    inicio: item.inicio,\n    fim: item.fim,\n    disciplina: item.disciplina,\n    cod: item.cod,\n    sala: item.salas.join('/'),\n    professor: item.professor || ''\n  }));\n}\n\nfunction parseBoletimPayload(payload) {\n  const rows = Array.isArray(payload) ? payload : [];\n  const map = {};\n\n  rows.forEach(row => {\n    const cod = String(row?.discCodVelhoVc || '').trim();\n    if (!cod) return;\n\n    const faltas = safeNumber(row?.faltas, 0);\n    const aulasDadas = safeNumber(row?.aulasDadas, 0);\n    const aulasPrevistas = safeNumber(row?.aulasPrevistas, 0);\n    const limiteFaltas = aulasPrevistas > 0 ? Math.floor(aulasPrevistas * 0.25) : null;\n\n    map[cod] = {\n      faltas,\n      aulasDadas,\n      aulasPrevistas,\n      limiteFaltas,\n      faltasRestantes: Number.isFinite(limiteFaltas) ? limiteFaltas - faltas : null,\n      faltaPercent: aulasDadas > 0 ? Number(((100 * faltas) / aulasDadas).toFixed(1)) : null\n    };\n  });\n\n  return map;\n}\n\nfunction collectRowsFromItems(nodeName) {\n  return $items(nodeName).flatMap(item => {\n    const json = item?.json;\n    if (Array.isArray(json)) return json;\n    if (json && typeof json === 'object') return [json];\n    return [];\n  });\n}\n\nconst authPayload = $items('UTFPR Auth')[0]?.json || {};\nconst selectedCourse = $items('Select UTFPR Course')[0]?.json || {};\nconst horarioPayload = collectRowsFromItems('UTFPR Horario');\nconst boletimPayload = collectRowsFromItems('UTFPR Boletim');\n\nif (!selectedCourse.ok) {\n  const detail = String(selectedCourse.detail || authPayload.message || authPayload.error || '').trim();\n  return [{\n    json: {\n      ok: false,\n      error: selectedCourse.error || 'utfpr_data_unavailable',\n      detail,\n      todayClasses: [],\n      todayAbsences: [],\n      hint: null\n    }\n  }];\n}\n\nconst nowInfo = nowInfoInTz();\nconst slots = parseHorarioPayload(horarioPayload);\nconst mergedBlocks = mergeConsecutiveSlots(slots);\nconst absencesByCode = parseBoletimPayload(boletimPayload);\n\nconst blocks = mergedBlocks.map(block => {\n  const dayOffset = (block.dia - nowInfo.todayDayNumber + 7) % 7;\n  const absence = block.cod ? absencesByCode[block.cod] || null : null;\n  return {\n    ...block,\n    dayOffset,\n    absence\n  };\n});\n\nblocks.sort((a, b) => {\n  if (a.dayOffset !== b.dayOffset) return a.dayOffset - b.dayOffset;\n  if (a.turno !== b.turno) return TURN_ORDER[a.turno] - TURN_ORDER[b.turno];\n  return a.slotStart - b.slotStart;\n});\n\nconst todayClasses = blocks\n  .filter(block => block.dayOffset === 0)\n  .sort((a, b) => toMinutes(a.inicio) - toMinutes(b.inicio));\n\nconst todayClassSlots = todayClasses.reduce((acc, block) => acc + countSlotsInBlock(block), 0);\n\nconst nowOrUpcomingToday = todayClasses\n  .filter(block => toMinutes(block.fim) >= nowInfo.nowMinutes)\n  .sort((a, b) => toMinutes(a.inicio) - toMinutes(b.inicio));\n\nlet hint = null;\nconst prepMinutes = safeNumber($env.UTFPR_PREP_MINUTES, 45);\nconst commuteMinutes = safeNumber($env.UTFPR_COMMUTE_MINUTES, 20);\n\nif (nowOrUpcomingToday.length > 0) {\n  const first = nowOrUpcomingToday[0];\n  hint = {\n    when: 'hoje',\n    dayLabel: first.dayLabel,\n    classStartsAt: first.inicio,\n    arrumarAt: shiftHHmm(first.inicio, -prepMinutes),\n    sairAt: shiftHHmm(first.inicio, -commuteMinutes),\n    prepMinutes,\n    commuteMinutes\n  };\n}\n\nconst todayAbsences = [];\nconst seenTodayDisciplines = new Set();\n\ntodayClasses.forEach(block => {\n  const disciplineKey = String(block.cod || block.disciplina || '').trim();\n  if (!disciplineKey || seenTodayDisciplines.has(disciplineKey)) return;\n\n  seenTodayDisciplines.add(disciplineKey);\n  const abs = block.absence || {};\n\n  todayAbsences.push({\n    cod: String(block.cod || ''),\n    disciplina: String(block.disciplina || ''),\n    faltas: safeNumber(abs.faltas, 0),\n    aulasDadas: Number.isFinite(Number(abs.aulasDadas)) ? Number(abs.aulasDadas) : null,\n    aulasPrevistas: Number.isFinite(Number(abs.aulasPrevistas)) ? Number(abs.aulasPrevistas) : null,\n    limiteFaltas: Number.isFinite(Number(abs.limiteFaltas)) ? Number(abs.limiteFaltas) : null,\n    faltasRestantes: Number.isFinite(Number(abs.faltasRestantes)) ? Number(abs.faltasRestantes) : null,\n    faltaPercent: Number.isFinite(Number(abs.faltaPercent)) ? Number(abs.faltaPercent) : null\n  });\n});\n\ntodayAbsences.sort((a, b) => String(a.disciplina).localeCompare(String(b.disciplina), 'pt-BR'));\n\nconst absencesSummary = todayAbsences.reduce(\n  (acc, row) => {\n    acc.disciplines += 1;\n    acc.totalFaltas += safeNumber(row.faltas, 0);\n    if (Number.isFinite(row.limiteFaltas)) {\n      acc.totalLimiteFaltas += row.limiteFaltas;\n    }\n    if (Number.isFinite(row.faltasRestantes)) {\n      acc.totalRestantes += row.faltasRestantes;\n    }\n    return acc;\n  },\n  { disciplines: 0, totalFaltas: 0, totalLimiteFaltas: 0, totalRestantes: 0 }\n);\n\nreturn [{\n  json: {\n    ok: true,\n    source: 'utfpr-aulas',\n    generatedAtMs: Date.now(),\n    todayDayNumber: nowInfo.todayDayNumber,\n    student: {\n      nome: String(selectedCourse.nomeAluno || ''),\n      ra: String(selectedCourse.ra || ''),\n      curso: String(selectedCourse.cursoNome || ''),\n      codAluno: String(selectedCourse.codAluno || '')\n    },\n    totals: {\n      slots: slots.length,\n      blocks: blocks.length,\n      todayBlocks: todayClasses.length,\n      todaySlots: todayClassSlots,\n      todayDisciplines: seenTodayDisciplines.size,\n      today: todayClasses.length,\n      next: 0\n    },\n    hint,\n    absencesSummary,\n    todayAbsences,\n    todayClasses,\n    nextClasses: [],\n    allBlocks: todayClasses\n  }\n}];"
      },
      "id": "parse_aulas_data",
      "name": "Parse Aulas Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// UTFPR Aulas \u2014 Format parsed classes into Telegram Markdown\n\nfunction esc(t) { return String(t).replace(/([_*`\\[\\]])/g, '\\\\$1'); }\n\nconst payload = $input.first().json || {};\n\nif (!payload.ok) {\n  const detail = String(payload.detail || '').trim();\n  let text = '*Aulas UTFPR*\\n\\n';\n  text += 'Nao foi possivel consultar o horario agora.\\n';\n  if (detail) {\n    text += `Detalhe: ${esc(detail)}\\n`;\n  }\n  text += '\\nConfira as variaveis UTFPR\\_PORTAL\\_USERNAME / UTFPR\\_PORTAL\\_PASSWORD e tente novamente.';\n  return [{ json: { text } }];\n}\n\nconst student = payload.student || {};\nconst totals = payload.totals || {};\nconst todayClasses = Array.isArray(payload.todayClasses) ? payload.todayClasses : [];\nconst todayAbsences = Array.isArray(payload.todayAbsences) ? payload.todayAbsences : [];\nconst hint = payload.hint || null;\n\nfunction countSlotsInBlock(block) {\n  const start = Number(block?.slotStart);\n  const end = Number(block?.slotEnd);\n  if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {\n    return end - start + 1;\n  }\n  return 1;\n}\n\nfunction plural(value, one, many) {\n  return Number(value) === 1 ? one : many;\n}\n\nfunction renderBlocks(blocks) {\n  return blocks\n    .map(block => {\n      const cod = esc(block.cod || 'DISC');\n      const disciplina = esc(block.disciplina || 'Sem nome');\n      const inicio = esc(block.inicio || '--:--');\n      const fim = esc(block.fim || '--:--');\n\n      const slots = countSlotsInBlock(block);\n      const details = [`${slots} ${plural(slots, 'aula', 'aulas')}`];\n\n      if (block.sala) {\n        details.push(`Sala ${esc(block.sala)}`);\n      }\n\n      if (block.professor) {\n        details.push(`Prof. ${esc(block.professor)}`);\n      }\n\n      return `\u2022 *${inicio}-${fim}* | *[${cod}]* ${disciplina}\\n  ${details.join(' | ')}`;\n    })\n    .join('\\n\\n');\n}\n\nfunction formatAbsenceStatus(row) {\n  const faltas = Number.isFinite(Number(row.faltas)) ? Number(row.faltas) : 0;\n  const limite = Number.isFinite(Number(row.limiteFaltas)) ? Number(row.limiteFaltas) : null;\n  const restantes = Number.isFinite(Number(row.faltasRestantes)) ? Number(row.faltasRestantes) : null;\n\n  let status = Number.isFinite(limite)\n    ? `Faltas: *${faltas}/${limite}*`\n    : `Faltas: *${faltas}*`;\n\n  if (Number.isFinite(limite) && Number.isFinite(restantes)) {\n    status += restantes >= 0\n      ? ` | Restam *${restantes}*`\n      : ` | Excesso *${Math.abs(restantes)}*`;\n  }\n\n  return status;\n}\n\nfunction renderAbsences(rows) {\n  return rows\n    .map(row => {\n      const cod = esc(row.cod || 'DISC');\n      const disciplina = esc(row.disciplina || 'Sem nome');\n      return `\u2022 *[${cod}]* ${disciplina}\\n  ${formatAbsenceStatus(row)}`;\n    })\n    .join('\\n\\n');\n}\n\nconst todayBlocks = Number.isFinite(Number(totals.todayBlocks))\n  ? Number(totals.todayBlocks)\n  : todayClasses.length;\nconst todaySlots = Number.isFinite(Number(totals.todaySlots))\n  ? Number(totals.todaySlots)\n  : todayClasses.reduce((acc, block) => acc + countSlotsInBlock(block), 0);\nconst todayDisciplines = Number.isFinite(Number(totals.todayDisciplines))\n  ? Number(totals.todayDisciplines)\n  : new Set(todayClasses.map(block => String(block.cod || block.disciplina || '').trim()).filter(Boolean)).size;\n\nlet text = '*Aulas UTFPR*\\n\\n';\ntext += '*RESUMO*\\n';\ntext += `\u2022 Curso: *${esc(student.curso || 'N/A')}*\\n`;\ntext += `\u2022 Hoje: *${todaySlots}* ${plural(todaySlots, 'aula', 'aulas')} em *${todayBlocks}* ${plural(todayBlocks, 'bloco', 'blocos')}\\n`;\ntext += `\u2022 Disciplinas: *${todayDisciplines}*\\n`;\n\nif (hint && hint.when === 'hoje') {\n  text += `\u2022 Dica: arrumar *${esc(hint.arrumarAt)}* | sair *${esc(hint.sairAt)}*\\n`;\n}\n\ntext += '\\n*AULAS DE HOJE*\\n';\nif (todayClasses.length === 0) {\n  text += 'Nenhuma aula para hoje.\\n';\n} else {\n  text += `${renderBlocks(todayClasses)}\\n`;\n}\n\ntext += '\\n*FREQUENCIA*\\n';\nif (todayAbsences.length === 0) {\n  text += 'Sem dados de faltas para hoje.';\n} else {\n  text += renderAbsences(todayAbsences);\n}\n\nreturn [{ json: { text: text.trim() } }];"
      },
      "id": "format_aulas",
      "name": "Format Aulas",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1400,
        0
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $env.TELEGRAM_CHAT_ID }}",
        "text": "={{ $json.text }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "id": "telegram_aulas",
      "name": "Telegram",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.1,
      "position": [
        1600,
        0
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Execute Workflow Trigger": {
      "main": [
        [
          {
            "node": "UTFPR Auth",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTFPR Auth": {
      "main": [
        [
          {
            "node": "UTFPR Dados",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTFPR Dados": {
      "main": [
        [
          {
            "node": "Select UTFPR Course",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select UTFPR Course": {
      "main": [
        [
          {
            "node": "UTFPR Horario",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTFPR Horario": {
      "main": [
        [
          {
            "node": "UTFPR Boletim",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTFPR Boletim": {
      "main": [
        [
          {
            "node": "Parse Aulas Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Aulas Data": {
      "main": [
        [
          {
            "node": "Format Aulas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Aulas": {
      "main": [
        [
          {
            "node": "Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}