mcpField
E
extension-email-calendar-events

extension-email-calendar-events

github.com/caffeinelabs生産性228.9k 回インストール6 日前に更新
リポジトリ

extension-email-calendar-events について

Caffeine AI向けのこの拡張機能は、会議を運営するアプリにカレンダーイベント管理とメール招待を追加します。iCalendarファイル付きの招待を送信しますが、出席者からのRSVP受信にはまだ対応していません。

主な機能

  1. 01イベントの作成・更新・削除
  2. 02イベントの取得と一覧表示
  3. 03メールでの招待送信
  4. 04iCalendarファイルの添付

活用シーン

  1. 01アプリ内の会議予定の管理
  2. 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)を使い、メールゲートウェイ経由で参加者ごとに 1 通送信します。これらの設定がない場合は #err を返します。caffeineai-email はこのパッケージの依存関係であり、アプリはカレンダー関連のメール送信にこれを呼び出しません。
  • fromUsername は送信者のローカル部分のみを指定します。たとえば events や no-reply です。招待状は <fromUsername>@<app domain> から送信されます。アプリのドメインはプロジェクト固有のもので、メールゲートウェイによって解決されます。完全なアドレスを渡してはならず、アプリ内でドメインを読み取ったり推測したりしてはなりません。
  • 主催者は同じメールボックスです。CalendarEvent.organizer に設定するのは表示名 { name = ?"Community Team" } のみで、ICS の ORGANIZER と最初のゲストに表示されます。これにより、カレンダーには主催者から招待状が届いたように表示されます。アドレスを持つメールボックスは参加者だけです。主催者が参加者としても登録されている場合に限り、主催者にもコピーが届きます。主催者をメールアドレス付きのユーザーとしてモデル化しないでください。
  • すべての参加者への招待状の送信が受け付けられると #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 を返します。

caffeineai-email-calendar-events 0.2.x で構築されたアプリは、mops add [email protected] と mops add [email protected] を実行して再ピン留めします。CalendarEvent.organizer は Mailbox ではなく Organizer({ name : ?Text })になりました。主催者のアドレスは送信者の <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 は、以前のレコードではなく、保存済みのレコード(更新されたシーケンスを含む)を返すようになりました。そのため、変更後に返されたレコードを送信するアプリでは、正しいシーケンスが送信されます。

コメント