{
  "name": "Учебный ICS-файл встречи",
  "active": false,
  "nodes": [
    {
      "id": "manual",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        260
      ],
      "parameters": {}
    },
    {
      "id": "sample-event",
      "name": "Sample Event",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        260
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{ json: {\n  \"title\": \"Учебная встреча\",\n  \"start_utc\": \"2026-10-20T10:00:00.000Z\",\n  \"end_utc\": \"2026-10-20T10:30:00.000Z\",\n  \"description\": \"Вымышленная встреча для проверки n8n.\",\n  \"location\": \"Учебный кабинет\",\n  \"uid\": \"ezgpt-demo-meeting-001@example.invalid\"\n} }];"
      }
    },
    {
      "id": "check-event",
      "name": "Check Event",
      "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_event_required');\nconst e = items[0].json;\nif (Object.keys(e).sort().join('|') !== 'description|end_utc|location|start_utc|title|uid') {\n  throw new Error('event_fields_invalid');\n}\nfor (const key of ['title', 'description', 'location']) {\n  const max = key === 'description' ? 1200 : 80;\n  if (typeof e[key] !== 'string' || !e[key].trim() || e[key].length > max ||\n      !e[key].isWellFormed() || /[\\u0000-\\u0009\\u000b-\\u001f\\u007f]/.test(e[key])) {\n    throw new Error('event_text_invalid');\n  }\n  if (key !== 'description' && e[key].includes('\\n')) throw new Error('single_line_required');\n}\nfor (const key of ['start_utc', 'end_utc']) {\n  const value = e[key];\n  if (typeof value !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:00\\.000Z$/.test(value) ||\n      !Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value) {\n    throw new Error('canonical_utc_required');\n  }\n}\nconst minutes = (Date.parse(e.end_utc) - Date.parse(e.start_utc)) / 60000;\nif (minutes <= 0 || minutes > 480) throw new Error('duration_invalid');\nif (typeof e.uid !== 'string' || !/^ezgpt-demo-[a-z0-9-]{1,20}@example\\.invalid$/.test(e.uid)) {\n  throw new Error('demo_uid_invalid');\n}\nreturn [{ json: e }];"
      }
    },
    {
      "id": "ics",
      "name": "Make ICS",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        660,
        260
      ],
      "parameters": {
        "operation": "iCal",
        "title": "={{$json.title}}",
        "start": "={{$json.start_utc}}",
        "end": "={{$json.end_utc}}",
        "allDay": false,
        "binaryPropertyName": "calendar",
        "additionalFields": {
          "fileName": "meeting-demo.ics",
          "description": "={{$json.description}}",
          "location": "={{$json.location}}",
          "uid": "={{$json.uid}}",
          "sequence": 0,
          "status": "CONFIRMED"
        }
      }
    },
    {
      "id": "check-file",
      "name": "Check File",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        260
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nif (items.length !== 1 || Object.keys(items[0].binary ?? {}).join('|') !== 'calendar') {\n  throw new Error('one_calendar_file_required');\n}\nconst item = items[0], meta = item.binary.calendar;\nif (meta.fileName !== 'meeting-demo.ics' || meta.mimeType !== 'text/calendar') throw new Error('calendar_metadata_invalid');\nconst bytes = await this.helpers.getBinaryDataBuffer(0, 'calendar');\nif (bytes.length < 1 || bytes.length > 10000) throw new Error('calendar_size_invalid');\nconst text = bytes.toString('utf8');\nif (!Buffer.from(text, 'utf8').equals(bytes) || bytes[0] === 239) throw new Error('calendar_utf8_invalid');\nconst e = $('Check Event').first().json;\n// Unfold content lines only for these checks, not a general iCalendar parser.\nconst lines = text.replace(/\\r\\n[ \\t]/g, '').split('\\r\\n');\nconst stamp = value => value.replaceAll('-', '').replaceAll(':', '').replace('.000', '');\nfor (const line of ['BEGIN:VCALENDAR', 'END:VCALENDAR', 'BEGIN:VEVENT', 'END:VEVENT',\n  'UID:' + e.uid, 'DTSTART:' + stamp(e.start_utc), 'DTEND:' + stamp(e.end_utc)]) {\n  if (lines.filter(value => value === line).length !== 1) throw new Error('calendar_marker_mismatch');\n}\nif (lines.some(line => /^(ATTENDEE|ORGANIZER|RRULE)(;|:)|^BEGIN:VALARM$/.test(line))) {\n  throw new Error('unexpected_calendar_action');\n}\nreturn [{ json: {\n  file_name: meta.fileName, start_utc: e.start_utc, end_utc: e.end_utc,\n  uid: e.uid, verified: true, action_performed: false, ics_text: text\n}, binary: item.binary }];"
      }
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Sample Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample Event": {
      "main": [
        [
          {
            "node": "Check Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Event": {
      "main": [
        [
          {
            "node": "Make ICS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Make ICS": {
      "main": [
        [
          {
            "node": "Check File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "UTC"
  }
}
