Skip to content

Commit 147b7b3

Browse files
authored
Merge pull request #196 from cloud-atlas-ai/feature/configurable-video-call-urls
Feature: Field Extraction
2 parents 4952f40 + 243881e commit 147b7b3

File tree

8 files changed

+1857
-750
lines changed

8 files changed

+1857
-750
lines changed

README.md

Lines changed: 287 additions & 201 deletions
Large diffs are not rendered by default.

src/IEvent.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ export interface IEvent {
1313
description: string; // Detailed description of the event
1414
format: Calendar["format"]; // Format preference for the event
1515
location: string; // Physical location where the event takes place, if applicable
16-
callUrl: string; // URL for joining online meetings/calls associated with the event
17-
callType: string; // Type of online meeting (e.g., Zoom, Skype, etc.)
16+
callUrl: string; // URL for joining online meetings/calls associated with the event (backward compatibility)
17+
callType: string; // Type of online meeting (e.g., Zoom, Skype, etc.) (backward compatibility)
18+
extractedFields: Record<string, string[]>; // Generic field extraction results
1819
organizer: IOrganizer; // Email of the organizer of the event
1920
attendees: IAttendee[]; // Array of attendees
2021
eventType: string; // Type of event (e.g., one-off, recurring, recurring override)

src/icalUtils.ts

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,94 @@ import { tz } from 'moment-timezone';
33
import { moment } from "obsidian";
44
import { WINDOWS_TO_IANA_TIMEZONES } from './generated/windowsTimezones';
55

6-
export function extractMeetingInfo(e: any): { callUrl: string, callType: string } {
6+
import { FieldExtractionPattern } from './settings/ICSSettings';
77

8-
// Check for Google Meet conference data
9-
if (e["GOOGLE-CONFERENCE"]) {
10-
return { callUrl: e["GOOGLE-CONFERENCE"], callType: 'Google Meet' };
8+
export function extractFields(e: any, patterns?: FieldExtractionPattern[]): Record<string, string[]> {
9+
// If patterns not provided or empty, return empty object
10+
if (!patterns || patterns.length === 0) {
11+
return {};
1112
}
12-
// Check if the location contains a Zoom link
13-
if (e.location && e.location.includes('zoom.us')) {
14-
return { callUrl: e.location, callType: 'Zoom' };
13+
14+
const extractedFields: Record<string, string[]> = {};
15+
16+
// Sort patterns by priority (lower numbers = higher priority)
17+
const sortedPatterns = patterns.sort((a, b) => a.priority - b.priority);
18+
19+
for (const pattern of sortedPatterns) {
20+
const matches = findPatternMatches(e, pattern);
21+
if (matches.length > 0) {
22+
const fieldName = pattern.extractedFieldName;
23+
if (!extractedFields[fieldName]) {
24+
extractedFields[fieldName] = [];
25+
}
26+
extractedFields[fieldName].push(...matches);
27+
}
28+
}
29+
30+
// Deduplicate all extracted fields
31+
for (const fieldName in extractedFields) {
32+
extractedFields[fieldName] = [...new Set(extractedFields[fieldName])];
33+
}
34+
35+
return extractedFields;
36+
}
37+
38+
function findPatternMatches(e: any, pattern: FieldExtractionPattern): string[] {
39+
const matches: string[] = [];
40+
41+
// Special handling for Google Meet conference data
42+
if (pattern.pattern === "GOOGLE-CONFERENCE" && e["GOOGLE-CONFERENCE"]) {
43+
matches.push(e["GOOGLE-CONFERENCE"]);
44+
return matches;
45+
}
46+
47+
// Check location field
48+
if (e.location) {
49+
const locationMatches = matchTextForPattern(e.location, pattern);
50+
matches.push(...locationMatches);
1551
}
52+
53+
// Check description field
1654
if (e.description) {
17-
const skypeMatch = e.description.match(/https:\/\/join.skype.com\/[a-zA-Z0-9]+/);
18-
if (skypeMatch) {
19-
return { callUrl: skypeMatch[0], callType: 'Skype' };
20-
}
55+
const descriptionMatches = matchTextForPattern(e.description, pattern);
56+
matches.push(...descriptionMatches);
57+
}
58+
59+
return matches;
60+
}
61+
62+
function matchTextForPattern(text: string, pattern: FieldExtractionPattern): string[] {
63+
const matches: string[] = [];
2164

22-
const teamsMatch = e.description.match(/(https:\/\/teams\.microsoft\.com\/l\/meetup-join\/[^>]+)/);
23-
if (teamsMatch) {
24-
return { callUrl: teamsMatch[0], callType: 'Microsoft Teams' };
65+
try {
66+
if (pattern.matchType === 'contains') {
67+
if (text.includes(pattern.pattern)) {
68+
// For contains match, try to extract URLs from the text
69+
const urlMatches = text.match(/https?:\/\/[^\s<>"]+/g);
70+
if (urlMatches) {
71+
matches.push(...urlMatches);
72+
} else {
73+
// If no URLs found, return the original text
74+
matches.push(text);
75+
}
76+
}
77+
} else if (pattern.matchType === 'regex') {
78+
const regex = new RegExp(pattern.pattern, 'g'); // Use global flag to find all matches
79+
let match;
80+
while ((match = regex.exec(text)) !== null) {
81+
// If regex has capture groups, use the first group, otherwise use full match
82+
matches.push(match[1] || match[0]);
83+
}
2584
}
85+
} catch {
86+
// Skip invalid regex patterns
87+
console.warn(`Invalid regex pattern: ${pattern.pattern}`);
2688
}
27-
return { callUrl: null, callType: null };
89+
90+
return matches;
2891
}
2992

93+
3094
function applyRecurrenceDateAndTimezone(originalDate: Date, currentDate: Date, tzid: string): Date {
3195
const originalMoment = tz(originalDate, tzid);
3296

src/main.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Calendar,
88
ICSSettings,
99
DEFAULT_SETTINGS,
10+
DEFAULT_FIELD_EXTRACTION_PATTERNS,
1011
} from "./settings/ICSSettings";
1112

1213
import ICSSettingsTab from "./settings/ICSSettingsTab";
@@ -19,7 +20,7 @@ import {
1920
Plugin,
2021
request
2122
} from 'obsidian';
22-
import { parseIcs, filterMatchingEvents, extractMeetingInfo } from './icalUtils';
23+
import { parseIcs, filterMatchingEvents, extractFields } from './icalUtils';
2324
import { IEvent } from './IEvent';
2425
import { DateNormalizer, FlexibleDateInput } from './DateNormalizer';
2526

@@ -135,7 +136,17 @@ export default class ICSPlugin extends Plugin {
135136

136137
try {
137138
dateEvents.forEach((e) => {
138-
const { callUrl, callType } = extractMeetingInfo(e);
139+
const patterns = this.data.fieldExtraction?.enabled ? this.data.fieldExtraction.patterns : [];
140+
const extractedFields = extractFields(e, patterns);
141+
142+
// Backward compatibility: extract first Video Call URL and type
143+
// Support both old singular and new plural field names
144+
const videoCallUrls = extractedFields['Video Call URLs'] || extractedFields['Video Call URL'] || [];
145+
const callUrl = videoCallUrls.length > 0 ? videoCallUrls[0] : null;
146+
147+
// For callType, we could derive it from the pattern name, but since we're going generic,
148+
// let's just use "Video Call" as a generic type when we have a URL
149+
const callType = callUrl ? "Video Call" : null;
139150

140151
const event: IEvent = {
141152
utime: moment(e.start).format('X'),
@@ -152,6 +163,7 @@ export default class ICSPlugin extends Plugin {
152163
location: e.location ? e.location : null,
153164
callUrl: callUrl,
154165
callType: callType,
166+
extractedFields: extractedFields,
155167
eventType: e.eventType,
156168
organizer: { email: e.organizer?.val?.substring(7) || null, name: e.organizer?.params?.CN || null },
157169
attendees: e.attendee ? (Array.isArray(e.attendee) ? e.attendee : [e.attendee]).map(attendee => ({
@@ -259,6 +271,45 @@ export default class ICSPlugin extends Plugin {
259271

260272
async loadSettings() {
261273
this.data = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
274+
275+
// Migration: migrate from old videoCallExtraction to new fieldExtraction
276+
let needsSave = false;
277+
278+
// If old videoCallExtraction exists, migrate it to fieldExtraction
279+
if ((this.data as any).videoCallExtraction) {
280+
const oldSettings = (this.data as any).videoCallExtraction;
281+
this.data.fieldExtraction = {
282+
enabled: oldSettings.enabled !== false,
283+
patterns: DEFAULT_FIELD_EXTRACTION_PATTERNS
284+
};
285+
delete (this.data as any).videoCallExtraction;
286+
needsSave = true;
287+
}
288+
289+
// Ensure fieldExtraction settings exist and are hydrated
290+
if (!this.data.fieldExtraction) {
291+
this.data.fieldExtraction = {
292+
enabled: true,
293+
patterns: [...DEFAULT_FIELD_EXTRACTION_PATTERNS]
294+
};
295+
needsSave = true;
296+
} else {
297+
// Ensure patterns array exists
298+
if (!this.data.fieldExtraction.patterns) {
299+
this.data.fieldExtraction.patterns = [...DEFAULT_FIELD_EXTRACTION_PATTERNS];
300+
needsSave = true;
301+
}
302+
303+
// Ensure enabled field exists
304+
if (this.data.fieldExtraction.enabled === undefined) {
305+
this.data.fieldExtraction.enabled = true;
306+
needsSave = true;
307+
}
308+
}
309+
310+
if (needsSave) {
311+
await this.saveData(this.data);
312+
}
262313
}
263314

264315
async saveSettings() {

src/settings/ICSSettings.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
export interface FieldExtractionPattern {
2+
name: string;
3+
pattern: string;
4+
matchType: 'regex' | 'contains';
5+
priority: number;
6+
extractedFieldName: string;
7+
}
8+
19
export interface ICSSettings {
210
format: {
311
timeFormat: string
412
dataViewSyntax: boolean,
513
},
614
calendars: Record < string, Calendar > ;
15+
fieldExtraction: {
16+
enabled: boolean;
17+
patterns: FieldExtractionPattern[];
18+
};
719
}
820

921
export interface Calendar {
@@ -43,11 +55,46 @@ export const DEFAULT_CALENDAR_FORMAT = {
4355
showTransparentEvents: false
4456
};
4557

58+
export const DEFAULT_FIELD_EXTRACTION_PATTERNS: FieldExtractionPattern[] = [
59+
{
60+
name: "Google Meet",
61+
pattern: "GOOGLE-CONFERENCE",
62+
matchType: "contains",
63+
priority: 1,
64+
extractedFieldName: "Video Call URLs"
65+
},
66+
{
67+
name: "Zoom",
68+
pattern: "zoom.us",
69+
matchType: "contains",
70+
priority: 2,
71+
extractedFieldName: "Video Call URLs"
72+
},
73+
{
74+
name: "Skype",
75+
pattern: "https:\\/\\/join\\.skype\\.com\\/[a-zA-Z0-9]+",
76+
matchType: "regex",
77+
priority: 3,
78+
extractedFieldName: "Video Call URLs"
79+
},
80+
{
81+
name: "Microsoft Teams",
82+
pattern: "https:\\/\\/teams\\.microsoft\\.com\\/l\\/meetup-join\\/[^>]+",
83+
matchType: "regex",
84+
priority: 4,
85+
extractedFieldName: "Video Call URLs"
86+
}
87+
];
88+
4689
export const DEFAULT_SETTINGS: ICSSettings = {
4790
format: {
4891
timeFormat: "HH:mm",
4992
dataViewSyntax: false,
5093
},
5194
calendars: {
95+
},
96+
fieldExtraction: {
97+
enabled: true,
98+
patterns: DEFAULT_FIELD_EXTRACTION_PATTERNS
5299
}
53100
};

0 commit comments

Comments
 (0)