Cisco Webex Messaging に投稿したファイルを kintone アプリに登録する方法

目次

はじめに

しばらく間が空きましたが、Cisco Webex Messaging(旧称: Cisco Webex Teams)の連携シリーズ第 5 弾です!
今回は、Cisco Webex Messaging に投稿された添付ファイルを kintone アプリへ登録する方法を紹介します。

この連携カスタマイズは Cisco Webex Messaging で共有した資料を kintone アプリへ添付する場合に使えます。
また、モバイルのカメラで撮った写真を kintone アプリにアップしたい場合などは、直接 kintone アプリに写真をアップするより手間がかからず便利です。

これまでの Cisco Webex Messaging と cybozu.com の連携シリーズはこちらです。

Cisco Webex Messaging についての詳しい説明は、 Cisco Webex Messaging から Garoon スケジュールを予約する|Cisco Webex Messagingとは を確認してください。

連携イメージ

Cisco Webex Messaging(以下 Webex Messaging)のスペースで写真を撮り、ファイル登録用の Bot にメンションを投げると、kintone アプリにファイルが登録されます。

連携概要

ここ何回は、Webex Messaging と kintone を連携する際に Azure Functions を使っていましたが、今回は AWS を使ってみたいと思います。(特に深い意味はありません!)

処理の流れは以下のようになります。

  1. Webex Messaging の Webhook によって、Amazon API Gateway 側で投稿されたファイルの URL 情報を取得する。
  2. 添付ファイルの URL より、ファイルの本体を取得する。
  3. 取得したファイルを、Lambda 内にコピーする。
  4. コピーしたファイルを kintone に転送し、ファイル ID を取得する。
  5. kintone アプリにファイル ID の情報を私てレコードを登録する。

下準備

他の Tips に詳細が記載されている内容については、簡略して記載しています。詳細はリンク先を参照してください。

kintone アプリ

フィールドは自由に設定できますが、次の 2 フィールドを必ず含めてください。

フィールド名 フィールドタイプ フィールドコード
タイトル 文字列(1行) title
添付ファイル 添付ファイル fl

API トークンの作成

本 Tips では API トークン認証を使うため、以下の手順で API を作成します。

作成した API トークンは、処理プログラムで必要になりますのでメモしておいてください。

  1. 先ほど作成した kintone アプリの管理画面を開き、「設定」のタブをクリックします。

  2. 「カスタマイズ/サービス連携」の下の「API トークン」をクリックします。

  3. 「生成する」ボタンをクリックし、アクセス権欄の「レコード追加」にチェックを入れ左上の「保存」ボタンをクリックします。

  4. 「アプリを更新」クリックします。

Cisco Webex Messaging 用 Bot

  1. Cisco Webex for Developers(旧:Cisco Spark for Developers) (External link) から「MyApps」を開きます。
  2. 「+」をクリックし、「Create a Bot」をクリックします。
  3. 以下を参考に設定します。
    項目 設定例
    Name 任意の bot 名(bot) 日本語可
    Bot Username 任意文字列@sparkbot.io
    Icon Default から選択するか、任意のアイコンをアップロード
    Description 任意の説明文

第3弾 に MyApps の画面ショット付きの説明を記載しています。

Cisco Webex Messaging

  1. 任意のスペース(タスクを表示させるスペース)のユーザーに作成した Bot を追加する。
  2. 上記のスペースを利用するユーザーを 1 人以上登録する。

AWS のアカウントセットアップ

AWS のアカウントをお持ちでない方は、 AWS アカウントを設定して管理ユーザーを作成する (External link) を参考にして、AWS アカウントのセットアップと管理者ユーザーを作成してください。

利用を開始してから 1 年間は、無料利用枠の範囲で利用できます。

設定&実装

ここまでは過去の Tips を参照しながら説明してまいりましたが、この後は Cisco Webex Messaging 連携が初めての方でも、なるべく本 Tips だけで設定と実装ができるようにしていますので、ご安心ください。

