kintoneからGaroonスケジュールの添付ファイルを更新したい

kintoneにスケジュール管理アプリを作成し、レコード作成/編集完了時にGaroonのスケジュールに
登録する処理を考えております。

以下のコードでスケジュールへ登録は実現できたのですが、更新が上手く行きません。
コンソールエラー無し・ステータスコード200で処理は正常に完了するのですが、
Garoon側の添付ファイルは差し替え前のまま更新されない状態です。(その他の項目は問題なく更新されます)

原因がわかる方がおりましたらご教示いただけますと幸いです。

・参考にしたドキュメント :予定を更新する

(() => {
  'use strict';

  // リクエストトークン取得
  const getRequestToken = async () => {
    const body =
      '<?xml version="1.0" encoding="UTF-8"?>' +
      '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:schedule_services="http://wsdl.cybozu.co.jp/schedule/2008">' +
      '<SOAP-ENV:Header>' +
      '<Action SOAP-ENV:mustUnderstand="1" xmlns="http://schemas.xmlsoap.org/ws/2003/03/addressing">UtilGetRequestToken</Action>' +
      '<Timestamp SOAP-ENV:mustUnderstand="1" Id="id" xmlns="http://schemas.xmlsoap.org/ws/2002/07/utility">' +
      '<Created>2037-08-12T14:45:00Z</Created>' +
      '<Expires>2037-08-12T14:45:00Z</Expires>' +
      '</Timestamp>' +
      '<Locale>jp</Locale>' +
      '</SOAP-ENV:Header>' +
      '<SOAP-ENV:Body>' +
      '<UtilGetRequestToken>' +
      '<parameters></parameters>' +
      '</UtilGetRequestToken>' +
      '</SOAP-ENV:Body>' +
      '</SOAP-ENV:Envelope>';

    const resp = await fetch('/g/util_api/util/api.csp?', {
      method: 'POST',
      body: body,
    });

    if (!resp.ok) alert(resp.statusText);

    const parser = new DOMParser();
    const respText = await resp.text();
    const doc = parser.parseFromString(respText, 'text/xml');
    const token = doc.querySelector('request_token').textContent;

    return token;
  };
  
  kintone.events.on(['app.record.create.submit.success', 'app.record.edit.submit.success'], async event => {
    const record = event.record;
    const appId = kintone.app.getId();
    const recordId = record['$id'].value;
    const scheduleId = record['予定ID'].value; // 数値フィールド
    const title = record['タイトル'].value; // 文字列一行フィールド
    const users = record['参加者'].value; // ユーザー選択フィールド
    const start = record['開始日時'].value; // 日時フィールド
    const end = record['終了日時'].value; // 日時フィールド
    const memo = record['メモ'].value; // 文字列複数行フィールド
    const files = record['添付ファイル'].value; // 添付ファイルフィールド

    // 参加者にtypeを追加
    const attendees = users.map(element => {
      return { ...element, type: 'USER' };
    });

    // 日付操作はdayjsを使用
    const startDate = dayjs(start).format('YYYY-MM-DDTHH:mm:ssZ');
    const endDate = dayjs(end).format('YYYY-MM-DDTHH:mm:ssZ');
    const requestToken = await getRequestToken();

    const fileKey = files[0].fileKey;
    const fileName = files[0].name;

    // 添付ファイルをダウンロード
    const getFileResp = await fetch(`/k/v1/file.json?fileKey=${fileKey}`, {
      method: 'GET',
      headers: { 'X-Requested-With': 'XMLHttpRequest' },
    });
    const blob = await getFileResp.blob();

    // 添付ファイルをBase64エンコード
    const reader = new FileReader();
    reader.readAsDataURL(blob);
    await new Promise(resolve => (reader.onload = () => resolve()));
    const base64Data = reader.result.split(',')[1];

    const scheduleUrl = 'https://**********.cybozu.com/g/api/v1/schedule/events';
    const header = {
      'Content-Type': 'application/json',
      'X-Requested-With': 'XMLHttpRequest',
    };

    const body = {
      __REQUEST_TOKEN__: requestToken,
      eventType: 'REGULAR',
      subject: title,
      start: {
        dateTime: startDate,
        timeZone: 'Asia/Tokyo',
      },
      end: {
        dateTime: endDate,
        timeZone: 'Asia/Tokyo',
      },
      attendees: attendees,
      attachments: [
        {
          name: fileName,
          content: base64Data,
        },
      ],
      notes: memo,
    };

    const params = {
      method: 'POST',
      headers: header,
      body: JSON.stringify(body),
    };

    if (!scheduleId) {
      // 予定IDがkintoneのレコード内に登録されていなければPOST
      const respPost = await fetch(scheduleUrl, params);
      if (!respPost.ok) {
        alert(respPost.statusText);
        return event;
      }
      const result = await respPost.json();

      // kintoneのレコード内に予定IDを登録
      const putParams = {
        app: appId,
        id: recordId,
        record: { 予定ID: { value: result.id } },
      };

      await kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', putParams).catch(error => {
        alert(error.messsage);
        return event;
      });
    } else {
      // 予定IDがkintoneのレコード内に登録されていればPATCH
      params.method = 'PATCH';
      const respPatch = await fetch(`${scheduleUrl}/${scheduleId}`, params);
      if (!respPatch.ok) {
        alert(respPatch.statusText);
        return event;
      }
    }
    alert('完了しました');
    return event;
  });
})();

