Skip to content

Commit c08d33c

Browse files
authored
Merge pull request #67 from plotday/refactor/notes
Refactor Activity and Note types
2 parents 78ed53d + edf26e5 commit c08d33c

26 files changed

Lines changed: 1544 additions & 1156 deletions

File tree

.changeset/cyan-snakes-follow.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@plotday/tool-outlook-calendar": minor
3+
"@plotday/tool-google-calendar": minor
4+
"@plotday/tool-gmail": minor
5+
"@plotday/tool-slack": minor
6+
"@plotday/twister": minor
7+
---
8+
9+
Changed: BREAKING: Refactored Activity and Note types for clarity and type safety.

tools/gmail/src/gmail-api.ts

Lines changed: 132 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import type { NewActivity } from "@plotday/twister";
2-
import { ActivityLinkType, ActivityType } from "@plotday/twister";
1+
import { ActivityType } from "@plotday/twister";
2+
import type {
3+
ActivityWithNotes,
4+
Actor,
5+
ActorId,
6+
ActorType,
7+
Note,
8+
} from "@plotday/twister";
39

410
export type GmailLabel = {
511
id: string;
@@ -234,6 +240,47 @@ export function parseEmailAddress(headerValue: string): EmailAddress {
234240
};
235241
}
236242

243+
/**
244+
* Converts an EmailAddress to an Actor.
245+
*/
246+
function emailAddressToActor(emailAddress: EmailAddress): Actor {
247+
return {
248+
id: `contact:${emailAddress.email}` as ActorId,
249+
type: 2 as ActorType, // ActorType.Contact
250+
email: emailAddress.email,
251+
name: emailAddress.name,
252+
};
253+
}
254+
255+
/**
256+
* Parses multiple email addresses from a header value (comma-separated).
257+
*/
258+
function parseEmailAddresses(headerValue: string | null): Actor[] {
259+
if (!headerValue) return [];
260+
261+
return headerValue
262+
.split(",")
263+
.map((addr) => addr.trim())
264+
.filter((addr) => addr.length > 0)
265+
.map((addr) => emailAddressToActor(parseEmailAddress(addr)));
266+
}
267+
268+
/**
269+
* Parses email addresses and returns just the ActorIds for mentions.
270+
*/
271+
function parseEmailAddressIds(headerValue: string | null): ActorId[] {
272+
if (!headerValue) return [];
273+
274+
return headerValue
275+
.split(",")
276+
.map((addr) => addr.trim())
277+
.filter((addr) => addr.length > 0)
278+
.map((addr) => {
279+
const parsed = parseEmailAddress(addr);
280+
return `contact:${parsed.email}` as ActorId;
281+
});
282+
}
283+
237284
/**
238285
* Gets a specific header value from a message
239286
*/
@@ -320,114 +367,104 @@ function extractAttachments(
320367
}
321368

