OpenWhisk シーケンスのソース

OpenWhiskは、IBMが提供するサーバレスアーキテクチャーです。
IBM Bluemix、またはOSSとして利用できます。
「トリガー」「アクション」「ルール」「シーケンス」の4つから構成されます。
概要図は以下を参照ください。
https://github.com/openwhisk/openwhisk/blob/master/docs/about.md

↓の自動化でトリガー、
　シーケンスへのリンクでシーケンス、
　をアクションと関連付けできます。

<img width="525" alt="secence action.png" src="https://qiita-image-store.s3.amazonaws.com/0/94200/4b5654b3-5057-6f00-23e5-7f71227c2ea2.png">

#１．アクション例

AWS Lambda , Azure Function , Google Function も近いアーキテクチャーですが、
Docker、Swift対応やWatson連携だけではなく、シーケンス設定の分かりやすさに
とソースが見えることだと思います。
そのソースコードを見てみましょう。

#Node.js

function main(params) {
	return { "message": "you sent me " + params.message };
}

#Python

import sys

def main(dict):
    if 'message' in dict:
        name = dict['message']
    else:
        name = 'stranger'
    greeting = 'Hello ' + name + '!'
    print(greeting)
    return {'greeting':greeting}


#Swift
func main(args: [String:Any]) -> [String:Any] {
      if let message = args["message"] as? String {
          return [ "greeting" : "Hello \(message)!" ]
      } else {
          return [ "greeting" : "Hello stranger!" ]
      }
}


# C inside Docker

Dockerの場合は、Dockerhub内に記載します。

イメージを使用して 
 が dockerhub 
 に接続されました: openwhisk/example

*request parameter
{
    "arg": "3"
}


*response
{
  "args": {
    "arg": "3"
  },
  "msg": "Hello from arbitrary C program!"
}

#２．シーケンス例
シーケンスはNode.jsで実装されます。

# Websocket 