原因謎ですねえ(><)
body作成するときの、base64DataってちゃんとBase64の文字列になっていますか:eyes:??
const bodyの行の直前あたりとか
const respPatch = await fetch(${scheduleUrl}/${scheduleId}, params);
の直前あたりとか
でブレークポイントを張ってbodyやparamsの中身を見てみると良いかも知れません(><)

jurippe様
ご返答ありがとうございます。

確認したところ、問題ないように思います。
添付画像では切れてますが、デコードで元のファイル(画像)が表示されることも確認できました。

ためしに、Garoon上でコンソールでAPI叩いてやってみたんですが・・・・
添付されませんでした:scream:

※ファイルはこんな感じで作ったやつです。

const blob = new Blob(['テストファイルです'], {
  type: 'text/plain',
});

↓Garoonのコンソール上で実行

body ={
    "eventType": "REGULAR",
    "subject": "aiueooo",
    "start": {
        "dateTime": "2023-07-27T00:00:00+09:00",
        "timeZone": "Asia/Tokyo"
    },
    "end": {
        "dateTime": "2023-07-27T01:00:00+09:00",
        "timeZone": "Asia/Tokyo"
    },
    "attachments": [
        {
            "name": "test.txt",
            "content": "44OG44K544OI44OV44Kh44Kk44Or44Gn44GZ"
        }
    ],
};
await garoon.api('api/v1/schedule/events/16','PATCH',body);

↓レスポンス

attachmentsからっぽ
添付されないですね:scream:なんで!?
聞いてみるといいかも知れないですね:eyes:!?

※色々書き直しました^^;

※システム設定で添付ファイル許可しているのに:eyes:

2023-07-26_15h13_08

わざわざ検証して頂きありがとうございます…!
一度、サイボウズに問い合わせてみたいと思います。

いいえ、、、あんまり検証とかしないんですが、めっちゃ気になっちゃってつい。。。^^;
ちなみに、POSTだとなぜか添付されました:scream:
(あっ、登録はもともと大丈夫だったんですね)

サポートから回答を頂いたので共有させて頂きます。

・REST APIでは添付ファイルの更新に未対応
・ドキュメントに関しては今後修正予定
・代替案としてSOAP APIで添付ファイルの更新を行う
<< 処理例 >>

  1. ScheduleGetEventsById で、更新対象の予定データを取得する
  2. ScheduleModifyEvents で、「kintone」に添付されたファイルをfile に指定し、
    remove_file_id に 1. で取得したファイルの ID を指定した処理を実行する

コードを見直し、以下の内容で期待通りの結果を得ることができました。
(処理例1.の部分はREST APIで対応しています)

