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/
})();
|