Initial backup 2026-02-17
This commit is contained in:
310
skills/lark-calendar/lib/calendar.mjs
Normal file
310
skills/lark-calendar/lib/calendar.mjs
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Lark Calendar Event Operations
|
||||
*/
|
||||
|
||||
import { larkApi } from './lark-api.mjs';
|
||||
import { ensureBoyangIncluded, getDisplayName } from './employees.mjs';
|
||||
|
||||
// Default calendar ID (Claw calendar)
|
||||
export const DEFAULT_CALENDAR_ID = 'feishu.cn_caF80RJxgGcbBGsQx64bCh@group.calendar.feishu.cn';
|
||||
|
||||
// Default timezone
|
||||
export const DEFAULT_TIMEZONE = 'Asia/Singapore';
|
||||
|
||||
/**
|
||||
* Convert datetime string to Unix timestamp
|
||||
* @param {string} timeStr - Format: YYYY-MM-DD HH:MM:SS
|
||||
* @param {string} timezone - IANA timezone (e.g., Asia/Singapore)
|
||||
* @returns {number} - Unix timestamp in seconds
|
||||
*/
|
||||
export function datetimeToTimestamp(timeStr, timezone = DEFAULT_TIMEZONE) {
|
||||
// Parse the datetime string
|
||||
const [datePart, timePart] = timeStr.split(' ');
|
||||
const [year, month, day] = datePart.split('-').map(Number);
|
||||
const [hour, minute, second] = (timePart || '00:00:00').split(':').map(Number);
|
||||
|
||||
// Create date in the specified timezone
|
||||
// JavaScript Date uses local time, so we need to handle timezone offset
|
||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second || 0).padStart(2, '0')}`;
|
||||
|
||||
// Use Intl to get the timezone offset
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: timezone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
|
||||
// Create a date object assuming the input is in the target timezone
|
||||
// We need to find the UTC equivalent
|
||||
const localDate = new Date(dateStr);
|
||||
|
||||
// Get the offset for this timezone at this date
|
||||
const utcDate = new Date(localDate.toLocaleString('en-US', { timeZone: 'UTC' }));
|
||||
const tzDate = new Date(localDate.toLocaleString('en-US', { timeZone: timezone }));
|
||||
const offset = utcDate - tzDate;
|
||||
|
||||
// Adjust and return timestamp
|
||||
return Math.floor((localDate.getTime() + offset) / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Unix timestamp to datetime string
|
||||
* @param {number} timestamp - Unix timestamp in seconds
|
||||
* @param {string} timezone - IANA timezone
|
||||
* @returns {string} - Format: YYYY-MM-DD HH:MM:SS
|
||||
*/
|
||||
export function timestampToDatetime(timestamp, timezone = DEFAULT_TIMEZONE) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
const formatter = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: timezone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(date);
|
||||
const values = {};
|
||||
for (const part of parts) {
|
||||
values[part.type] = part.value;
|
||||
}
|
||||
|
||||
return `${values.year}-${values.month}-${values.day} ${values.hour}:${values.minute}:${values.second}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a calendar event
|
||||
* @param {object} options
|
||||
* @param {string} options.title - Event title
|
||||
* @param {string} [options.description] - Event description
|
||||
* @param {string} options.startTime - Start time (YYYY-MM-DD HH:MM:SS)
|
||||
* @param {string} options.endTime - End time (YYYY-MM-DD HH:MM:SS)
|
||||
* @param {string[]} [options.attendeeIds] - Array of user_ids
|
||||
* @param {string} [options.location] - Event location
|
||||
* @param {string} [options.timezone] - IANA timezone
|
||||
* @param {string} [options.calendarId] - Calendar ID
|
||||
* @returns {object} - Created event data
|
||||
*/
|
||||
export async function createEvent({
|
||||
title,
|
||||
description = '',
|
||||
startTime,
|
||||
endTime,
|
||||
attendeeIds = [],
|
||||
location = '',
|
||||
timezone = DEFAULT_TIMEZONE,
|
||||
calendarId = DEFAULT_CALENDAR_ID
|
||||
}) {
|
||||
// Always include Boyang
|
||||
const finalAttendeeIds = ensureBoyangIncluded(attendeeIds);
|
||||
|
||||
// Convert times to timestamps
|
||||
const startTimestamp = datetimeToTimestamp(startTime, timezone);
|
||||
const endTimestamp = datetimeToTimestamp(endTime, timezone);
|
||||
|
||||
// Build location params
|
||||
const locationParams = location ? { name: location } : null;
|
||||
|
||||
// Create the event
|
||||
const eventData = await larkApi('POST', `/calendar/v4/calendars/${calendarId}/events`, {
|
||||
data: {
|
||||
summary: title,
|
||||
description: description,
|
||||
start_time: { timestamp: String(startTimestamp), timezone },
|
||||
end_time: { timestamp: String(endTimestamp), timezone },
|
||||
visibility: 'private',
|
||||
attendee_ability: 'can_modify_event',
|
||||
free_busy_status: 'busy',
|
||||
reminders: [],
|
||||
schemas: [],
|
||||
attachments: [],
|
||||
color: -1,
|
||||
recurrence: null,
|
||||
location: locationParams
|
||||
}
|
||||
});
|
||||
|
||||
const event = eventData.event;
|
||||
|
||||
// Add attendees if any
|
||||
let attendees = [];
|
||||
if (finalAttendeeIds.length > 0) {
|
||||
const attendeesData = await addEventAttendees(calendarId, event.event_id, finalAttendeeIds);
|
||||
attendees = attendeesData.attendees || [];
|
||||
}
|
||||
|
||||
return {
|
||||
event,
|
||||
attendees,
|
||||
attendeeNames: attendees.map(a => a.display_name).join(', ')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a calendar event
|
||||
* @param {object} options
|
||||
* @param {string} options.eventId - Event ID
|
||||
* @param {string} [options.title] - Event title
|
||||
* @param {string} [options.description] - Event description
|
||||
* @param {string} [options.startTime] - Start time (YYYY-MM-DD HH:MM:SS)
|
||||
* @param {string} [options.endTime] - End time (YYYY-MM-DD HH:MM:SS)
|
||||
* @param {string} [options.location] - Event location
|
||||
* @param {string} [options.timezone] - IANA timezone
|
||||
* @param {string} [options.calendarId] - Calendar ID
|
||||
* @returns {object} - Updated event data
|
||||
*/
|
||||
export async function updateEvent({
|
||||
eventId,
|
||||
title,
|
||||
description,
|
||||
startTime,
|
||||
endTime,
|
||||
location,
|
||||
timezone = DEFAULT_TIMEZONE,
|
||||
calendarId = DEFAULT_CALENDAR_ID
|
||||
}) {
|
||||
const updateData = {};
|
||||
|
||||
if (title !== undefined) updateData.summary = title;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (location !== undefined) updateData.location = location ? { name: location } : null;
|
||||
|
||||
if (startTime) {
|
||||
const startTimestamp = datetimeToTimestamp(startTime, timezone);
|
||||
updateData.start_time = { timestamp: String(startTimestamp), timezone };
|
||||
}
|
||||
|
||||
if (endTime) {
|
||||
const endTimestamp = datetimeToTimestamp(endTime, timezone);
|
||||
updateData.end_time = { timestamp: String(endTimestamp), timezone };
|
||||
}
|
||||
|
||||
const result = await larkApi('PATCH', `/calendar/v4/calendars/${calendarId}/events/${eventId}`, {
|
||||
data: updateData
|
||||
});
|
||||
|
||||
return result.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a calendar event
|
||||
* @param {string} eventId - Event ID
|
||||
* @param {string} [calendarId] - Calendar ID
|
||||
* @param {boolean} [notify] - Send notification to attendees
|
||||
* @returns {boolean} - Success
|
||||
*/
|
||||
export async function deleteEvent(eventId, calendarId = DEFAULT_CALENDAR_ID, notify = true) {
|
||||
await larkApi('DELETE', `/calendar/v4/calendars/${calendarId}/events/${eventId}`, {
|
||||
params: { need_notification: String(notify) }
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add attendees to an event
|
||||
* @param {string} calendarId - Calendar ID
|
||||
* @param {string} eventId - Event ID
|
||||
* @param {string[]} userIds - Array of user_ids
|
||||
* @returns {object} - Attendees data
|
||||
*/
|
||||
export async function addEventAttendees(calendarId, eventId, userIds) {
|
||||
const attendees = userIds.map(userId => ({
|
||||
type: 'user',
|
||||
is_optional: true,
|
||||
user_id: userId
|
||||
}));
|
||||
|
||||
return larkApi('POST', `/calendar/v4/calendars/${calendarId}/events/${eventId}/attendees`, {
|
||||
params: { user_id_type: 'user_id' },
|
||||
data: {
|
||||
attendees,
|
||||
need_notification: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove attendees from an event
|
||||
* @param {string} calendarId - Calendar ID
|
||||
* @param {string} eventId - Event ID
|
||||
* @param {string[]} userIds - Array of user_ids to remove
|
||||
* @returns {object} - Result
|
||||
*/
|
||||
export async function removeEventAttendees(calendarId, eventId, userIds) {
|
||||
const deleteIds = userIds.map(userId => ({
|
||||
type: 'user',
|
||||
is_optional: true,
|
||||
user_id: userId
|
||||
}));
|
||||
|
||||
return larkApi('POST', `/calendar/v4/calendars/${calendarId}/events/${eventId}/attendees/batch_delete`, {
|
||||
params: { user_id_type: 'user_id' },
|
||||
data: {
|
||||
delete_ids: deleteIds,
|
||||
need_notification: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List events in a calendar
|
||||
* @param {object} options
|
||||
* @param {string} [options.calendarId] - Calendar ID
|
||||
* @param {string} [options.startTime] - Start of range (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)
|
||||
* @param {string} [options.endTime] - End of range
|
||||
* @param {string} [options.timezone] - Timezone
|
||||
* @returns {object[]} - Array of events
|
||||
*/
|
||||
export async function listEvents({
|
||||
calendarId = DEFAULT_CALENDAR_ID,
|
||||
startTime,
|
||||
endTime,
|
||||
timezone = DEFAULT_TIMEZONE
|
||||
} = {}) {
|
||||
// Default to next 7 days if not specified
|
||||
const now = new Date();
|
||||
if (!startTime) {
|
||||
startTime = now.toISOString().split('T')[0] + ' 00:00:00';
|
||||
} else if (!startTime.includes(' ')) {
|
||||
startTime += ' 00:00:00';
|
||||
}
|
||||
|
||||
if (!endTime) {
|
||||
const weekLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
endTime = weekLater.toISOString().split('T')[0] + ' 23:59:59';
|
||||
} else if (!endTime.includes(' ')) {
|
||||
endTime += ' 23:59:59';
|
||||
}
|
||||
|
||||
const startTs = datetimeToTimestamp(startTime, timezone);
|
||||
const endTs = datetimeToTimestamp(endTime, timezone);
|
||||
|
||||
const result = await larkApi('GET', `/calendar/v4/calendars/${calendarId}/events`, {
|
||||
params: {
|
||||
start_time: String(startTs),
|
||||
end_time: String(endTs),
|
||||
page_size: 50
|
||||
}
|
||||
});
|
||||
|
||||
return result.items || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single event
|
||||
* @param {string} eventId - Event ID
|
||||
* @param {string} [calendarId] - Calendar ID
|
||||
* @returns {object} - Event data
|
||||
*/
|
||||
export async function getEvent(eventId, calendarId = DEFAULT_CALENDAR_ID) {
|
||||
const result = await larkApi('GET', `/calendar/v4/calendars/${calendarId}/events/${eventId}`);
|
||||
return result.event;
|
||||
}
|
||||
Reference in New Issue
Block a user