(() => {
  'use strict';

  const scheduleUrl = 'https://**********.cybozu.com/g/api/v1/schedule/events';
  const headers = {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest',
  };

  // SOAP APIリクエストのテンプレート
  const createSoapEnvelope = args => {
    return (
      `<?xml version="1.0" encoding="UTF-8"?>` +
      `<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:schedule_services="http://wsdl.cybozu.co.jp/schedule/2008">` +
      `<SOAP-ENV:Header>` +
      `<Action SOAP-ENV:mustUnderstand="1" xmlns="http://schemas.xmlsoap.org/ws/2003/03/addressing">${args.action}</Action>` +
      `<Timestamp SOAP-ENV:mustUnderstand="1" Id="id" xmlns="http://schemas.xmlsoap.org/ws/2002/07/utility">` +
      `<Created>2037-08-12T14:45:00Z</Created>` +
      `<Expires>2037-08-12T14:45:00Z</Expires>` +
      `</Timestamp>` +
      `<Locale>jp</Locale>` +
      `</SOAP-ENV:Header>` +
      `<SOAP-ENV:Body>` +
      `<${args.action}>` +
      `<parameters>${args.params}</parameters>` +
      `</${args.action}>` +
      `</SOAP-ENV:Body>` +
      `</SOAP-ENV:Envelope>`
    );
  };

  // リクエストトークン取得
  const getRequestToken = async () => {
    const params = null;
    const body = createSoapEnvelope({ action: 'UtilGetRequestToken', params });
    const resp = await fetch('/g/util_api/util/api.csp?', { method: 'POST', body });
    if (!resp.ok) {
      alert(resp.statusText);
      return;
    }
    const respText = await resp.text();
    const doc = new DOMParser().parseFromString(respText, 'text/xml');
    const requestToken = doc.querySelector('request_token').textContent;
    return requestToken;
  };

  // 更新対象のファイルIDとユーザーIDを取得
  const getFileIdAndUserId = async scheduleId => {
    if (!scheduleId) return [];
    const resp = await fetch(`${scheduleUrl}/${scheduleId}`, { method: 'GET', headers });
    if (!resp.ok) {
      alert(resp.statusText);
      return [];
    }
    const scheduleObj = await resp.json();
    const fileId = scheduleObj.attachments[0]?.id;
    const userId = scheduleObj.attendees[0].id;
    return [fileId, userId];
  };

  // 添付ファイルをダウンロード→base64エンコード
  const encodingToBase64 = async filekey => {
    if (!filekey) return;
    const resp = await fetch(`/k/v1/file.json?fileKey=${filekey}`, {
      method: 'GET',
      headers: { 'X-Requested-With': 'XMLHttpRequest' },
    });
    if (!resp.ok) {
      alert(resp.statusText);
      return;
    }
    const blob = await resp.blob();
    const reader = new FileReader();
    reader.readAsDataURL(blob);
    await new Promise(resolve => (reader.onload = () => resolve()));
    const base64Data = reader.result.split(',')[1];
    return base64Data;
  };

  kintone.events.on(['app.record.create.submit.success', 'app.record.edit.submit.success'], async event => {
    const record = event.record;
    const appId = kintone.app.getId();
    const recordId = record['$id'].value;
    const scheduleId = record['予定ID'].value;
    const users = record['参加者'].value;
    const user = [{ ...users[0], type: 'USER' }];
    const title = record['タイトル'].value;
    const startDate = record['開始日時'].value;
    const endDate = record['終了日時'].value;
    const memo = record['メモ'].value;
    const files = record['添付ファイル'].value;
    const fileKey = files[0]?.fileKey;
    const fileName = files[0]?.name;

    const requestToken = await getRequestToken();
    const base64Data = await encodingToBase64(fileKey);
    const [fileId, userId] = await getFileIdAndUserId(scheduleId);

    if (!scheduleId) {
      // 予定IDがkintoneのレコード内に登録されていなければPOST
      const postBody = {
        __REQUEST_TOKEN__: requestToken,
        eventType: 'REGULAR',
        subject: title,
        start: {
          dateTime: startDate,
          timeZone: 'Asia/Tokyo',
        },
        end: {
          dateTime: endDate,
          timeZone: 'Asia/Tokyo',
        },
        attendees: user,
        attachments: [
          {
            name: fileName,
            content: base64Data,
          },
        ],
        notes: memo,
      };
      const body = JSON.stringify(postBody);
      const resp = await fetch(scheduleUrl, { method: 'POST', headers, body });
      if (!resp.ok) {
        alert(resp.statusText);
        return event;
      }
      const scheduleObj = await resp.json();

      // kintoneのレコード内に予定IDを登録
      const putParams = {
        app: appId,
        id: recordId,
        record: { 予定ID: { value: scheduleObj.id } },
      };
      await kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', putParams).catch(error => {
        alert(error.message);
        return event;
      });
    } else {
      // 予定IDがkintoneのレコード内に登録されていれば更新
      const fileParams = files.length
        ? `<file id="" name="${fileName}" size="" mime_type=""><content>${base64Data}</content></file>`
        : '';

      const params =
        `<request_token>${requestToken}</request_token>` +
        `<schedule_event xmlns="" id="${scheduleId}" event_type="normal" pubic_type="public" version="" ` +
        `plan="" detail="${title}" description="${memo}" ` +
        `timezone="Asia/Tokyo" end_timezone="Asia/Tokyo" allday="false" start_only="false">` +
        `<members>` +
        `<member>` +
        `<user id="${userId}"></user>` +
        `</member>` +
        `</members>` +
        `<when>` +
        `<datetime start="${startDate}" end="${endDate}"></datetime>` +
        `</when>` +
        `${fileParams}` +
        `<remove_file_id>${fileId}</remove_file_id>` +
        `</schedule_event>`;

      const body = createSoapEnvelope({ action: 'ScheduleModifyEvents', params });
      const resp = await fetch('/g/cbpapi/schedule/api?', { method: 'POST', body });
      if (!resp.ok) {
        alert(resp.statusText);
        return event;
      }
    }
    alert('完了しました');
    return event;
  });
})();

1 Like

このトピックは最後の返信から 3 日が経過したので自動的にクローズされました。新たに返信することはできません。