Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
438 changes: 237 additions & 201 deletions README.md

Large diffs are not rendered by default.

73 changes: 59 additions & 14 deletions src/icalUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,75 @@ import { tz } from 'moment-timezone';
import { moment } from "obsidian";
import { WINDOWS_TO_IANA_TIMEZONES } from './generated/windowsTimezones';

export function extractMeetingInfo(e: any): { callUrl: string, callType: string } {
import { CallUrlPattern } from './settings/ICSSettings';

// Check for Google Meet conference data
if (e["GOOGLE-CONFERENCE"]) {
return { callUrl: e["GOOGLE-CONFERENCE"], callType: 'Google Meet' };
export function extractMeetingInfo(e: any, patterns?: CallUrlPattern[]): { callUrl: string, callType: string } {
// If patterns not provided or empty, return null (extraction disabled)
if (!patterns || patterns.length === 0) {
return { callUrl: null, callType: null };
}
// Check if the location contains a Zoom link
if (e.location && e.location.includes('zoom.us')) {
return { callUrl: e.location, callType: 'Zoom' };

// Sort patterns by priority (lower numbers = higher priority)
const sortedPatterns = patterns.sort((a, b) => a.priority - b.priority);

for (const pattern of sortedPatterns) {
const result = checkPattern(e, pattern);
if (result.callUrl) {
return result;
}
}
if (e.description) {
const skypeMatch = e.description.match(/https:\/\/join.skype.com\/[a-zA-Z0-9]+/);
if (skypeMatch) {
return { callUrl: skypeMatch[0], callType: 'Skype' };

return { callUrl: null, callType: null };
}

function checkPattern(e: any, pattern: CallUrlPattern): { callUrl: string, callType: string } {
// Special handling for Google Meet conference data
if (pattern.pattern === "GOOGLE-CONFERENCE" && e["GOOGLE-CONFERENCE"]) {
return { callUrl: e["GOOGLE-CONFERENCE"], callType: pattern.name };
}

// Check location field
if (e.location) {
const match = matchText(e.location, pattern);
if (match) {
return { callUrl: match, callType: pattern.name };
}
}

const teamsMatch = e.description.match(/(https:\/\/teams\.microsoft\.com\/l\/meetup-join\/[^>]+)/);
if (teamsMatch) {
return { callUrl: teamsMatch[0], callType: 'Microsoft Teams' };
// Check description field
if (e.description) {
const match = matchText(e.description, pattern);
if (match) {
return { callUrl: match, callType: pattern.name };
}
}

return { callUrl: null, callType: null };
}

function matchText(text: string, pattern: CallUrlPattern): string | null {
try {
if (pattern.matchType === 'contains') {
if (text.includes(pattern.pattern)) {
// For contains match, try to extract a URL from the text
const urlMatch = text.match(/https?:\/\/[^\s<>"]+/);
return urlMatch ? urlMatch[0] : text;
}
} else if (pattern.matchType === 'regex') {
const regex = new RegExp(pattern.pattern);
const match = text.match(regex);
if (match) {
return match[0];
}
}
} catch {
// Skip invalid regex patterns
console.warn(`Invalid regex pattern: ${pattern.pattern}`);
}

return null;
}

function applyRecurrenceDateAndTimezone(originalDate: Date, currentDate: Date, tzid: string): Date {
const originalMoment = tz(originalDate, tzid);

Expand Down
31 changes: 30 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Calendar,
ICSSettings,
DEFAULT_SETTINGS,
DEFAULT_VIDEO_CALL_PATTERNS,
} from "./settings/ICSSettings";

import ICSSettingsTab from "./settings/ICSSettingsTab";
Expand Down Expand Up @@ -135,7 +136,8 @@ export default class ICSPlugin extends Plugin {

try {
dateEvents.forEach((e) => {
const { callUrl, callType } = extractMeetingInfo(e);
const patterns = this.data.videoCallExtraction?.enabled ? this.data.videoCallExtraction.patterns : [];
const { callUrl, callType } = extractMeetingInfo(e, patterns);

const event: IEvent = {
utime: moment(e.start).format('X'),
Expand Down Expand Up @@ -259,6 +261,33 @@ export default class ICSPlugin extends Plugin {

async loadSettings() {
this.data = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());

// Migration: ensure videoCallExtraction settings exist and are hydrated
let needsSave = false;

if (!this.data.videoCallExtraction) {
this.data.videoCallExtraction = {
enabled: true,
patterns: [...DEFAULT_VIDEO_CALL_PATTERNS]
};
needsSave = true;
} else {
// Ensure patterns array exists
if (!this.data.videoCallExtraction.patterns) {
this.data.videoCallExtraction.patterns = [...DEFAULT_VIDEO_CALL_PATTERNS];
needsSave = true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Deep clone video-call defaults during migration

This migration still copies DEFAULT_VIDEO_CALL_PATTERNS by reference—[...DEFAULT_VIDEO_CALL_PATTERNS] creates a new array but keeps the same pattern objects. Editing or reordering patterns in the settings UI will therefore mutate the exported defaults, so “reset to defaults” no longer restores the original values and those mutations get saved back as if they were defaults. Please clone the pattern objects before storing them.

-        patterns: [...DEFAULT_VIDEO_CALL_PATTERNS]
+        patterns: DEFAULT_VIDEO_CALL_PATTERNS.map(pattern => ({ ...pattern }))

The same adjustment is needed in every branch where we hydrate videoCallExtraction.patterns.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main.ts around lines 269 to 278, the migration copies
DEFAULT_VIDEO_CALL_PATTERNS by shallow-cloning the array which keeps references
to the same pattern objects; change assignments so you deep-clone the pattern
objects (e.g. map over DEFAULT_VIDEO_CALL_PATTERNS and create new objects for
each pattern) before storing them into this.data.videoCallExtraction.patterns,
and apply the same deep-clone approach to every other branch in this file where
videoCallExtraction.patterns is hydrated during migration.

}

// Ensure enabled field exists (global setting)
if (this.data.videoCallExtraction.enabled === undefined) {
this.data.videoCallExtraction.enabled = true;
needsSave = true;
}
}

if (needsSave) {
await this.saveData(this.data);
}
}

async saveSettings() {
Expand Down
42 changes: 42 additions & 0 deletions src/settings/ICSSettings.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
export interface CallUrlPattern {
name: string;
pattern: string;
matchType: 'regex' | 'contains';
priority: number;
}

export interface ICSSettings {
format: {
timeFormat: string
dataViewSyntax: boolean,
},
calendars: Record < string, Calendar > ;
videoCallExtraction: {
enabled: boolean;
patterns: CallUrlPattern[];
};
}

export interface Calendar {
Expand Down Expand Up @@ -43,11 +54,42 @@ export const DEFAULT_CALENDAR_FORMAT = {
showTransparentEvents: false
};

export const DEFAULT_VIDEO_CALL_PATTERNS: CallUrlPattern[] = [
{
name: "Google Meet",
pattern: "GOOGLE-CONFERENCE",
matchType: "contains",
priority: 1
},
{
name: "Zoom",
pattern: "zoom.us",
matchType: "contains",
priority: 2
},
{
name: "Skype",
pattern: "https:\\/\\/join\\.skype\\.com\\/[a-zA-Z0-9]+",
matchType: "regex",
priority: 3
},
{
name: "Microsoft Teams",
pattern: "https:\\/\\/teams\\.microsoft\\.com\\/l\\/meetup-join\\/[^>]+",
matchType: "regex",
priority: 4
}
];

export const DEFAULT_SETTINGS: ICSSettings = {
format: {
timeFormat: "HH:mm",
dataViewSyntax: false,
},
calendars: {
},
videoCallExtraction: {
enabled: true,
patterns: DEFAULT_VIDEO_CALL_PATTERNS
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent default video-call patterns from being mutated at runtime

DEFAULT_SETTINGS.videoCallExtraction.patterns currently points straight at DEFAULT_VIDEO_CALL_PATTERNS. Because loadSettings does a shallow Object.assign, the plugin state shares that same array/object. As soon as we call extractMeetingInfo (which sorts in place) or the user edits/reorders a pattern, we mutate the exported defaults. “Reset to defaults” will then reapply the already-mutated data, and future migrations treat those mutations as canonical defaults. Please deep-clone the defaults before storing them (and mirror that change in the migration code so every entry is copied).

-        patterns: DEFAULT_VIDEO_CALL_PATTERNS
+        patterns: DEFAULT_VIDEO_CALL_PATTERNS.map(pattern => ({ ...pattern }))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
videoCallExtraction: {
enabled: true,
patterns: DEFAULT_VIDEO_CALL_PATTERNS
}
videoCallExtraction: {
enabled: true,
patterns: DEFAULT_VIDEO_CALL_PATTERNS.map(pattern => ({ ...pattern }))
}
🤖 Prompt for AI Agents
In src/settings/ICSSettings.ts around lines 91-94,
DEFAULT_SETTINGS.videoCallExtraction.patterns currently references
DEFAULT_VIDEO_CALL_PATTERNS directly which allows runtime mutations to alter the
exported defaults; change the assignment to deep-clone
DEFAULT_VIDEO_CALL_PATTERNS when building DEFAULT_SETTINGS (use structuredClone
or JSON.parse(JSON.stringify(...))) so the settings get their own copy, and
update the migration code that copies/initializes videoCallExtraction patterns
to also deep-clone each entry rather than copying references so migrations don’t
propagate mutated defaults.

};
Loading