AWS Lambda の設定

まず、Cisco Webex Messaging に投稿されたファイルを取得して kintone アプリに登録する Lambda 関数の設定をしましょう。

  1. AWS のコンソールにログイン後、Lambda を選択し「関数の作成」をクリックします。

  2. 関数の設定、Lambda 関数のコードについて以下のとおり設定し「関数の作成」をクリックします。

    • 「一から作成」を選択します。

    • 「名前」は必須です。任意の名前を入力してください。

    • 「ランタイム」は設計図の選択で選択した言語「Node.js 6.10」を選択します。

    • 「ロール」は「既存のロール選択」を選択し、「lambda_basic_execution」を選択します。

  3. 同じ画面で下にスクロールし、「関数コード」に処理プログラムを書きますが、詳細は後述します。

  4. さらに下にスクロールし、「基本設定」でタイムアウトを 30 秒に設定し、保存します。

処理プログラム

ここまで設定できたら、Node.js で Cisco Webex Messaging に投稿された画像ファイルを kintone へ登録する処理を書きます。

先ほどの「関数コード」のエディタ部分に以下のプログラムをコピーし、「XXX」となっている部分を環境に合わせて変更します。

  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

/*
 * Cisco Webex Messaging_kintone5 sample program
 * Copyright (c) 2018 Cybozu
 *
 * Licensed under the MIT License
*/
(function() {
  'use strict';
  const fs = require('fs');
  const https = require('https');

  const CYB = {
    CSPARK: {
      // Cisco Webex Messaging の Bot の Access Token
      BEARER: 'XXX',
      // Cisco Webex Messaging の api の Host
      HOST: 'api.ciscospark.com'},
    KINTONE: {
      // cybozu.com の domain
      HOST: 'XXX.cybozu.com',
      // kintone の アプリ番号
      APPID: 'XXX',
      // kintone の アプリの API Token
      APITOKEN: 'XXX'},
    NODE: {
      // Cisco Webex Messaging からのファイルを一時的に保存する場所
      FILEDIR: '/tmp/'
    }
  };

  // ログを出力
  const outputlog = function(msg) {
    console.log(msg);
  };

  // kintone の Options
  const getOptionsKintone = function(path, method) {
    return {
      hostname: CYB.KINTONE.HOST,
      port: 443,
      path: path,
      method: method,
      headers: {
        'X-Cybozu-API-Token': CYB.KINTONE.APITOKEN,
        'Content-Type': 'application/json'
      }
    };
  };

  // Cisco Webex Messaging の Options
  const getOptionsCisco = function(path, method) {
    return {
      hostname: CYB.CSPARK.HOST,
      port: 443,
      path: path,
      method: method,
      headers: {
        Authorization: 'Bearer ' + CYB.CSPARK.BEARER
      }
    };
  };

  // node のファイルを削除する
  const unlinkFile = function(resource) {
    const ret = null;
    if (resource) {
      try {
        fs.unlinkSync(resource);
      } catch (err) {
        outputlog('postProcessResource: Failed to delete');

      }
    }
    return ret;
  };

  // https を使用する(ファイル送信以外)
  const runHttps = function(options, json, filename, callback) {
    let resstring = '';

    const req = https.request(options, (res) => {
      outputlog('STATUS: ' + res.statusCode);
      outputlog('HEADER: ' + JSON.stringify(res.headers));

      if (!filename) {
        res.setEncoding('utf8');
      }

      res.on('data', (chunk) => {
        if (filename) {
          outputlog(chunk.length + ' chunked');
          fs.appendFileSync(filename, chunk, 'binary');
        } else {
          resstring += chunk;
        }
      });

      res.on('end', () => {
        if (res.statusCode === 200) {
          if (filename) {
            callback(null, true);
          } else {
            const rtnvalue = options.method === 'HEAD' ? res.headers : resstring;
            callback(null, rtnvalue);
          }
        }
      });
    });

    req.on('error', (e) => {
      outputlog('problem with request: ' + e.message);
      callback(e.message);
    });

    if (json) {
      req.write(json);
    }

    req.end();
  };

  // https を利用してファイルを送信する
  const runHttpsPostFile = function(options, objparams, boundary, callback) {
    outputlog('start postFile');

    let resstring = '';
    options.headers['Content-Type'] = 'multipart/form-data; boundary="' + boundary + '"';

    const req = https.request(options, (res) => {
      outputlog('STATUS: ' + res.statusCode);
      outputlog('HEADERS: ' + JSON.stringify(res.headers));
      res.on('data', (chunk) => {
        resstring += chunk;
        outputlog('chunk:' + resstring);
      });

      res.on('end', () => {
        outputlog('resstring: ' + resstring);
        if (res.statusCode === 200) {
          callback(null, resstring);
        }
      });
    });

    req.on('error', (e) => {
      outputlog('problem with request: ' + e.message);
      callback(e.message);
    });

    req.write(
      '--' + boundary + '\r\n' +
            'Content-Type: application/octet-stream\r\n' +
            'Content-Disposition: form-data; name="file"; filename="' +
                objparams.filename.split(CYB.NODE.FILEDIR)[1] + '"\r\n' +
            'Content-Transfer-Encoding: binary\r\n\r\n'
    );

    const stream = fs.createReadStream(objparams.filename, {
      bufferSize: 4 * 1024
    });
    stream.on('data', (chunk) => {
      req.write(chunk);
    });
    stream.on('end', () => {
      req.end('\r\n--' + boundary + '--');
    });
  };

  // Cisco Webex Messaging にメッセージを send する
  const sendSpark = function(msg, markdown, objparams) {
    const options = getOptionsCisco('/v1/messages/', 'POST');
    const body_post_spark = {}; // Cisco Webex Messaging に投稿する内容

    options.headers['Content-Type'] = 'application/json';

    // markdown か否かで区分
    if (markdown) {
      body_post_spark.markdown = msg;
    } else {
      body_post_spark.text = msg;
    }

    body_post_spark.roomId = objparams.roomid;
    const postDataStr = JSON.stringify(body_post_spark);

    runHttps(options, postDataStr, null, (err, resstring) => {
      if (err) {
        outputlog('ERROR: Cisco Webex Messaging にメッセージ送信時にエラーが発生しました');
        return;
      }
      outputlog('processing ended');
    });
  };

  // kintone にレコードを登録する
  const createRecord = function(objparams) {
    let msgstr;
    const fileparams = {
      app: CYB.KINTONE.APPID,
      record: {
        fl: {value: [{fileKey: objparams.filekey}]},
        title: {value: objparams.messagebody}
      }
    };

    const json = JSON.stringify(fileparams);
    const options = getOptionsKintone('/k/v1/record.json', 'POST');
    options.headers['Content-Type'] = 'application/json';

    runHttps(options, json, null, (err, resstring) => {
      if (err) {
        outputlog('ERROR: ファイル本体取得時にエラーが発生しました');
        return;
      }
      outputlog('resstring: ' + resstring);
      msgstr = '----------------------------\n\n';
      msgstr += '**' + objparams.filename.split(CYB.NODE.FILEDIR)[1] + '**\n\n';
      msgstr += 'レコード番号 **' + JSON.parse(resstring).id + '**で登録されました\n\n';
      msgstr += '[http://' + CYB.KINTONE.HOST + '/k/' + CYB.KINTONE.APPID +
                      '/show#record=' + JSON.parse(resstring).id + ']' +
                      '(http://' + CYB.KINTONE.HOST + '/k/' + CYB.KINTONE.APPID +
                      '/show#record=' + JSON.parse(resstring).id + ')\n\n';
      msgstr += '----------------------------\n\n';

      sendSpark(msgstr, true, objparams);
    });
  };


  const getMessageDetail = function(objparams) {
    let temp, objbody;

    // メッセージの詳細を取得する

    const options = getOptionsCisco('/v1/messages/' + objparams.messageid, 'GET');
    outputlog('objparams.messageid =', objparams.messageid);
    outputlog('getMessageDetail =', options);

    const req = https.request(options, (res) => {
      outputlog('STATUS: ' + res.statusCode);
      res.on('data', (body) => {
        objbody = JSON.parse(body);

        if (!objbody.html) {
          return null;
        }

        temp = objbody.html.split('</spark-mention>');
        objparams.messagebody = temp[temp.length - 1].split('</p>')[0];

        // 投稿者の cybozu のユーザー情報を取得する
        outputlog(objparams.messagebody);
        createRecord(objparams);
      });
    });

    req.on('error', (e) => {
      outputlog('get message detail err: ' + e.message);
    });

    req.end();
  };


  // ファイルを kintone に転送する
  const postFile = function(objparams) {
    const options = getOptionsKintone('/k/v1/file.json', 'POST'),
      boundary = 'afdasfd77a6s234ak3hs7';

    options.headers['Content-Type'] = 'multipart/form-data; boundary="' + boundary + '"';

    runHttpsPostFile(options, objparams, boundary, (err, resstring) => {
      if (err) {
        outputlog('ERROR: ファイル送信時にエラーが発生しました');
        return;
      }

      objparams.filekey = JSON.parse(resstring).fileKey;

      getMessageDetail(objparams);
      // ファイルの削除を行う
      unlinkFile(objparams.filename);
    });
  };

  // ファイルの本体を取得し、/tmp の中に配置する
  const getFileCisco = function(url, file, objparams) {
    outputlog('start getFileCisco');
    outputlog('file=' + file['content-disposition']);

    const options = getOptionsCisco(url.split(CYB.CSPARK.HOST)[1], 'GET'),
      fname = file['content-disposition'].split('filename="')[1];

    objparams.filename = CYB.NODE.FILEDIR + fname.substr(0, fname.length - 1);

    // ファイルの削除を行う
    unlinkFile(objparams.filename);

    // ファイルの本体を取得する
    runHttps(options, null, objparams.filename, (err, resstring) => {
      if (err) {
        outputlog('ERROR: ファイル本体取得時にエラーが発生しました');
        return;
      }
      postFile(objparams);
    });
  };


  exports.handler = function(event, context) {
    outputlog(event.data);
    const url = event.data.files[0];
    const options = getOptionsCisco(url.split(CYB.CSPARK.HOST)[1], 'HEAD');

    const objparams = {
      roomid: event.data.roomId, // Cisco Webex Messaging の スペースの ID
      messageid: event.data.id, // Cisco Webex Messaging の メッセージの ID
      messagebody: null, // Cisco Webex Messaging のメッセージの内容
      filekey: null, // kintone に送信するファイルの fileKey
      filename: null // kintone に送信するファイルの名前
    };

    // ファイル情報を取得する
    runHttps(options, null, null, (err, headers) => {
      if (err) {
        outputlog('ERROR: ファイル情報取得時にエラーが発生しました');
        return;
      }
      getFileCisco(url, headers, objparams);
    });
  };

})();