Websocket send sample　です。
ws.send(payload　・・・　部分がメッセージ送信となります。


/**
 * Sends a payload message to the designated WebSocket URI
 *
 * @param uri       String representation of the WebSocket uri
 * @param payload   Message to send to the WebSocket
 * @return  Standard OpenWhisk success/error response
 */
function main(params) {
    if (!params.uri) {
        return whisk.error('You must specify a uri parameter.');
    }
    var uri = params.uri; 
   console.log("URI param is " + params.uri);

    if (!params.payload) {
        return whisk.error('You must specify a payload parameter.');
    }
    var payload = params.payload;

    console.log("Payload param is " + params.payload);

    var WebSocket = require('ws');

    var connectionEstablished = false;
    var ws = new WebSocket(uri);

    var connectionTimeout = 30 * 1000; // 30 seconds

    var promise = new Promise(function(resolve, reject) {
        setTimeout(function () {
            if (!connectionEstablished) {
                reject('Did not establish websocket connection to ' + uri + ' in a timely manner.');
            }
        }, connectionTimeout);

        ws.on('open', function () {
            connectionEstablished = true;

            console.log("Sending payload: " + payload);
            ws.send(payload, function (error) {
                if (error) {
                    console.log("Error received communicating with websocket: " + error);
                    ws.close();
                    reject(error);
                } else {
                    console.log("Send was successful.");
                    ws.close();
                    resolve({
                        'payload': payload
                    });
                }
            });
        });

        ws.on('error', function (error) {
            console.log("Error communicating with websocket: " + error);
            ws.close();
            reject(error);
        });
    });

    return promise;
}


# Slack

Slackへの連携です。Slackの基本認証とメッセージが必須パラメータです。

var request = require('request');

/**
 *   Action to post to slack
 *  @param {string} url - Slack webhook url
 *  @param {string} channel - Slack channel to post the message to
 *  @param {string} username - name to post the message as
 *  @param {string} text - message to post
 *  @param {string} icon_emoji - (optional) emoji to use as the icon for the message
 *  @param {boolean} as_user - (optional) when the token belongs to a bot, whether to post as the bot itself
 *  @param {object} attachments - (optional) message attachments (see Slack documentation for format)
 *  @return {object} whisk async
 */
function main(params) {

  if (checkParams(params)) {

    var body = {
      channel: params.channel,
      username: params.username || 'Simple Message Bot',
      text: params.text
    };

    if (params.icon_emoji) {
      // guard against sending icon_emoji: undefined
      body.icon_emoji = params.icon_emoji;
    }

    if (params.token) {
      //
      // this allows us to support /api/chat.postMessage
      // e.g. users can pass params.url = https://slack.com/api/chat.postMessage
      //                 and params.token = <their auth token>
      //
      body.token = params.token;
    } else {
      //
      // the webhook api expects a nested payload
      //
      // notice that we need to stringify; this is due to limitations
      // of the formData npm: it does not handle nested objects
      //
      console.log(body);
      console.log("to: " + params.url);

      body = {
        payload: JSON.stringify(body)
      };
    }

    if (params.as_user === true) {
        body.as_user = true;
    }

    if (params.attachments) {
      body.attachments = params.attachments;
    }

    var promise = new Promise(function (resolve, reject) {
      request.post({
        url: params.url,
        formData: body
      }, function (err, res, body) {
        if (err) {
          console.log('error: ', err, body);
          reject(err);
        } else {
          console.log('success: ', params.text, 'successfully sent');
          resolve();
        }
      });
    });

    return promise;
  }
}

/**
Checks if all required params are set
*/
function checkParams(params) {
  if (params.text === undefined) {
    whisk.error('No text provided');
    return false;
  }
  if (params.url === undefined) {
    whisk.error('No Webhook URL provided');
    return false;
  }
  if (params.channel === undefined) {
    whisk.error('No channel provided');
    return false;
  }
  return true;
}


# Cloudant 

ドキュメントNoSQLCloudantが変更された場合です。

/**
 * Create and update attachment for document in Cloudant database:
 * https://docs.cloudant.com/attachments.html#create-/-update
 **/

function main(message) {
  var cloudantOrError = getCloudantAccount(message);
  if (typeof cloudantOrError !== 'object') {
    return whisk.error('getCloudantAccount returned an unexpected object type.');
  }
  var cloudant = cloudantOrError;
  var dbName = message.dbname;
  var docId = message.docid;
  var attName = message.attachmentname;
  var att = message.attachment;
  var contentType = message.contenttype;
  var params = {};

  if(!dbName) {
    return whisk.error('dbname is required.');
  }
  if(!docId) {
    return whisk.error('docid is required.');
  }
  if(!attName) {
    return whisk.error('attachmentname is required.');
  }
  if(!att) {
    return whisk.error('attachment is required.');
  }
  if(!contentType) {
    return whisk.error('contenttype is required.');
  }
  //Add document revision to query if it exists
  if(typeof message.docrev !== 'undefined') {
    params.rev = message.docrev;
  }
  var cloudantDb = cloudant.use(dbName);

  if (typeof message.params === 'object') {
    params = message.params;
  } else if (typeof message.params === 'string') {
    try {
      params = JSON.parse(message.params);
    } catch (e) {
      return whisk.error('params field cannot be parsed. Ensure it is valid JSON.');
    }
  }

  return insert(cloudantDb, docId, attName, att, contentType, params);
}

/**
 * Insert attachment for document in database.
 */
function insert(cloudantDb, docId, attName, att, contentType, params) {
  return new Promise(function(resolve, reject) {
    cloudantDb.attachment.insert(docId, attName, att, contentType, params, function(error, response) {
      if (!error) {
        console.log("success", response);
        resolve(response);
      } else {
        console.log("error", error)
        reject(error);
      }
    });
  });
}

function getCloudantAccount(message) {
  // full cloudant URL - Cloudant NPM package has issues creating valid URLs
  // when the username contains dashes (common in Bluemix scenarios)
  var cloudantUrl;

  if (message.url) {
    // use bluemix binding
    cloudantUrl = message.url;
  } else {
    if (!message.host) {
      whisk.error('cloudant account host is required.');
      return;
    }
    if (!message.username) {
      whisk.error('cloudant account username is required.');
      return;
    }
    if (!message.password) {
      whisk.error('cloudant account password is required.');
      return;
    }

    cloudantUrl = "https://" + message.username + ":" + message.password + "@" + message.host;
  }

  return require('cloudant')({
    url: cloudantUrl
  });
}

# Watson Text to speech

watson-developer-cloudを呼び出すため、認証情報、文字情報等を送ります。

var watson = require('watson-developer-cloud');

function isValidEncoding(encoding) {
  return encoding === 'ascii' ||
    encoding === 'utf8' ||
    encoding === 'utf16le' ||
    encoding === 'ucs2' ||
    encoding === 'base64' ||
    encoding === 'binary' ||
    encoding === 'hex';
}

/**
 * Synthesizes text to spoken audio.
 * See https://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/text-to-speech/api/v1/
 *
 * @param voice The voice to be used for synthesis. Example: en-US_MichaelVoice
 * @param accept The requested MIME type of the audio. Example: audio/wav
 * @param payload The text to synthesized. Required.
 * @param encoding The encoding of the speech binary data. Defaults to base64.
 * @param username The Watson service username.
 * @param password The Watson service password.
 *
 * @return {
 *  "payload": "<encoded speech file>",
 *  "encoding": "<encoding of payload>",
 *  "content_type": "<content_type of payload>"
 * }
 */
function main(params) {
  var voice = params.voice;
  var accept = params.accept;
  var payload = params.payload;
  var encoding = isValidEncoding(params.encoding) ? params.encoding : 'base64';
  var username = params.username;
  var password = params.password;

  console.log('params:', params);

  var textToSpeech = watson.text_to_speech({
    username: username,
    password: password,
    version: 'v1'
  });

  var promise = new Promise(function(resolve, reject) {
    textToSpeech.synthesize({
      voice: voice,
      accept: accept,
      text: payload,
    }, function (err, res) {
      if (err) {
        reject(err);
      } else {
        resolve({
          payload: res.toString(encoding),
          encoding: encoding,
          mimetype: accept
        });
      }
    }, function (err) {
      reject(err);
    });
  });

  return promise;
}


# Weather 

Bluemix上の
Forcast 情報との連動です。

var request = require('request');

/**
 * Get hourly weather forecast for a lat/long from the Weather API service.
 *
 * Must specify one of zipCode or latitude/longitude.
 *
 * @param username The Weather service API account username.
 * @param username The Weather service API account password.
 * @param latitude Latitude of coordinate to get forecast.
 * @param longitude Longitude of coordinate to get forecast.
 * @param zipCode ZIP code of desired forecast.
 * @return The hourly forecast for the lat/long.
 */
function main(params) {
    console.log('input params:', params);
    var username = params.username;
    var password = params.password;
    var lat = params.latitude || '0';
    var lon = params.longitude ||  '0';
    var language = params.language || 'en-US';
    var units = params.units || 'm';
    var timePeriod = params.timePeriod || '10day';
    var url = 'https://twcservice.mybluemix.net/api/weather/v1/geocode/' + lat + '/' + lon;
    var qs = {language: language, units: units};

    switch(timePeriod) {
        case '48hour':
            url += '/forecast/hourly/48hour.json';
            break;
        case 'current':
            url += '/observations.json';
            break;
        case 'timeseries':
            url += '/observations/timeseries.json';
            qs.hours = '23';
            break;
        case '10day':
        default:
            url += '/forecast/daily/10day.json';
            break;
    }

    console.log('url:', url);

    var promise = new Promise(function(resolve, reject) {
        request({
            url: url,
            qs: qs,
            auth: {username: username, password: password},
            timeout: 30000
        }, function (error, response, body) {
            if (!error && response.statusCode === 200) {
                var j = JSON.parse(body);
                resolve(j);
            } else {
                console.log('error getting forecast');
                console.log('http status code:', (response || {}).statusCode);
                console.log('error:', error);
                console.log('body:', body);
                reject({
                    error: error,
                    response: response,
                    body: body
                });
            }
        });
    });

    return promise;
}


#シーケンス関連付

以下は、CloudandのDocumentDBに変更があった場合にアクションが起動する例です。

<img width="565" alt="relation.png" src="https://qiita-image-store.s3.amazonaws.com/0/94200/f89d7dd5-aa80-2eb4-e55e-352577806355.png">