{
  "name": "Учебное чтение события из ICS",
  "active": false,
  "nodes": [
    {
      "id": "manual",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        260
      ],
      "parameters": {}
    },
    {
      "id": "download",
      "name": "Download ICS",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        220,
        260
      ],
      "parameters": {
        "method": "GET",
        "url": "https://ezgpt.ru/storage/article-files/n8n-meeting-demo.ics",
        "authentication": "none",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": false,
              "responseFormat": "file",
              "outputPropertyName": "document"
            }
          },
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 15000,
          "allowUnauthorizedCerts": false
        }
      }
    },
    {
      "id": "check-file",
      "name": "Check File",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        260
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nif (items.length !== 1) throw new Error('one_download_required');\nconst item = items[0];\nconst type = String(item.json.headers?.['content-type'] ?? '').split(';')[0].trim().toLowerCase();\nif (item.json.statusCode !== 200 || !['text/calendar', 'application/octet-stream'].includes(type)) {\n  throw new Error('calendar_http_response_required');\n}\nif (Object.keys(item.binary ?? {}).join('|') !== 'document') throw new Error('document_required');\nconst bytes = await this.helpers.getBinaryDataBuffer(0, 'document');\nif (bytes.length < 1 || bytes.length > 8192) throw new Error('calendar_size_invalid');\nconst text = bytes.toString('utf8');\nif (!Buffer.from(text, 'utf8').equals(bytes) || text.includes('\\u0000')) throw new Error('calendar_utf8_invalid');\nconst readable = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;\nif (!readable.startsWith('BEGIN:VCALENDAR') || !readable.trimEnd().endsWith('END:VCALENDAR')) {\n  throw new Error('calendar_wrapper_required');\n}\nreturn [{ json: { source_bytes: bytes.length }, binary: item.binary }];"
      }
    },
    {
      "id": "read",
      "name": "Read ICS",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        660,
        260
      ],
      "parameters": {
        "operation": "fromIcs",
        "binaryPropertyName": "document",
        "destinationKey": "calendar",
        "options": {
          "encoding": "utf8",
          "stripBOM": true,
          "keepSource": "both"
        }
      }
    },
    {
      "id": "check-event",
      "name": "Check Event",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        260
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nif (items.length !== 1) throw new Error('one_calendar_required');\nconst item = items[0], calendar = item.json.calendar;\nif (!Number.isInteger(item.json.source_bytes) || !item.binary?.document) throw new Error('source_missing');\nif (calendar?.version !== '2.0' || !Array.isArray(calendar.events) || calendar.events.length !== 1) {\n  throw new Error('one_event_required');\n}\nconst e = calendar.events[0];\nconst keys = 'description|end|location|stamp|start|status|summary|uid';\nif (Object.keys(e).sort().join('|') !== keys) throw new Error('sample_event_fields_mismatch');\nconst expected = {\n  uid: 'ezgpt-demo-meeting-001@example.invalid', summary: 'Учебная встреча',\n  description: 'Вымышленная встреча для проверки n8n.', location: 'Учебный кабинет', status: 'CONFIRMED'\n};\nfor (const [key, value] of Object.entries(expected)) {\n  if (e[key] !== value) throw new Error('sample_value_mismatch: ' + key);\n}\nconst iso = value => {\n  if (!value || value.type !== 'DATE-TIME') throw new Error('date_time_required');\n  const date = new Date(value.date);\n  if (!Number.isFinite(date.getTime())) throw new Error('date_invalid');\n  return date.toISOString();\n};\nconst start = iso(e.start), end = iso(e.end);\niso(e.stamp);\nif (start !== '2026-10-20T10:00:00.000Z' || end !== '2026-10-20T10:30:00.000Z') {\n  throw new Error('sample_time_mismatch');\n}\nreturn [{ json: {\n  uid: e.uid, title: e.summary, description: e.description, location: e.location,\n  start_utc: start, end_utc: end, duration_minutes: (Date.parse(end) - Date.parse(start)) / 60000,\n  source_bytes: item.json.source_bytes, verified: true, action_performed: false\n}, binary: item.binary }];"
      }
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Download ICS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download ICS": {
      "main": [
        [
          {
            "node": "Check File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check File": {
      "main": [
        [
          {
            "node": "Read ICS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read ICS": {
      "main": [
        [
          {
            "node": "Check Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