Amazon API Gateway の設定

次に Amazon API Gateway で、Cisco Webex Messaging の Webhook を受けて Lambda 関数を呼び出す API を作成します。

  1. AWS のコンソールで、Amazon API Gateway を選択し、「+API の作成」をクリックします。

  2. 「API 名」に任意の API 名を入力し、「API の作成」をクリックします。「説明」欄は任意です。

  3. リソース画面でアクションのドロップダウンから「リソースの作成」を選択します。

  4. 「リソース名」と「リソースパス」に任意の文字列を入力し、「リソースの作成」をクリックします。

  5. 今回は、Cisco Webex Messaging から POST されるため、アクションのドロップダウンの「メソッドの作成」より、「POST」を選択し、横の✔マークをクリックします。

  6. POST のセットアップ画面にて、各設定をして「保存」をクリックします。

    • 「総合タイプ」は Lambda 関数を選択します。

    • 「Lambda リージョン」は。ご自身のAWSのリージョンを選択します。

    • 「Lambda 関数」には Lambda で作成した関数名を入力します。

  7. Lambda 関数に権限を追加する設定画面が出るので「OK」を押します。

  8. アクションのドロップダウンの「API のデプロイ」を選択し、各設定をして「デプロイ」をクリックします。

    • 「デプロイされるステージ」は[新しいステージ]を選択します。
    • 「ステージ名」には任意の名称を記入します。ここは必須です。
    • 他の項目は任意入力です。

