@rubytech/create-maxy-code 0.1.140 → 0.1.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -1
- package/payload/platform/lib/graph-style/dist/index.d.ts +6 -4
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +10 -23
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/index.ts +10 -20
- package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-trash/dist/index.js +6 -1
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +6 -1
- package/payload/platform/neo4j/edge-annotations.json +3 -3
- package/payload/platform/neo4j/schema.cypher +81 -119
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js +14 -8
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.d.ts +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.js +10 -7
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.js.map +1 -1
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/docs/references/linkedin-extension.md +1 -1
- package/payload/platform/plugins/docs/references/neo4j.md +3 -3
- package/payload/platform/plugins/docs/references/plugins-guide.md +3 -2
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +3 -7
- package/payload/platform/plugins/email/mcp/dist/index.js +7 -46
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.js +4 -15
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +51 -43
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js +223 -229
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js +7 -4
- package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts +14 -4
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js +69 -84
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +49 -64
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js +1 -2
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js.map +1 -1
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +1 -1
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +12 -13
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +17 -15
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +13 -7
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.d.ts +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js +101 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js +2 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +5 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +12 -10
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +7 -4
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +3 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +47 -39
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +7 -6
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +12 -11
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +59 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +326 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +31 -27
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +72 -49
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +5 -6
- package/payload/platform/plugins/memory/skills/archive-crawler/SKILL.md +1 -1
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +16 -15
- package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +8 -8
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +14 -13
- package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +13 -9
- package/payload/platform/plugins/notion-import/skills/notion-import/references/databases.md +11 -9
- package/payload/platform/plugins/notion-import/skills/notion-import/references/page-tree.md +12 -12
- package/payload/platform/plugins/scheduling/PLUGIN.md +18 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +93 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.d.ts +80 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.js +395 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.d.ts +16 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.js +111 -2
- package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.d.ts +20 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.js +63 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.js.map +1 -0
- package/payload/platform/plugins/scheduling/skills/daily-prep/SKILL.md +2 -2
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/plugins/x-import/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/x-import/PLUGIN.md +33 -0
- package/payload/platform/plugins/x-import/references/archive-shape.md +124 -0
- package/payload/platform/plugins/x-import/references/transcript-shape.md +121 -0
- package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +123 -0
- package/payload/platform/scripts/check-no-conversation-id-leaks.mjs +1 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +6 -2
- package/payload/platform/templates/specialists/agents/librarian.md +4 -4
- package/payload/server/public/assets/{admin-BM9aXUUh.js → admin-_pm0RRTE.js} +1 -1
- package/payload/server/public/assets/{data-Ds2ECtfP.js → data-b6q64Ot4.js} +1 -1
- package/payload/server/public/assets/{graph-BzxObKMM.js → graph-D1SVlBLb.js} +1 -1
- package/payload/server/public/assets/graph-labels-BiDGzVs2.js +1 -0
- package/payload/server/public/assets/{page-BlkBsjP2.js → page-BV6E4J2q.js} +1 -1
- package/payload/server/public/assets/{page-h_SA5SVQ.js → page-DwFigpyR.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +18 -21
- package/payload/server/public/assets/graph-labels-Dwgep_nr.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ics-graph-ingest.d.ts","sourceRoot":"","sources":["../../src/lib/ics-graph-ingest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAM3E,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EACF,uBAAuB,GACvB,wBAAwB,GACxB,aAAa,GACb,YAAY,CAAC;IACjB,qCAAqC;IACrC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kCAAkC;IAClC,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,mBAAmB,EAAE,MAAM,CAAC;QAC5B,oBAAoB,EAAE,MAAM,CAAC;QAC7B,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,uFAAuF;IACvF,eAAe,EAAE,oBAAoB,EAAE,CAAC;IACxC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yBAAyB,EAAE,MAAM,CAAC;IAClC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,MAAM,eAAe,GAAG,qBAAqB,GAAG,qBAAqB,CAAC;AAM5E,wBAAsB,SAAS,CAC7B,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,eAAe,CAAC,CA8D1B;AAkYD,YAAY,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph-ingest path for ICS calendar archives.
|
|
3
|
+
*
|
|
4
|
+
* .ics file ──▶ parseICS ──▶ (dry-run preview │ commit writes)
|
|
5
|
+
*
|
|
6
|
+
* Writes one `:Meeting` per VEVENT, anchors it to the importing `:AdminUser`
|
|
7
|
+
* via `[:IMPORTED]`, resolves attendees to `:Person` by lowercased email, and
|
|
8
|
+
* MERGEs `[:ATTENDED]` edges with role + partstat.
|
|
9
|
+
*
|
|
10
|
+
* Cross-account collision rule: `Person.email` is globally unique in the
|
|
11
|
+
* schema. When an attendee email already exists under a different accountId,
|
|
12
|
+
* the import skips the attendance edge (no edge written, the foreign Person
|
|
13
|
+
* is untouched) and increments `skippedCrossAccountPerson`. The Meeting still
|
|
14
|
+
* lands, anchored to the importer's AdminUser. This preserves absolute brand
|
|
15
|
+
* / account isolation while keeping the import as a whole non-fatal.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { parseICS } from "./ics.js";
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Entry point
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
export async function ingestIcs(params) {
|
|
23
|
+
const start = Date.now();
|
|
24
|
+
const importId = randomUUID();
|
|
25
|
+
const { fileContent, filename, mode, accountId, userId, sessionId, session } = params;
|
|
26
|
+
const parsed = parseICS(fileContent);
|
|
27
|
+
// Partition events into valid / skipped buckets. parseICS already rejects
|
|
28
|
+
// VEVENTs without DTSTART (they land in parseErrors); the parser stops short
|
|
29
|
+
// of inferring skip-reason buckets, so we classify here too. UID is required
|
|
30
|
+
// for the (accountId, uid) composite key.
|
|
31
|
+
const valid = [];
|
|
32
|
+
let skippedNoUid = 0;
|
|
33
|
+
let skippedNoStartTime = 0;
|
|
34
|
+
for (const ev of parsed.events) {
|
|
35
|
+
if (!ev.dtstart) {
|
|
36
|
+
skippedNoStartTime++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!ev.uid) {
|
|
40
|
+
skippedNoUid++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
valid.push(ev);
|
|
44
|
+
}
|
|
45
|
+
// Events the parser already rejected for missing DTSTART surface via
|
|
46
|
+
// parseErrors. Count those too so the operator-visible signal matches the
|
|
47
|
+
// file's actual content.
|
|
48
|
+
for (const err of parsed.parseErrors) {
|
|
49
|
+
if (err.includes("missing required DTSTART"))
|
|
50
|
+
skippedNoStartTime++;
|
|
51
|
+
}
|
|
52
|
+
if (mode === "dry-run") {
|
|
53
|
+
return dryRunPreview({
|
|
54
|
+
session,
|
|
55
|
+
accountId,
|
|
56
|
+
valid,
|
|
57
|
+
filename,
|
|
58
|
+
importId,
|
|
59
|
+
skippedNoUid,
|
|
60
|
+
skippedNoStartTime,
|
|
61
|
+
parseErrors: parsed.parseErrors,
|
|
62
|
+
eventsParsed: parsed.events.length,
|
|
63
|
+
start,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return commit({
|
|
67
|
+
session,
|
|
68
|
+
accountId,
|
|
69
|
+
userId,
|
|
70
|
+
sessionId,
|
|
71
|
+
valid,
|
|
72
|
+
filename,
|
|
73
|
+
importId,
|
|
74
|
+
skippedNoUid,
|
|
75
|
+
skippedNoStartTime,
|
|
76
|
+
parseErrors: parsed.parseErrors,
|
|
77
|
+
eventsParsed: parsed.events.length,
|
|
78
|
+
start,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function dryRunPreview(ctx) {
|
|
82
|
+
// Flatten attendees across valid events. Track raw counts so the preview
|
|
83
|
+
// numbers match what the commit phase would emit.
|
|
84
|
+
let attendees = 0;
|
|
85
|
+
let organizers = 0;
|
|
86
|
+
let nonMailto = 0;
|
|
87
|
+
const addressOrder = []; // dedup-preserving insertion order
|
|
88
|
+
const addressSeen = new Set();
|
|
89
|
+
const addressDisplayName = new Map();
|
|
90
|
+
for (const ev of ctx.valid) {
|
|
91
|
+
for (const a of ev.attendees) {
|
|
92
|
+
if (a.role === "organizer")
|
|
93
|
+
organizers++;
|
|
94
|
+
else
|
|
95
|
+
attendees++;
|
|
96
|
+
if (!a.address) {
|
|
97
|
+
nonMailto++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!addressSeen.has(a.address)) {
|
|
101
|
+
addressSeen.add(a.address);
|
|
102
|
+
addressOrder.push(a.address);
|
|
103
|
+
}
|
|
104
|
+
if (a.displayName && !addressDisplayName.has(a.address)) {
|
|
105
|
+
addressDisplayName.set(a.address, a.displayName);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// One round-trip to classify every unique address against the live graph.
|
|
110
|
+
const existing = new Map();
|
|
111
|
+
if (addressOrder.length > 0) {
|
|
112
|
+
const result = await ctx.session.run(`
|
|
113
|
+
UNWIND $emails AS email
|
|
114
|
+
MATCH (p:Person {email: email})
|
|
115
|
+
RETURN p.email AS email, p.accountId AS accountId, p.displayName AS displayName
|
|
116
|
+
`, { emails: addressOrder });
|
|
117
|
+
for (const rec of result.records) {
|
|
118
|
+
existing.set(rec.get("email"), {
|
|
119
|
+
accountId: rec.get("accountId") ?? null,
|
|
120
|
+
displayName: rec.get("displayName") ?? null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
let existingSameAccount = 0;
|
|
125
|
+
let existingCrossAccount = 0;
|
|
126
|
+
let willCreate = 0;
|
|
127
|
+
const sample = [];
|
|
128
|
+
const SAMPLE_CAP = 20;
|
|
129
|
+
for (const address of addressOrder) {
|
|
130
|
+
const hit = existing.get(address);
|
|
131
|
+
let status;
|
|
132
|
+
let existingDisplayName;
|
|
133
|
+
if (!hit) {
|
|
134
|
+
willCreate++;
|
|
135
|
+
status = "will-create";
|
|
136
|
+
}
|
|
137
|
+
else if (hit.accountId === ctx.accountId) {
|
|
138
|
+
existingSameAccount++;
|
|
139
|
+
status = "existing-same-account";
|
|
140
|
+
if (hit.displayName)
|
|
141
|
+
existingDisplayName = hit.displayName;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
existingCrossAccount++;
|
|
145
|
+
status = "existing-cross-account";
|
|
146
|
+
if (hit.displayName)
|
|
147
|
+
existingDisplayName = hit.displayName;
|
|
148
|
+
}
|
|
149
|
+
if (sample.length < SAMPLE_CAP) {
|
|
150
|
+
const entry = { address, status };
|
|
151
|
+
const dn = addressDisplayName.get(address);
|
|
152
|
+
if (dn)
|
|
153
|
+
entry.displayName = dn;
|
|
154
|
+
if (existingDisplayName)
|
|
155
|
+
entry.existingDisplayName = existingDisplayName;
|
|
156
|
+
sample.push(entry);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Non-mailto attendees are interesting enough to sample at least one.
|
|
160
|
+
if (nonMailto > 0 && sample.length < SAMPLE_CAP) {
|
|
161
|
+
for (const ev of ctx.valid) {
|
|
162
|
+
for (const a of ev.attendees) {
|
|
163
|
+
if (a.address)
|
|
164
|
+
continue;
|
|
165
|
+
sample.push({
|
|
166
|
+
address: a.rawValue,
|
|
167
|
+
status: "non-mailto",
|
|
168
|
+
...(a.displayName ? { displayName: a.displayName } : {}),
|
|
169
|
+
});
|
|
170
|
+
if (sample.length >= SAMPLE_CAP)
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (sample.length >= SAMPLE_CAP)
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const ms = Date.now() - ctx.start;
|
|
178
|
+
process.stderr.write(`[ics-ingest] importId=${ctx.importId} filename=${ctx.filename} mode=dry-run ` +
|
|
179
|
+
`events=${ctx.eventsParsed} meetings=${ctx.valid.length} ` +
|
|
180
|
+
`attendees=${attendees} organizers=${organizers} nonMailto=${nonMailto} ` +
|
|
181
|
+
`existingSame=${existingSameAccount} existingCross=${existingCrossAccount} ` +
|
|
182
|
+
`willCreate=${willCreate} skippedNoUid=${ctx.skippedNoUid} ` +
|
|
183
|
+
`skippedNoStartTime=${ctx.skippedNoStartTime} ms=${ms}\n`);
|
|
184
|
+
return {
|
|
185
|
+
mode: "dry-run",
|
|
186
|
+
filename: ctx.filename,
|
|
187
|
+
importId: ctx.importId,
|
|
188
|
+
events: ctx.eventsParsed,
|
|
189
|
+
validMeetings: ctx.valid.length,
|
|
190
|
+
skippedNoUid: ctx.skippedNoUid,
|
|
191
|
+
skippedNoStartTime: ctx.skippedNoStartTime,
|
|
192
|
+
preview: {
|
|
193
|
+
attendees,
|
|
194
|
+
organizers,
|
|
195
|
+
nonMailtoAttendees: nonMailto,
|
|
196
|
+
existingSameAccount,
|
|
197
|
+
existingCrossAccount,
|
|
198
|
+
willCreate,
|
|
199
|
+
},
|
|
200
|
+
sampleAttendees: sample,
|
|
201
|
+
parseErrors: ctx.parseErrors,
|
|
202
|
+
ms,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async function commit(ctx) {
|
|
206
|
+
// Resolve the importing AdminUser once. We refuse to start the commit if
|
|
207
|
+
// the userId doesn't resolve — anchoring every Meeting to a real AdminUser
|
|
208
|
+
// is what keeps :Meeting from being an orphan.
|
|
209
|
+
const adminCheck = await ctx.session.run(`MATCH (a:AdminUser {userId: $userId}) RETURN elementId(a) AS id LIMIT 1`, { userId: ctx.userId });
|
|
210
|
+
if (adminCheck.records.length === 0) {
|
|
211
|
+
throw new Error(`ics-ingest: no :AdminUser found for userId=${ctx.userId}. ` +
|
|
212
|
+
`The importer must exist in the graph before ingesting an .ics archive.`);
|
|
213
|
+
}
|
|
214
|
+
let meetingsCreated = 0;
|
|
215
|
+
let meetingsUpdated = 0;
|
|
216
|
+
let peopleCreated = 0;
|
|
217
|
+
let existingPersonsLinked = 0;
|
|
218
|
+
let skippedCrossAccountPerson = 0;
|
|
219
|
+
let attendees = 0;
|
|
220
|
+
let organizers = 0;
|
|
221
|
+
// One transaction per meeting keeps the blast radius small — if attendee
|
|
222
|
+
// resolution fails on event #4000, events 1..3999 are already durably
|
|
223
|
+
// committed. Calendar archives are long flat lists, not interlinked.
|
|
224
|
+
for (const ev of ctx.valid) {
|
|
225
|
+
const meetingProps = {
|
|
226
|
+
uid: ev.uid,
|
|
227
|
+
title: ev.summary,
|
|
228
|
+
startsAt: ev.dtstart,
|
|
229
|
+
endsAt: ev.dtend ?? null,
|
|
230
|
+
location: ev.location ?? null,
|
|
231
|
+
description: ev.description ?? null,
|
|
232
|
+
isAllDay: ev.isAllDay,
|
|
233
|
+
sourceFilename: ctx.filename,
|
|
234
|
+
accountId: ctx.accountId,
|
|
235
|
+
sessionId: ctx.sessionId ?? null,
|
|
236
|
+
};
|
|
237
|
+
const meetingResult = await ctx.session.executeWrite(async (tx) => {
|
|
238
|
+
const r = await tx.run(`
|
|
239
|
+
MATCH (a:AdminUser {userId: $userId})
|
|
240
|
+
MERGE (m:Meeting {accountId: $accountId, uid: $props.uid})
|
|
241
|
+
ON CREATE SET
|
|
242
|
+
m.title = $props.title,
|
|
243
|
+
m.startsAt = datetime($props.startsAt),
|
|
244
|
+
m.endsAt = CASE WHEN $props.endsAt IS NULL THEN null ELSE datetime($props.endsAt) END,
|
|
245
|
+
m.location = $props.location,
|
|
246
|
+
m.description = $props.description,
|
|
247
|
+
m.isAllDay = $props.isAllDay,
|
|
248
|
+
m.source = 'ics',
|
|
249
|
+
m.sourceFilename = $props.sourceFilename,
|
|
250
|
+
m.scope = 'admin',
|
|
251
|
+
m.createdAt = datetime(),
|
|
252
|
+
m.updatedAt = datetime(),
|
|
253
|
+
m.createdByAgent = 'ics-ingest',
|
|
254
|
+
m.createdBySource = 'ics-ingest',
|
|
255
|
+
m.createdBySession = $sessionId,
|
|
256
|
+
m._created = true
|
|
257
|
+
ON MATCH SET
|
|
258
|
+
m.title = $props.title,
|
|
259
|
+
m.startsAt = datetime($props.startsAt),
|
|
260
|
+
m.endsAt = CASE WHEN $props.endsAt IS NULL THEN null ELSE datetime($props.endsAt) END,
|
|
261
|
+
m.location = $props.location,
|
|
262
|
+
m.description = $props.description,
|
|
263
|
+
m.isAllDay = $props.isAllDay,
|
|
264
|
+
m.sourceFilename = $props.sourceFilename,
|
|
265
|
+
m.updatedAt = datetime(),
|
|
266
|
+
m._created = false
|
|
267
|
+
WITH a, m, m._created AS created
|
|
268
|
+
REMOVE m._created
|
|
269
|
+
MERGE (a)-[imp:IMPORTED]->(m)
|
|
270
|
+
ON CREATE SET imp.createdAt = datetime(), imp.source = 'ics-ingest'
|
|
271
|
+
RETURN created, elementId(m) AS meetingId
|
|
272
|
+
`, {
|
|
273
|
+
userId: ctx.userId,
|
|
274
|
+
accountId: ctx.accountId,
|
|
275
|
+
props: meetingProps,
|
|
276
|
+
sessionId: ctx.sessionId ?? null,
|
|
277
|
+
});
|
|
278
|
+
const created = r.records[0].get("created");
|
|
279
|
+
const meetingId = r.records[0].get("meetingId");
|
|
280
|
+
return { created, meetingId };
|
|
281
|
+
});
|
|
282
|
+
if (meetingResult.created)
|
|
283
|
+
meetingsCreated++;
|
|
284
|
+
else
|
|
285
|
+
meetingsUpdated++;
|
|
286
|
+
// Attendee + organizer resolution. We separate organizers from attendees
|
|
287
|
+
// only for counting; the edge written is :ATTENDED in both cases, with the
|
|
288
|
+
// role property carrying the distinction.
|
|
289
|
+
for (const att of ev.attendees) {
|
|
290
|
+
if (att.role === "organizer")
|
|
291
|
+
organizers++;
|
|
292
|
+
else
|
|
293
|
+
attendees++;
|
|
294
|
+
if (!att.address)
|
|
295
|
+
continue; // non-mailto: counted but no graph effect
|
|
296
|
+
const personResult = await ctx.session.executeWrite(async (tx) => {
|
|
297
|
+
// Pre-flight existence check so we never silently create an :ATTENDED
|
|
298
|
+
// edge from a foreign-accountId Person.
|
|
299
|
+
const lookup = await tx.run(`MATCH (p:Person {email: $email}) RETURN p.accountId AS accountId LIMIT 1`, { email: att.address });
|
|
300
|
+
const existingAccountId = lookup.records[0]?.get("accountId");
|
|
301
|
+
if (existingAccountId && existingAccountId !== ctx.accountId) {
|
|
302
|
+
return { outcome: "cross-account-skip" };
|
|
303
|
+
}
|
|
304
|
+
if (existingAccountId === ctx.accountId) {
|
|
305
|
+
// Same-account Person: link without overwriting their identity.
|
|
306
|
+
await tx.run(`
|
|
307
|
+
MATCH (p:Person {email: $email})
|
|
308
|
+
MATCH (m:Meeting) WHERE elementId(m) = $meetingId
|
|
309
|
+
MERGE (p)-[r:ATTENDED]->(m)
|
|
310
|
+
ON CREATE SET r.role = $role, r.partstat = $partstat,
|
|
311
|
+
r.createdAt = datetime(),
|
|
312
|
+
r.createdBySource = 'ics-ingest'
|
|
313
|
+
ON MATCH SET r.role = $role, r.partstat = $partstat
|
|
314
|
+
FOREACH (_ IN CASE WHEN p.displayName IS NULL AND $displayName IS NOT NULL THEN [1] ELSE [] END |
|
|
315
|
+
SET p.displayName = $displayName
|
|
316
|
+
)
|
|
317
|
+
`, {
|
|
318
|
+
email: att.address,
|
|
319
|
+
meetingId: meetingResult.meetingId,
|
|
320
|
+
role: att.role,
|
|
321
|
+
partstat: att.partstat ?? null,
|
|
322
|
+
displayName: att.displayName ?? null,
|
|
323
|
+
});
|
|
324
|
+
return { outcome: "linked-existing" };
|
|
325
|
+
}
|
|
326
|
+
// Brand new Person stub under the importer's account.
|
|
327
|
+
await tx.run(`
|
|
328
|
+
MATCH (m:Meeting) WHERE elementId(m) = $meetingId
|
|
329
|
+
CREATE (p:Person {
|
|
330
|
+
email: $email,
|
|
331
|
+
accountId: $accountId,
|
|
332
|
+
scope: 'admin',
|
|
333
|
+
source: 'ics-ingest',
|
|
334
|
+
displayName: $displayName,
|
|
335
|
+
createdAt: datetime(),
|
|
336
|
+
createdByAgent: 'ics-ingest',
|
|
337
|
+
createdBySource: 'ics-ingest',
|
|
338
|
+
createdBySession: $sessionId
|
|
339
|
+
})
|
|
340
|
+
CREATE (p)-[:ATTENDED {
|
|
341
|
+
role: $role,
|
|
342
|
+
partstat: $partstat,
|
|
343
|
+
createdAt: datetime(),
|
|
344
|
+
createdBySource: 'ics-ingest'
|
|
345
|
+
}]->(m)
|
|
346
|
+
`, {
|
|
347
|
+
email: att.address,
|
|
348
|
+
accountId: ctx.accountId,
|
|
349
|
+
displayName: att.displayName ?? null,
|
|
350
|
+
sessionId: ctx.sessionId ?? null,
|
|
351
|
+
meetingId: meetingResult.meetingId,
|
|
352
|
+
role: att.role,
|
|
353
|
+
partstat: att.partstat ?? null,
|
|
354
|
+
});
|
|
355
|
+
return { outcome: "person-created" };
|
|
356
|
+
});
|
|
357
|
+
if (personResult.outcome === "cross-account-skip") {
|
|
358
|
+
skippedCrossAccountPerson++;
|
|
359
|
+
}
|
|
360
|
+
else if (personResult.outcome === "linked-existing") {
|
|
361
|
+
existingPersonsLinked++;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
peopleCreated++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const ms = Date.now() - ctx.start;
|
|
369
|
+
process.stderr.write(`[ics-ingest] importId=${ctx.importId} filename=${ctx.filename} mode=commit ` +
|
|
370
|
+
`events=${ctx.eventsParsed} meetings=${ctx.valid.length} ` +
|
|
371
|
+
`meetingsCreated=${meetingsCreated} meetingsUpdated=${meetingsUpdated} ` +
|
|
372
|
+
`peopleCreated=${peopleCreated} existingPersonsLinked=${existingPersonsLinked} ` +
|
|
373
|
+
`attendees=${attendees} organizers=${organizers} ` +
|
|
374
|
+
`skippedCrossAccountPerson=${skippedCrossAccountPerson} ` +
|
|
375
|
+
`skippedNoUid=${ctx.skippedNoUid} skippedNoStartTime=${ctx.skippedNoStartTime} ` +
|
|
376
|
+
`ms=${ms}\n`);
|
|
377
|
+
return {
|
|
378
|
+
mode: "commit",
|
|
379
|
+
filename: ctx.filename,
|
|
380
|
+
importId: ctx.importId,
|
|
381
|
+
events: ctx.eventsParsed,
|
|
382
|
+
meetingsCreated,
|
|
383
|
+
meetingsUpdated,
|
|
384
|
+
peopleCreated,
|
|
385
|
+
existingPersonsLinked,
|
|
386
|
+
attendees,
|
|
387
|
+
organizers,
|
|
388
|
+
skippedNoUid: ctx.skippedNoUid,
|
|
389
|
+
skippedNoStartTime: ctx.skippedNoStartTime,
|
|
390
|
+
skippedCrossAccountPerson,
|
|
391
|
+
parseErrors: ctx.parseErrors,
|
|
392
|
+
ms,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
//# sourceMappingURL=ics-graph-ingest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ics-graph-ingest.js","sourceRoot":"","sources":["../../src/lib/ics-graph-ingest.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,6 +20,18 @@ export interface ICSEvent {
|
|
|
20
20
|
recurrence?: string;
|
|
21
21
|
timezone?: string;
|
|
22
22
|
}
|
|
23
|
+
export type AttendeeRole = "organizer" | "chair" | "req-participant" | "opt-participant" | "non-participant";
|
|
24
|
+
export interface ParsedAttendee {
|
|
25
|
+
/** Lowercased email local+domain from `mailto:` URI; null for non-mailto values (rooms, resources, groups). */
|
|
26
|
+
address: string | null;
|
|
27
|
+
/** CN parameter, unescaped. */
|
|
28
|
+
displayName?: string;
|
|
29
|
+
role: AttendeeRole;
|
|
30
|
+
/** PARTSTAT parameter, lowercased (e.g. "accepted", "declined", "tentative", "needs-action"). */
|
|
31
|
+
partstat?: string;
|
|
32
|
+
/** Raw URI value, kept verbatim for visibility on non-mailto entries. */
|
|
33
|
+
rawValue: string;
|
|
34
|
+
}
|
|
23
35
|
export interface ParsedEvent {
|
|
24
36
|
summary: string;
|
|
25
37
|
dtstart: string;
|
|
@@ -28,6 +40,10 @@ export interface ParsedEvent {
|
|
|
28
40
|
description?: string;
|
|
29
41
|
uid?: string;
|
|
30
42
|
recurrence?: string;
|
|
43
|
+
/** True when DTSTART carries `VALUE=DATE` (RFC 5545 §3.3.4 date-only). */
|
|
44
|
+
isAllDay: boolean;
|
|
45
|
+
/** Every ATTENDEE plus the ORGANIZER if present. Empty array if none. */
|
|
46
|
+
attendees: ParsedAttendee[];
|
|
31
47
|
}
|
|
32
48
|
export interface ParseResult {
|
|
33
49
|
events: ParsedEvent[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ics.d.ts","sourceRoot":"","sources":["../../src/lib/ics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ics.d.ts","sourceRoot":"","sources":["../../src/lib/ics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,iBAAiB,GACjB,iBAAiB,CAAC;AAEtB,MAAM,WAAW,cAAc;IAC7B,+GAA+G;IAC/G,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,iGAAiG;IACjG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,SAAS,EAAE,cAAc,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AA6GD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAkCrD;AAmCD,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,MAAM,CA6BjF;AAiFD,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAkJrD"}
|
|
@@ -275,6 +275,13 @@ export function parseICS(content) {
|
|
|
275
275
|
let inVEvent = false;
|
|
276
276
|
let nestedDepth = 0; // tracks BEGIN:VALARM, BEGIN:VTIMEZONE etc. inside VEVENT
|
|
277
277
|
let currentProps = new Map();
|
|
278
|
+
// ATTENDEE may repeat per VEVENT (Map.set would overwrite). Keep raw lines
|
|
279
|
+
// and parse at END:VEVENT.
|
|
280
|
+
let attendeeLines = [];
|
|
281
|
+
let organizerLine = null;
|
|
282
|
+
// VALUE=DATE on DTSTART signals an all-day event (RFC 5545 §3.3.4).
|
|
283
|
+
// The Map-based prop store strips parameters, so we detect this inline.
|
|
284
|
+
let dtStartIsDate = false;
|
|
278
285
|
let eventIndex = 0;
|
|
279
286
|
for (const line of rawLines) {
|
|
280
287
|
const trimmed = line.trim();
|
|
@@ -282,6 +289,9 @@ export function parseICS(content) {
|
|
|
282
289
|
inVEvent = true;
|
|
283
290
|
nestedDepth = 0;
|
|
284
291
|
currentProps = new Map();
|
|
292
|
+
attendeeLines = [];
|
|
293
|
+
organizerLine = null;
|
|
294
|
+
dtStartIsDate = false;
|
|
285
295
|
eventIndex++;
|
|
286
296
|
continue;
|
|
287
297
|
}
|
|
@@ -306,9 +316,22 @@ export function parseICS(content) {
|
|
|
306
316
|
parseErrors.push(`VEVENT #${eventIndex}: missing required DTSTART property`);
|
|
307
317
|
continue;
|
|
308
318
|
}
|
|
319
|
+
const attendees = [];
|
|
320
|
+
if (organizerLine) {
|
|
321
|
+
const a = parseAttendeeLine(organizerLine, "organizer");
|
|
322
|
+
if (a)
|
|
323
|
+
attendees.push(a);
|
|
324
|
+
}
|
|
325
|
+
for (const al of attendeeLines) {
|
|
326
|
+
const a = parseAttendeeLine(al, null);
|
|
327
|
+
if (a)
|
|
328
|
+
attendees.push(a);
|
|
329
|
+
}
|
|
309
330
|
const parsed = {
|
|
310
331
|
summary: summary ? unescapeText(summary) : "(untitled)",
|
|
311
332
|
dtstart: fromICSDateTime(extractDateValue(dtstart)),
|
|
333
|
+
isAllDay: dtStartIsDate,
|
|
334
|
+
attendees,
|
|
312
335
|
};
|
|
313
336
|
const dtend = currentProps.get("DTEND");
|
|
314
337
|
if (dtend)
|
|
@@ -334,9 +357,23 @@ export function parseICS(content) {
|
|
|
334
357
|
if (colonIdx === -1)
|
|
335
358
|
continue;
|
|
336
359
|
const propFull = line.substring(0, colonIdx);
|
|
337
|
-
const value = line.substring(colonIdx + 1);
|
|
338
|
-
// Strip parameters (e.g., DTSTART;TZID=Europe/London:20260413...)
|
|
339
360
|
const propName = propFull.split(";")[0].trim().toUpperCase();
|
|
361
|
+
// ATTENDEE may repeat; ORGANIZER appears at most once. Route both away
|
|
362
|
+
// from the single-value currentProps Map so re-occurrences aren't lost.
|
|
363
|
+
if (propName === "ATTENDEE") {
|
|
364
|
+
attendeeLines.push(line);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (propName === "ORGANIZER") {
|
|
368
|
+
organizerLine = line;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
// DTSTART;VALUE=DATE:20260413 → all-day event. Detect before stripping
|
|
372
|
+
// parameters.
|
|
373
|
+
if (propName === "DTSTART" && /;VALUE=DATE(?:[;:]|$)/i.test(propFull)) {
|
|
374
|
+
dtStartIsDate = true;
|
|
375
|
+
}
|
|
376
|
+
const value = line.substring(colonIdx + 1);
|
|
340
377
|
currentProps.set(propName, value);
|
|
341
378
|
}
|
|
342
379
|
}
|
|
@@ -359,4 +396,76 @@ export function parseICS(content) {
|
|
|
359
396
|
function extractDateValue(value) {
|
|
360
397
|
return value.trim();
|
|
361
398
|
}
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// ATTENDEE / ORGANIZER parsing (RFC 5545 §3.8.4)
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
const ROLE_NORMALISE = {
|
|
403
|
+
CHAIR: "chair",
|
|
404
|
+
"REQ-PARTICIPANT": "req-participant",
|
|
405
|
+
"OPT-PARTICIPANT": "opt-participant",
|
|
406
|
+
"NON-PARTICIPANT": "non-participant",
|
|
407
|
+
};
|
|
408
|
+
/**
|
|
409
|
+
* Parse one raw `ATTENDEE` or `ORGANIZER` line.
|
|
410
|
+
*
|
|
411
|
+
* `forcedRole` is `"organizer"` when parsing the ORGANIZER property (the role
|
|
412
|
+
* isn't carried in the line itself — it's the property name). For ATTENDEE
|
|
413
|
+
* lines pass `null` and the ROLE parameter (or its absence) decides.
|
|
414
|
+
*
|
|
415
|
+
* Returns null only if the line has no colon at all (malformed).
|
|
416
|
+
*/
|
|
417
|
+
function parseAttendeeLine(line, forcedRole) {
|
|
418
|
+
const colonIdx = line.indexOf(":");
|
|
419
|
+
if (colonIdx === -1)
|
|
420
|
+
return null;
|
|
421
|
+
const propFull = line.substring(0, colonIdx);
|
|
422
|
+
const value = line.substring(colonIdx + 1).trim();
|
|
423
|
+
// Params live between the property name and the first colon, semicolon-
|
|
424
|
+
// separated. e.g. `ATTENDEE;CN="Alice";ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED`
|
|
425
|
+
const paramSegments = propFull.split(";").slice(1);
|
|
426
|
+
const params = new Map();
|
|
427
|
+
for (const seg of paramSegments) {
|
|
428
|
+
const eq = seg.indexOf("=");
|
|
429
|
+
if (eq === -1)
|
|
430
|
+
continue;
|
|
431
|
+
const k = seg.substring(0, eq).trim().toUpperCase();
|
|
432
|
+
let v = seg.substring(eq + 1).trim();
|
|
433
|
+
// Strip surrounding double quotes from quoted-string param values.
|
|
434
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2)
|
|
435
|
+
v = v.slice(1, -1);
|
|
436
|
+
params.set(k, v);
|
|
437
|
+
}
|
|
438
|
+
// Address resolution. Only `mailto:` URIs become graph identities; other
|
|
439
|
+
// schemes (urn:uuid:..., resource://room/...) are recorded with null and
|
|
440
|
+
// surfaced in the dry-run preview rather than dropped.
|
|
441
|
+
let address = null;
|
|
442
|
+
const mailtoMatch = value.match(/^mailto:(.+)$/i);
|
|
443
|
+
if (mailtoMatch) {
|
|
444
|
+
address = mailtoMatch[1].trim().toLowerCase();
|
|
445
|
+
if (!address)
|
|
446
|
+
address = null;
|
|
447
|
+
}
|
|
448
|
+
// Role: ORGANIZER is fixed; ATTENDEE uses ROLE param (default REQ-PARTICIPANT
|
|
449
|
+
// per RFC 5545 §3.2.18).
|
|
450
|
+
let role;
|
|
451
|
+
if (forcedRole) {
|
|
452
|
+
role = forcedRole;
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
const roleParam = params.get("ROLE")?.toUpperCase() ?? "REQ-PARTICIPANT";
|
|
456
|
+
role = ROLE_NORMALISE[roleParam] ?? "req-participant";
|
|
457
|
+
}
|
|
458
|
+
const attendee = {
|
|
459
|
+
address,
|
|
460
|
+
role,
|
|
461
|
+
rawValue: value,
|
|
462
|
+
};
|
|
463
|
+
const cn = params.get("CN");
|
|
464
|
+
if (cn)
|
|
465
|
+
attendee.displayName = unescapeText(cn);
|
|
466
|
+
const partstat = params.get("PARTSTAT");
|
|
467
|
+
if (partstat)
|
|
468
|
+
attendee.partstat = partstat.toLowerCase();
|
|
469
|
+
return attendee;
|
|
470
|
+
}
|
|
362
471
|
//# sourceMappingURL=ics.js.map
|