Cisco Webex Messaging から Garoon スケジュールを予約する

目次

caution
警告

はじめに

緊急な来訪で今すぐ会議室を抑えておきたいケースがあると思います。
グループウェアが未導入だと担当者に電話で確認したり、会議室前の予約用紙にサインしたりと面倒ですが、グループウェアを導入済みでもパソコンを立ち上げたり、空き状況を検索したりと意外と手間もかかります。

そこで、今回はクラウドベースのコラボレーションサービス『 Cisco Webex Messaging (External link) 』と大規模向けグループウェア『 Garoon (External link) 』を連携することで、簡単に空き部屋を見つけて予約する Tips を紹介します。

システム構成図

Cisco Webex MessagingGaroon on cybozu(以下 Garoon) の API を連携させるために、今回は AWS Lambda を利用しました。

Cisco Webex Messaging とは

  • 1 対ゼロから、複数メンバーによるグループメッセージでシームレスに会話が成り立つ。
  • モバイル、パソコン専用の Cisco Webex Messaging アプリがあり、複数のデバイス利用が可能
  • RESTful な API がそろっており、Webhook も搭載

そのほか多くの特徴があります。 Cisco Webex Messaging のサイト (External link) で確認してください。

下準備

Garoon

  • 必要に応じて施設を登録してください。
  • API 実行用のユーザーを 1 つ用意してください。

Cisco Webex Messaging

  • Cisco Webex Messaging 上に結果を表示するための bot 用のアカウントを用意してください。
  • 専用の会議室を 1 つ用意してください。
  • Cisco Webex Messaging の API 情報は こちら (External link) のページです。

Node.js の JavaScript ファイル

  • 後述の index.js, garoonapi.js の内容をコピーして配置してください。
  • request-promise, moment, cheerio を使用していますので、必要に応じて npm install でインストールしてください。

環境を作成する

アップロード用Zipファイルの作成

index.js を開いて、以下の項目を記入して保存します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Cisco Webex Messaging の会議室のID
const ROOMID = 'XXX';

// Cisco Webex Messaging の bot アカウントの Access Token
const BEARER = 'XXX';

// Garoon のドメイン
const DOMAIN = '{subdomain}.cybozu.com';

// Cisco Webex Messaging の bot アカウントのメールアドレス
const SPARK_MADDRESS = 'xxx@xx.xx';

項目 設定値
ROOMID Cisco Webex Messaging の会議室のID
List Rooms (External link) で該当する会議室のID を確認できます。
Test Mode を On にして(ログインする必要あり) Run をクリックすると、会議室の情報が Response に表示されます。
Response の中から、該当する会議室の ID を取得してください。
BEARER Cisco Webex Messaging の bot アカウントの Access Token
Webex for developer (External link) のログイン後に右上のユーザーのアイコンをクリックすると表示されます

【補足】
OAuth 2.0を利用した認証も可能です。複数のアカウントに関連する操作を行いたい場合や、APIへのアクセス権を制限したい場合などには、OAuth 2.0を利用する必要があります。
詳細は、 Integrations & Authorization (External link) を参照ください。
DOMAIN kintone のドメイン
SPARK_MADDRESS Cisco Webex Messaging の bot アカウントのメールアドレス

同様に Garoonpi.js を開いて、以下の項目を記入して保存します。

1
2
const USERID = xxx, // API 実行ユーザーのログイン名
  PASSWD = xxx; // API 実行ユーザーのパスワード

項目 設定値
USERID API 実行ユーザーのログイン名
PASSWD API 実行ユーザーのパスワード

最後に、以下のコマンドで、zip ファイルを作成します。

zip ファイルの名前は、SparkGSch.zip とします。

1
zip -r SparkGSch.zip index.js garoonapi.js node_modules/

AWS の設定(Lambda, API Gateway)

Slackから手軽にkintoneへレコード登録する方法 の中の「Lambda の設定」「Amazon API Gateway の設定」を参考に、それぞれの設定します。

ここでは、注意すべき点を記載します。