Cisco Webex Messaging Webhook の設定

いよいよ大詰めです。次は Cisco Webex Messaging の Webhook を設定します。

Cisco Webex Messaging for Developers (External link) にログインし、「Documentation」から Webhook の「Create a Webhook」を開きます。
「Test Mode」を「ON」にしたうえで、各設定をして「Run」をクリックします。

項目 設定値
Authorization Bearer 「Bot のアクセストークン」( Bot のアクセストークンの入手方法)
name 今回作成する Webhook の名前(任意)
targetUrl Lambda 関数の URL ( Lambda 関数の URL の確認方法)
resource messages(固定)
event created(固定)
filter mentionedPeople=me

Bot のアクセストークンの入手方法

Bot のアクセストークンは、以下の方法で取得できます。(Bot ID とは異なります)

  1. Cisco Webex Messaging for Developer (External link) の「My Apps」から、先ほど作った Bot を選択します。
  2. 「Access token」のリンクをクリックし、アクセストークンをコピーします。

Lambda 関数の URL の確認方法

Lambda 関数の URL は、以下の方法で取得できます。

  1. Amazon API Gateway の作成した API のステージは以下のメソッドをクリックします。
  2. 「POST」をクリックすると、URL 呼び出しが表示されます。

動作確認

長かったですが、設定と実装はすべて完了です。さっそく、動かしてみましょう!