322369
/**
323-
* Transforms a Gmail thread into an array of Activities
324-
* The first message is the parent, subsequent messages are replies
370+
* Transforms a Gmail thread into an ActivityWithNotes structure.
371+
* The subject becomes the Activity title, and each email becomes a Note.
325372
*/
326-
export function transformGmailThread(thread: GmailThread): NewActivity[] {
327-
if (!thread.messages || thread.messages.length === 0) return [];
373+
export function transformGmailThread(thread: GmailThread): ActivityWithNotes {
374+
if (!thread.messages || thread.messages.length === 0) {
375+
// Return empty structure for invalid threads
376+
return {
377+
id: `gmail:${thread.id}` as any,
378+
type: ActivityType.Note,
379+
author: { id: "system" as ActorId, type: 1 as ActorType, name: null },
380+
title: null,
381+
assignee: null,
382+
doneAt: null,
383+
start: null,
384+
end: null,
385+
recurrenceUntil: null,
386+
recurrenceCount: null,
387+
priority: null as any,
388+
recurrenceRule: null,
389+
recurrenceExdates: null,
390+
recurrenceDates: null,
391+
recurrence: null,
392+
occurrence: null,
393+
meta: null,
394+
mentions: null,
395+
tags: null,
396+
draft: false,
397+
private: false,
398+
notes: [],
399+
};
400+
}
328401

329-
const activities: NewActivity[] = [];
330402
const parentMessage = thread.messages[0];
331-
332-
// Extract key headers
333-
const from = getHeader(parentMessage, "From");
334403
const subject = getHeader(parentMessage, "Subject");
335-
const to = getHeader(parentMessage, "To");
336-
const cc = getHeader(parentMessage, "Cc");
337404

338-
// Parse sender
339-
const sender = from ? parseEmailAddress(from) : null;
340-
341-
// Extract body
342-
const body = extractBody(parentMessage.payload);
343-
344-
// Create parent activity
345-
const parentActivity: NewActivity = {
346-
type: ActivityType.Action,
347-
title: subject || parentMessage.snippet || "Email",
348-
note: body || parentMessage.snippet,
349-
noteType: "text",
405+
// Create Activity
406+
const activity: ActivityWithNotes = {
407+
id: `gmail:${thread.id}` as any,
408+
type: ActivityType.Note,
409+
author: { id: "system" as ActorId, type: 1 as ActorType, name: null },
410+
title: subject || "Email",
411+
assignee: null,
412+
doneAt: null,
350413
start: new Date(parseInt(parentMessage.internalDate)),
414+
end: null,
415+
recurrenceUntil: null,
416+
recurrenceCount: null,
417+
priority: null as any,
418+
recurrenceRule: null,
419+
recurrenceExdates: null,
420+
recurrenceDates: null,
421+
recurrence: null,
422+
occurrence: null,
351423
meta: {
352-
source: `gmail:${thread.id}:${parentMessage.id}`,
424+
source: `gmail:${thread.id}`,
353425
threadId: thread.id,
354-
messageId: parentMessage.id,
355-
from: sender,
356-
to,
357-
cc,
358-
labels: parentMessage.labelIds,
426+
historyId: thread.historyId,
359427
},
428+
mentions: null,
429+
tags: null,
430+
draft: false,
431+
private: false,
432+
notes: [],
360433
};
361434

362-
// Initialize links array
363-
parentActivity.links = [];
364-
365-
// Add Gmail URL as action link
366-
parentActivity.links.push({
367-
type: ActivityLinkType.external,
368-
title: "Open in Gmail",
369-
url: `https://mail.google.com/mail/u/0/#inbox/${thread.id}`,
370-
});
371-
372-
// Add attachments as links
373-
const attachments = extractAttachments(parentMessage);
374-
attachments.forEach((att) => {
375-
parentActivity.links!.push({
376-
type: ActivityLinkType.external,
377-
title: `Attachment: ${att.filename}`,
378-
url: att.url,
379-
});
380-
});
381-
382-
activities.push(parentActivity);
383-
384-
// Create activities for replies (messages after the first)
385-
for (let i = 1; i < thread.messages.length; i++) {
386-
const message = thread.messages[i];
387-
const replyFrom = getHeader(message, "From");
388-
const replySender = replyFrom ? parseEmailAddress(replyFrom) : null;
389-
const replyBody = extractBody(message.payload);
390-
391-
const replyActivity: NewActivity = {
392-
type: ActivityType.Action,
393-
title: `Re: ${subject || "Email"}`,
394-
note: replyBody || message.snippet,
395-
noteType: "text",
396-
start: new Date(parseInt(message.internalDate)),
397-
parent: { id: `gmail:${thread.id}:${parentMessage.id}` },
398-
meta: {
399-
source: `gmail:${thread.id}:${message.id}`,
400-
threadId: thread.id,
401-
messageId: message.id,
402-
from: replySender,
403-
labels: message.labelIds,
404-
},
435+
// Create Notes for all messages (including first)
436+
for (const message of thread.messages) {
437+
const from = getHeader(message, "From");
438+
const to = getHeader(message, "To");
439+
const cc = getHeader(message, "Cc");
440+
441+
const sender = from ? parseEmailAddress(from) : null;
442+
if (!sender) continue; // Skip messages without sender
443+
444+
const body = extractBody(message.payload);
445+
446+
// Combine to and cc for mentions
447+
const mentions: ActorId[] = [
448+
...parseEmailAddressIds(to),
449+
...parseEmailAddressIds(cc),
450+
];
451+
452+
const note: Note = {
453+
id: `gmail:${thread.id}:${message.id}` as any,
454+
activity: activity,
455+
author: emailAddressToActor(sender),
456+
note: body || message.snippet,
457+
links: null,
458+
mentions: mentions.length > 0 ? mentions : null,
459+
tags: null,
460+
draft: false,
461+
private: false,
405462
};
406463

407-
// Initialize links array
408-
replyActivity.links = [];
409-
410-
// Add Gmail URL as action link
411-
replyActivity.links.push({
412-
type: ActivityLinkType.external,
413-
title: "Open in Gmail",
414-
url: `https://mail.google.com/mail/u/0/#inbox/${thread.id}`,
415-
});
416-
417-
// Add attachments as links
418-
const replyAttachments = extractAttachments(message);
419-
replyAttachments.forEach((att) => {
420-
replyActivity.links!.push({
421-
type: ActivityLinkType.external,
422-
title: `Attachment: ${att.filename}`,
423-
url: att.url,
424-
});
425-
});
426-
427-
activities.push(replyActivity);
464+
activity.notes.push(note);
428465
}
429466

430-
return activities;
467+
return activity;
431468
}
432469

433470
/**

0 commit comments

Comments
 (0)