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で動作を確認しています。