まず、Cisco Webex Messaging アプリを立ち上げ、用意したスペースを開きます。
ちゃんと Bot がメンバーにいることを確認してくださいね。

モバイルやカメラ付きのタブレットでしたら、Cisco Webex Messaging アプリのカメラ機能を使って何か撮影してみましょう。
PC をご利用の場合は何か適当な画像を添付します。

メンションで先ほど作った Bot を指定して、投稿します。

実際に登録されているか、kintone アプリを確認してみましょう。

いかがでしたでしょうか?登録されていましたか?

もし登録に失敗している場合、AWS の CloudWatch でログを確認してみてくださいね。

おわりに

今回は、モバイルで撮影した画像を簡単に kintone アプリへ登録する方法を題材に、ファイルを Cisco Webex Messaging から kintone へ登録するやり方を紹介しました。

この方法を応用すると、工事現場や店舗の商品展示の現状などを写真に撮って、kintone の報告アプリにアップできます。

他にも、 Cisco Webex Board (External link) を利用すると、会議中の手書きのメモを kintone の議事録アプリに添付できます!

こんなイメージです。

会議が終ったら、さっと手書きした画像ファイルを会議で使ったスペースに添付します。
連携 Bot にメンションすると、kintone の議事録アプリに添付ファイルが登録された状態で、議事録のレコードが作成されます。

Cisco Webex Board を使うと、拠点間での会議や、リモートワーカーとの会議などが、より快適になりそうですね!

ここに挙げたいくつかの例以外にも、いろいろ便利な利用方法があると思います。ぜひお試しください。

information

この Tips は、2018 年 2 月版 kintone で動作を確認しています。