Lambda の設定
  • Run Time で Node のバージョンが指定できます。
    最新バージョン(記事作成時点では、Node.js 4.3)を指定することをおすすめします。
  • Advanced settings の Timeout は 0min 20sec としてください。
    環境によりますが 7 ~ 8 秒かかる場合もあります。
API Gateway の設定

Invoke URL の中に Domain 名、 Stage 名、 Method 名が含まれていることを確認してください。

1
https://(Domain 名)/(Stage 名)/(Method 名)

Cisco Webex Messaging Webhook の設定

Webex for Developers の Create a Webhook (External link) より、Webhook を作成します。
Test Mode を On にして(ログインする必要あり)、以下の項目を入力後、Run をクリックしてください。

項目 設定値
name 今回作成する Webhook の名前(任意)
targetUrl API Gateway の Invoke URL
resource messages(固定)
event created(固定)
filter roomId=(Cisco Webex Messaging の会議室のID)

List Webhooks (External link) Get Webhook Details (External link) から作成した Webhook を確認できます。

確認する

Cisco Webex Messaging の会議室から実際に確認してみましょう。

caution
警告

必ず、bot 以外のアカウントで確認してください。
bot アカウントで確認した場合、プログラムは作動しません。

いまから 1 時間空いている施設を検索する場合は、「1 時間で検索して」と入力します。
スクリーンショットはパソコン版の画面です。

数秒待つと bot がスケジュールの空き状況を教えてくれます。

施設を予約する

先ほどの検索結果より、会議室 B を 1 時間予約したいと思います。
会議室 B は[2]ですので、以下のように入力します。

数秒待つと bot から、会議室を予約しましたとメッセージが返ってきます。

実際に、Cybozu Garoon で施設のスケジュールを見てみます。
たしかに実行した時から 1 時間の時間帯で、会議室 B が予約されていることがわかります。

応用編

今回は、現在の時刻から検索・予約する方法を紹介しました。
たとえば施設名に収容員数を含ませることで、人数による絞り込みができたり、施設を検索・予約する際に時間を指定したりするなど、いろいろカスタマイズしてオリジナルのシステムを構築できます。

今後の展開

リアルタイム性を活かして、今すぐ知りたい、教えてくれるサービス展開ができそうです。

  1. 朝に自分の今日の予定を Cisco Webex Messaging で取得する。
  2. 他の人の予定を取得する。

JavaScript

