关于 extension-email-calendar-events
此 Caffeine AI 扩展为应用提供日历事件管理和邮件邀请功能,适合需要组织会议并通知参会者的应用。它通过邮件发送附带 iCalendar 文件的邀请,但目前不支持接收 RSVP。
主要功能
- 01创建、更新和删除事件
- 02获取及列出日历事件
- 03通过邮件发送邀请
- 04附加 iCalendar 文件
适用场景
- 01管理应用中的会议日程
- 02向参会者发送活动邀请
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ caffeinelabs/ skills --skill extension-email-calendar-events安装后在 Claude Code 中输入/extension-email-calendar-events确认已加载。
详细介绍
邮件 — 日历事件
面向 Caffeine AI 的日历事件邮件扩展。
概述
此技能支持安排活动/会议,并通过邮件发送 iCalendar 邀请。它提供日历事件的 CRUD 操作,以及基于邮件的邀请发送功能。
后端
此组件用于安排活动/会议并通过邮件发送邀请
- 它会在内部为每位参会者的邮件创建并附加 iCalendar 文件
- 此组件目前尚不支持接收参会者的 RSVP 回复
添加/更新/取消/删除/获取/列出日历事件
- 使用预制模块
mo:caffeineai-email-calendar-events/calendarEvents.mo,该模块不可修改。
module {
public type CalendarEvent = {
uid : Text;
sequence : Nat32;
method : CalendarEventMethod;
summary : Text;
description : Text;
location : Text;
startTime : Nat64;
endTime : Nat64;
organizer : Organizer;
attendees : [Attendee];
};
public type CalendarEventMethod = {
#request;
#publish;
#cancel;
};
// The organizer is the sender: the invitation comes from
// `<fromUsername>@<app domain>` and that mailbox, under this display name,
// is the ICS ORGANIZER and the first guest. Only attendees carry addresses.
public type Organizer = {
name : ?Text;
};
public type Mailbox = {
email : Text;
name : ?Text;
};
public type Attendee = {
who : Mailbox;
role : CalendarEventRole;
};
public type CalendarEventRole = {
#chair;
#required;
#optional;
#notParticipating;
};
public type State = {
var events : List.List<CalendarEvent>;
var uidMap : Map.Map<Text, Nat>;
};
public func add(
self : State,
uid : Text,
summary : Text,
description : Text,
location : Text,
startTime : Nat64,
endTime : Nat64,
organizer : Organizer,
attendees : [Attendee]
) : ?CalendarEvent;
// update, addAttendees, removeAttendees and cancel store the change with
// the next sequence number and return the stored record. Send that record:
// calendars apply an update or a cancellation only when its sequence is
// higher than the one they hold.
public func update(
self : State,
uid : Text,
summary : ?Text,
description : ?Text,
location : ?Text,
startTime : ?Nat64,
endTime : ?Nat64,
organizer : ?Organizer,
attendees : ?[Attendee]
) : ?CalendarEvent;
public func addAttendees(
self : State,
uid : Text,
attendees : [Attendee]
) : ?CalendarEvent;
public func removeAttendees(
self : State,
uid : Text,
attendees : [Text]
) : ?CalendarEvent;
public func cancel(self : State, uid : Text) : ?CalendarEvent;
public func delete(self : State, uid : Text);
public func get(self : State, uid : Text) : ?CalendarEvent;
// Iterate over all calendar events older to newer
public func iter(self : State) : Iter.Iter<CalendarEvent>;
// Iterate over all calendar events newer to older
public func reverse(self : State) : Iter.Iter<CalendarEvent>;
// One invitation copy per attendee from `<fromUsername>@<app domain>`, the
// organizer's mailbox, through the mail gateway.
public func sendCalendarEvent(fromUsername : Text, event : CalendarEvent) : async { #ok; #err : Text };
}
通过邮件向参会者发送日历事件邀请
- 使用同一模块的
sendCalendarEvent函数。它会通过邮件网关向每位参会者发送一封邮件,使用平台在每次部署时注入的设置(INTEGRATIONS_GATEWAY_URL/INTEGRATIONS_GATEWAY_API_KEY);缺少这些设置时会返回#err。此包依赖caffeineai-email,应用不会调用它来发送日历邮件。 fromUsername仅指发件人的本地部分,例如events或no-reply。邀请来自<fromUsername>@<app domain>,应用域名由邮件网关解析,属于项目自身域名。切勿传入完整地址,也不要在应用中读取或猜测域名。- 组织者就是同一个邮箱。
CalendarEvent.organizer仅包含显示名称{ name = ?"Community Team" },该名称会作为 ICSORGANIZER和首位嘉宾显示,因此日历会将邀请显示为由其组织者发出。只有参会者才有邮箱地址;仅当组织者也列为参会者时,组织者才会收到副本。不要将组织者建模为带有电子邮件地址的用户。 - 所有参会者都接受邀请后,它会返回
#ok;否则返回包含错误文本的#err(error)。发送失败时绝不会抛出异常。
可添加/更新/取消/删除/获取/列出日历事件并通过邮件发送邀请的应用示例
import Runtime "mo:core/Runtime";
import Principal "mo:core/Principal";
import Map "mo:core/Map";
import Random "mo:core/Random";
import Set "mo:core/Set";
import Iter "mo:core/Iter";
import Option "mo:core/Option";
import Text "mo:core/Text";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import CalendarEvents "mo:caffeineai-email-calendar-events/calendarEvents";
import Uuid "mo:caffeineai-email-calendar-events/uuid";
actor {
public type UserProfile = {
name : Text;
email : Text;
};
// Include authorization component
let accessControlState : AccessControl.AccessControlState;
include MixinAuthorization(accessControlState, null);
// Store a map of caller principal to UserProfile
var userProfiles : Map.Map<Principal, UserProfile>;
// Store a set of emails for uniqueness check
var emails : Set.Set<Text>;
// Store the calendar events
let calendarEvents : CalendarEvents.State;
public shared ({ caller }) func registerUser(name : Text, email : Text) : async () {
// Check if the user already exists
if (userProfiles.containsKey(caller)) {
Runtime.trap("User already registered");
};
// Check if the email is already used
if (emails.contains(email)) {
Runtime.trap("Email already taken");
};
// Add a user record
userProfiles.add(
caller,
{
name;
email;
}
);
emails.add(email);
};
public shared ({ caller }) func addCalendarEvent(summary : Text, description : Text, location : Text, startTimeMs : Nat64, endTimeMs : Nat64) : async () {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can add calendar events");
};
let organiser = userProfiles.get(caller)
?? Runtime.trap("Admin profile not found");
let seed = await Random.blob();
let uid = Uuid.generateV4(seed);
if (
calendarEvents.add(
uid,
summary,
description,
location,
startTimeMs,
endTimeMs,
{ name = ?organiser.name },
userProfiles.values().map(
func({ name; email }) {
{
who = { name = ?name; email };
role = #required;
};
}
).toArray()
).isNull()
) {
Runtime.trap("Failed to add calendar event");
};
};
public shared ({ caller }) func updateEventDetails(uid : Text, summary : ?Text, description : ?Text, location : ?Text, startTimeMs : ?Nat64, endTimeMs : ?Nat64) : async () {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can update calendar events");
};
if (
calendarEvents.update(
uid,
summary,
description,
location,
startTimeMs,
endTimeMs,
null,
null
).isNull()
) {
Runtime.trap("Failed to update calendar event");
};
};
public shared ({ caller }) func addEventAttendees(uid : Text, attendees : [CalendarEvents.Attendee]) : async () {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can add calendar event attendees");
};
if (calendarEvents.addAttendees(uid, attendees).isNull()) {
Runtime.trap("Failed to add attendees to calendar event");
};
};
public shared ({ caller }) func removeEventAttendees(uid : Text, attendees : [Text]) : async () {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can remove calendar event attendees");
};
if (calendarEvents.removeAttendees(uid, attendees).isNull()) {
Runtime.trap("Failed to remove attendees from calendar event");
};
};
public shared ({ caller }) func cancelEvent(uid : Text) : async () {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can cancel calendar events");
};
// The returned record carries method #cancel and the bumped sequence,
// which is what tells the attendees' calendars to drop the event.
let cancelled = calendarEvents.cancel(uid)
?? Runtime.trap("Failed to cancel calendar event");
switch (await CalendarEvents.sendCalendarEvent("events", cancelled)) {
case (#ok) {};
case (#err(error)) {
Runtime.trap("Failed to send the cancellation: " # error);
};
};
};
public shared ({ caller }) func listEvents() : async [CalendarEvents.CalendarEvent] {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can list calendar events");
};
calendarEvents.iter().toArray();
};
public shared ({ caller }) func deleteEvent(uid : Text) : async () {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can delete calendar events");
};
calendarEvents.delete(uid);
};
public shared ({ caller }) func sendEventInvitation(uid : Text) : async () {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can send calendar event invitations");
};
let event = calendarEvents.get(uid)
?? Runtime.trap("Calendar event not found");
switch (await CalendarEvents.sendCalendarEvent("events", event)) {
case (#ok) {};
case (#err(error)) {
Runtime.trap("Failed to send the invitation: " # error);
};
};
};
};
迁移链的最新版本:
import Map "mo:core/Map";
import Set "mo:core/Set";
import AccessControl "mo:caffeineai-authorization/access-control";
import CalendarEvents "mo:caffeineai-email-calendar-events/calendarEvents";
module {
type UserProfile = {
name : Text;
email : Text;
};
type NewActor = {
accessControlState : AccessControl.AccessControlState;
var userProfiles : Map.Map<Principal, UserProfile>;
var emails : Set.Set<Text>;
calendarEvents : CalendarEvents.State;
};
public func migration(_old : {}) : NewActor {
{
accessControlState = AccessControl.initState();
var userProfiles = Map.empty<Principal, UserProfile>();
var emails = Set.empty<Text>();
calendarEvents = CalendarEvents.new();
};
};
};
从之前的版本升级
使用 caffeineai-email-calendar-events 0.3.x 构建的应用,需要通过 mops add [email protected] 和 mops add [email protected] 重新固定依赖。应用代码无需更改:邀请现在始终通过邮件网关发送;如果缺少网关设置,sendCalendarEvent 会返回 #err,而不会回退到传输 canister。
使用 caffeineai-email-calendar-events 0.2.x 构建的应用,需要通过 mops add [email protected] 和 mops add [email protected] 重新固定依赖。CalendarEvent.organizer 现在是 Organizer({ name : ?Text }),而不是 Mailbox:组织者地址由邮件网关构建,为发件人的 <fromUsername>@<app domain>,因此应用不再需要提供该地址。add 和 update 接受新类型;如果应用存储了事件,则需添加一个迁移步骤,将每个已存储的 organizer 映射为 { name = old.organizer.name }。
使用 caffeineai-email-calendar-events 0.1.x 构建的应用,需要通过 mops add [email protected] 和 mops add [email protected] 重新固定依赖。sendCalendarEvent 的签名不变,但它从 EmailClient 移到了此模块(CalendarEvents.sendCalendarEvent);事件类型(CalendarEvent、Attendee、Mailbox、CalendarEventMethod、CalendarEventRole)也属于此模块。如果应用从 EmailClient 导入这些类型,则需改用此模块的导入;若不再使用 EmailClient,则可移除它。现在由邮件网关负责生成并发送邀请。update、addAttendees、removeAttendees 和 cancel 现在会返回已存储的记录(序列号已递增),而不是之前的记录,因此在变更后发送返回的记录时,应用会发送正确的序列号。