index.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
(function() {

  'use strict';

  const rp = require('request-promise');
  const moment = require('moment');
  const gapi = require('./garoonapi');
  const cheerio = require('cheerio');

  // Cisco Webex Messaging の会議室のID
  const ROOMID = 'XXX';

  // Cisco Webex Messaging の bot アカウントの Access Token
  const BEARER = 'XXX';

  // Garoon のドメイン
  const DOMAIN = '{subdomain}.cybozu.com';

  // Cisco Webex Messaging の bot アカウントのメールアドレス
  const SPARK_MADDRESS = 'xxx@xx.xx';

  // Cisco Webex Messaging にレコードを登録
  function sendSpark(msg) {

    // Cisco Webex Messaging に投稿する内容
    const body_post_spark = {
      roomId: ROOMID, // 会議室
      text: msg // 投稿内容
    };

    // Cisco Webex Messaging に投稿するためのオブジェクト
    const postspark = {
      url: 'https://api.ciscospark.com/v1/messages/',
      method: 'POST',
      auth: {bearer: BEARER},
      'Content-Type': 'application/json',
      json: body_post_spark
    };

    // 投稿を実行する
    rp(postspark).then((res) => {
      console.log('投稿されました:' + msg);
    });
  }

  // garoon に登録されているすべての施設の id を取得する
  function getFacilityId() {

    // 施設の id を取得するためのオブジェクト
    const getfid = {url: 'https://' + DOMAIN + '/g/cbpapi/schedule/api.csp',
      method: 'POST',
      body: gapi.scheduleGetFacilityVersions(),
      'Content-Type': 'text/xml; charset=UTF-8'};

    // 施設の id の取得を実行する
    return rp(getfid).then((res) => {
      const aryfid = [],
        $ = cheerio.load(res),
        aryfobj = $('facility_item');

      for (let i = 0; i < aryfobj.length; i += 1) {
        aryfid.push(aryfobj[i].attribs.id);
      }
      return aryfid;
    });

  }

  // garoon に登録されているすべての施設の詳細情報を取得する
  function getFacilityDetail(aryfid) {
    const objfacility = {},
      // 施設の詳細情報を取得するためのオブジェクト
      getfinfo = {url: 'https://' + DOMAIN + '/g/cbpapi/schedule/api.csp',
        method: 'POST',
        body: gapi.scheduleGetFacilitiesById(aryfid),
        'Content-Type': 'text/xml; charset=UTF-8'};

    // 施設の詳細情報の取得を実行する
    return rp(getfinfo).then((res2) => {
      const $ = cheerio.load(res2),
        aryfdetail = $('facility');
      for (let j = 0; j < aryfdetail.length; j += 1) {
        objfacility[aryfdetail[j].attribs.key] = {name: aryfdetail[j].attribs.name};
      }

      return objfacility;
    });
  }

  // 分を HH:mm:ss 形式に変換する 例) 80分 → 01:20:00
  function changeKintoneTimeByMinute(pmin) {
    const minute = parseInt(pmin, 10);

    // 分は 1から 1439 までとする
    if (minute < 1 || minute > 1439) {
      sendSpark('【エラー】時間は 1 ~ 1439 を指定してください:' + pmin);
      return false;
    }

    // 分を HH:mm:ss 形式に変換する
    const dt = moment('2016-01-01T00:00:00');
    dt.set('minute', minute);
    return dt.format('HH:mm:ss');
  }

  // Cisco Webex Messaging の入力文字列から時間と分を取得する
  function getHourMinute(str) {
    const objreq = {};

    // '時間'で文字列を分割する
    const aryItem = str.split('時間');

    // '時間' が含まれていない場合
    if (aryItem.length === 1) {
      // 分を取得する(時間は 0をセットする)
      objreq.hour = 0;
      objreq.minute = parseInt(aryItem[0].replace(/[^-^0-9^.]/g, ''), 10);

      // '時間' が含まれている場合
    } else {
      // 時間と分を取得する
      objreq.hour = parseInt(aryItem[0].replace(/[^-^0-9^.]/g, ''), 10);
      objreq.minute = parseInt(aryItem[1].replace(/[^-^0-9^.]/g, ''), 10) || 0;
    }

    return objreq;
  }

  // 施設の空き時間を検索する
  function getGaroonSchedule(userid, groupid, facilityid, obj) {
    // 施設の空き時間を取得するためのオブジェクト
    const getgsch = {
      url: 'https://' + DOMAIN + '/g/cbpapi/schedule/api.csp',
      method: 'POST',
      body: gapi.scheduleSearchFreeTimes(userid, groupid, facilityid, obj),
      'Content-Type': 'text/xml; charset=UTF-8'
    };

    // 検索を実行する
    return rp(getgsch).then((res) => {
      const ary = [],
        $ = cheerio.load(res),
        sch = $('candidate');
      // 取得した結果を配列に格納する
      for (let i = 0; i < sch.length; i += 1) {
        // 開始日時、終了日時、施設のid
        ary.push({start: sch[i].attribs.start,
          end: sch[i].attribs.end,
          facility_id: sch[i].attribs.facility_id});
      }
      return ary;
    });
  }

  // garoon の施設に予約を登録する
  function setGaroonSchedule(userid, groupid, facilityid, obj) {
    // 施設のスケジュールを予約するためのオブジェクト
    const reservegsch = {
      url: 'https://' + DOMAIN + '/g/cbpapi/schedule/api.csp',
      method: 'POST',
      body: gapi.scheduleAddEvents(userid, groupid, facilityid, obj),
      'Content-Type': 'text/xml; charset=UTF-8'
    };

    // スケジュールの予約を実行する
    return rp(reservegsch).then((res) => {
      const ary = [],
        $ = cheerio.load(res),
        sch = $('datetime');
      // 予約された内容の開始日時、終了日時
      ary.push({start: sch[0].attribs.start,
        end: sch[0].attribs.end});
      return ary;

      // 予約に失敗した場合
    }).catch((error) => {
      sendSpark('【予定登録失敗】\n' +
                      '予約できませんでした。すでに予約が入っている可能性があります。');
    });
  }

  // 施設の空き時間を検索する準備をする
  function searchFreeTime(str) {

    const gettime = {url: 'https://' + DOMAIN + '/g/',
        method: 'HEAD',
        'Content-Type': 'text/xml; charset=UTF-8'},
      arystr = [];
    let objreq = {},
      dtstart, dtend,
      aryfid = [],
      objfdetail = {},
      sdt, edt, facilityname;

    // Cisco Webex Messaging の入力文字列から検索する空き時間の時間、分を取得する
    objreq = getHourMinute(str);

    // 施設を検索する時間の長さ(分)を計算する
    objreq.timescale = changeKintoneTimeByMinute(objreq.hour * 60 + objreq.minute);

    // Garoon に登録されているすべての施設の ID を取得する
    return getFacilityId().then((fid) => {

      // 施設の id は後で使用するので、保持しておく
      aryfid = fid;

      // Garoon に登録されているすべての施設の詳細情報を取得する
      return getFacilityDetail(aryfid);

    }).then((fdetail) => {

      // 施設の詳細情報は後で使用するので、保持しておく
      objfdetail = fdetail;

      // 現在の時刻(kintone サーバーの時刻)を取得する
      return rp(gettime);

    }).then((res) => {

      // 空き時間検索の開始日時
      dtstart = moment(new Date(res.date));

      // 空き時間検索の終了日時
      dtend = moment(new Date(res.date));
      dtend.set('hour', dtend.get('hour') + objreq.hour);
      dtend.set('minute', dtend.get('minute') + objreq.minute);

      // 時刻を kintone フォーマットに合わせる
      objreq.dt = [];
      objreq.dt[0] = {start: dtstart.format('YYYY-MM-DDTHH:mm:ss') + 'Z',
        end: dtend.format('YYYY-MM-DDTHH:mm:ss') + 'Z'};

      // 施設の id と日付からスケジュールの取得を実行する
      return getGaroonSchedule([], [], aryfid, objreq);

    }).then((result) => {

      // 検索結果が 0件の場合は、メッセージを出力して処理を抜ける
      if (result.length < 1) {
        sendSpark('【結果】\nスケジュールに空きが見つかりませんでした');
        return;
      }

      // 結果を配列に格納する
      for (let i = 0; i < result.length; i += 1) {
        // 開始日時、終了日時
        sdt = new Date(result[i].start);
        edt = new Date(result[i].end);
        // 施設の名前
        facilityname = objfdetail[result[i].facility_id].name;
        // 日時を日本時間に変換する
        sdt.setHours(sdt.getHours() + 9);
        edt.setHours(edt.getHours() + 9);
        // 配列に追加する
        arystr.push('[' + result[i].facility_id + '] ' + facilityname + ' ' +
                            moment(sdt).format('YYYY/MM/DD HH:mm') + ' - ' + moment(edt).format('HH:mm'));
      }
      // Cisco Webex Messaging に結果を出力する
      sendSpark('【検索結果】\n' + arystr.join('\n'));


    }).catch((error) => {

    });
  }

  // 施設を検索、登録する
  function reservation(str) {
    const gettime = {url: 'https://' + DOMAIN + '/g/',
        method: 'HEAD',
        'Content-Type': 'text/xml; charset=UTF-8'},
      aryItem = [],
      aryresistfid = [];
    let
      objreq = {},
      dtstart, dtend,
      sdt, edt;

    // Cisco Webex Messaging の入力文字列を 'を' で分割する
    aryItem[0] = str.split('を');

    // 'を' が含まれている数が、1つではない場合は、処理を抜ける
    if (aryItem[0].length !== 2) {
      return null;
    }

    // 予約する施設の id を配列に格納する
    // 【補足】同時に複数の施設を予約したい場合は、さらに追加してください
    if (parseInt(aryItem[0][0].replace(/[^-^0-9^.]/g, ''), 10) > 0) {
      aryresistfid.push(parseInt(aryItem[0][0].replace(/[^-^0-9^.]/g, ''), 10));
    } else {
      sendSpark('施設 ID が不正です: ' + aryItem[0][0]);
      return null;
    }

    // Cisco Webex Messaging の入力文字列から検索する空き時間の時間、分を取得する
    objreq = getHourMinute(aryItem[0][1]);

    // 施設を予約する時間(分)
    objreq.timescale = changeKintoneTimeByMinute(objreq.hour * 60 + objreq.minute);

    // 現在の時刻(kintone サーバーの時刻)を取得する
    return rp(gettime).then((res) => {

      // 予約の開始日時(現在のサーバー時刻)
      dtstart = moment(new Date(res.date));

      // 予約の終了日時
      dtend = moment(new Date(res.date));
      dtend.set('hour', dtend.get('hour') + objreq.hour);
      dtend.set('minute', dtend.get('minute') + objreq.minute);

      // 開始日時と終了日時を 'YYYY-MM-DDTHH:mm:ss' 形式に変換する
      objreq.sdt = dtstart.format('YYYY-MM-DDTHH:mm:ss') + 'Z';
      objreq.edt = dtend.format('YYYY-MM-DDTHH:mm:ss') + 'Z';

      // Garoon のスケジュールに登録する際のタイトル
      objreq.title = '★From Spark';

      // 予約を実行する
      return setGaroonSchedule([], [], aryresistfid, objreq);

    }).then((result) => {

      // 予約されたスケジュールの開始日時、終了日時
      sdt = new Date(result[0].start);
      edt = new Date(result[0].end);

      // 日時を日本時間に変換する
      sdt.setHours(sdt.getHours() + 9);
      edt.setHours(edt.getHours() + 9);

      // Cisco Webex Messaging に結果を出力する
      sendSpark('【予定登録完了】\n' +
                      '[' + aryresistfid[0] + '] ' + moment(sdt).format('YYYY/MM/DD HH:mm') +
                        ' - ' + moment(edt).format('HH:mm') + ' で会議室を予約しました');

    }).catch((error) => {
      return null;
    });
  }

  // 入力された文字の内容によって処理を振り分ける
  function allocation(str) {
    // 検索
    // Cisco Webex Messaging に入力された文字が条件を満たしている場合のみ検索を実行する
    if ((str.indexOf('時間') > -1 || str.indexOf('分') > -1) && str.indexOf('検索') > -1) {
      searchFreeTime(str);
      return;
    }

    // 予約
    // Cisco Webex Messaging に入力された文字が条件を満たしている場合のみ予約を実行する
    if (str.indexOf('を') > -1 &&
            (str.indexOf('時間') > -1 || str.indexOf('分') > -1) &&
            (str.indexOf('登録') > -1 || str.indexOf('予約')) > -1) {
      reservation(str);

    }

  }

  // Webhook を受けた際の処理
  exports.handler = function(event, context) {
    // 【注意】
    // Webhookはインターネットにさらされているので、セキュリティに関して注意する必要があります。
    // Webhookは、httpsで受信するように設計したうえで、イベント受信時に、
    // event.idで取得したWebhookのIDが、Webhook登録時のレスポンスのWebhookのIDと同じかどうかをチェックする必要があります。
    // また、登録時に指定したフィルタに適合しているかチェックすることも推奨します。
    // 本記事では、サンプルにつき、詳細は省略しています。

    // event.data.id で投稿されたメッセージの id を取得できる
    // id を利用してメッセージの詳細を取得する
    const getmessage = {
      url: 'https://api.ciscospark.com/v1/messages/' + event.data.id,
      method: 'GET',
      auth: {bearer: BEARER},
      'Content-Type': 'application/json'
    };

    // メッセージの詳細の取得を実行する
    rp(getmessage).then((body) => {
      const objbody = JSON.parse(body);
      // bot アカウントからのメッセージの場合は、処理しない(無限ループを防ぐ)
      if (objbody.personEmail === SPARK_MADDRESS) {
        return;
      }
      // メッセージの本文を解析する
      allocation(objbody.text);
    });
  };

  // zip ファイルを作成する際のコマンド
  // zip -r SparkGSch.zip index.js garoonapi.js node_modules/

})();

garoonapi.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109

(function() {

  'use strict';

  const USERID = 'xxx', // API 実行ユーザーのログイン名
    PASSWD = 'xxx'; // API 実行ユーザーのパスワード

  // xml の共通部分
  function soapFormat(fname) {
    const obj = {};
    obj.head = '<?xml version="1.0" encoding="UTF-8"?>' +
                    '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">' +
                      '<soap:Header>' +
                        '<Action>' + fname + '</Action>' +
                        '<Security>' +
                          '<UsernameToken>' +
                            '<Username>' + USERID + '</Username>' +
                            '<Password>' + PASSWD + '</Password>' +
                          '</UsernameToken>' +
                        '</Security>' +
                        '<Timestamp>' +
                          '<Created>2010-08-12T14:45:00Z' +
                          '<Expires>2037-08-12T14:45:00Z' +
                        '</Timestamp>' +
                        '<Locale>jp' +
                      '</soap:Header>' +
                      '<soap:Body>' +
                        '<' + fname + '>';

    obj.foot = '</' + fname + '>' +
                  '</soap:Body>' +
                '</soap:Envelope>';

    return obj;
  }

  // スケジュールの空き時間を検索する
  exports.scheduleSearchFreeTimes = function(userid, groupid, facilityid, obj) {
    const sf = soapFormat('ScheduleSearchFreeTimes');
    let
      membertag = '',
      dttag = '';

    for (let i = 0; i < userid.length; i += 1) {
      membertag += '<member><user id="' + userid[i] + '"></member>';
    }

    for (let j = 0; j < groupid.length; j += 1) {
      membertag += '<member><organization id="' + groupid[j] + '"></member>';
    }

    for (let k = 0; k < facilityid.length; k += 1) {
      membertag += '<member><facility id="' + facilityid[k] + '"></member>';
    }

    for (let m = 0; m < obj.dt.length; m += 1) {
      dttag += '<candidate start="' + obj.dt[m].start + '" end="' + obj.dt[m].end + '">';
    }

    return sf.head + '<parameters search_time="' + obj.timescale + '" search_condition="or">' +
               dttag +
               membertag +
              '</parameters>' + sf.foot;
  };

  // スケジュールに予定を登録する
  exports.scheduleAddEvents = function(userid, groupid, facilityid, obj) {
    const sf = soapFormat('ScheduleAddEvents');
    let membertag = '';

    for (let i = 0; i < userid.length; i += 1) {
      membertag += '<member><user id="' + userid[i] + '"></member>';
    }

    for (let j = 0; j < groupid.length; j += 1) {
      membertag += '<member><organization id="' + groupid[j] + '"></member>';
    }

    for (let k = 0; k < facilityid.length; k += 1) {
      membertag += '<member><facility id="' + facilityid[k] + '"></member>';
    }

    return sf.head + '<parameters>' +
               '<schedule_event xmlns="" id="dummy" event_type="normal" version="dummy"' +
               ' public_type="public"' +
               ' detail="' + obj.title + '" start_only="false">' +
               '<members>' + membertag + '</members>' +
               '<when>' +
               '</schedule_event>' + sf.foot;
  };

  // 施設の詳細情報を取得する
  exports.scheduleGetFacilitiesById = function(ids) {
    const sf = soapFormat('ScheduleGetFacilitiesById');
    let facility = '';
    for (let i = 0; i < ids.length; i += 1) {
      facility += '<facility_id>' + ids[i] + '</facility_id>';
    }
    return sf.head + '<parameters>' + facility + '</parameters>' + sf.foot;
  };

  // Garoon に登録されているすべての施設の id を取得する
  exports.scheduleGetFacilityVersions = function() {
    const sf = soapFormat('ScheduleGetFacilityVersions');
    return sf.head + '<parameters>' + sf.foot;
  };

})();
information

この Tips は、 2016 年 4 月時点の Garoon, Cisco Webex Messaging で動作を確認しています。