2017年1月9日 星期一

使用Docomo雑談対話API做日文閒聊機器人

做BOT最難的地方應該就是「如何讓BOT可以應對閒聊」。現在的BOT大部分都還是做封閉式的,也就是說遇到不在定義好的scenario中的話,就用一個特定的term回應。
不過最近發現NTT Docomo推出了一個閒聊的交談API「雑談対話API」,可以做到連續性的閒聊,覺得很有趣,而且申請開發者是免費的。

不過畢竟因為是日本Docomo推出的,所以只支援日文,如果完全不懂日文的,就不用往下看了XD

雑談対話API介紹

能紀錄對話並持續地聊天

雑談対話API會用一個叫做context的parameter來紀錄對話的歷史,所以只要context一樣,雑談対話API會接續著跟你聊天。雑談対話API也支援日文的しりとり遊戲(根據上一個人說的單字的最後一個假名來當做第一個假名的單字遊戲,比方說いも→もの→のり,如果回答的單字是ん結尾就輸了),因此還有一個parameter來紀錄對話模式是一般對話還是しりとり遊戲。

不過,可能因為蒐集到的pattern還不夠多,所以有時候還是會誤判,會有很爆笑的對話內容。

 我問他推薦什麼巧克力,回我日本的未來如果是光明的就好了呢。他問我喜歡的音樂是什麼,我回Classic(古典音樂)就回我「好像要用英文來交談呢」XD整個很牛頭不對馬嘴
然後就被機器人教訓了....XDDDDDDDDDDD
好像訓練的pattern是讀到classic就會回跟禮貌有關的句子。
另外只要你回問號的話他就會很機靈(?)的轉移話題,果然很日本人XDDDDD

角色設定

雑談対話API一共有三個角色,回應你的角色可以在打API的payload中做設定。預設是一個叫做零(ゼロ)的26歲男性(雖然我覺得26歲男生會打てへぺろ(日文的啾咪)實在很不可思議),還有說關西腔的16歲女高中生桜子跟兩歲的寶寶ハヤテ。

讀取個人資訊功能(見人說人話)

在打payload的時候可以帶一些個人資訊過去,他就會依照你的個人資訊來回話。(比方說帶生日過去,問他幸運日是哪一天之類的)

詳細的payload設定可以參考API介紹

程式架構

開發語言是用Node.js,環境是AWS Lambda+DynamoDB。之前有人問過我怎麼用Claudia.js開發Line BOT,順道說一下目前是沒有支援的喔,也無法用Claudia-API-Builder來開發,我測試過是不行XD

程式架構如下:


當Lambda經由API Gateway接收到Messaging傳來的訊息之後,先到DynamoDB搜尋這個line id是否已經有雑談対話API的context紀錄,有的話就一起傳給雑談対話API。如果正確無誤,雑談対話API會回訊,這時就把context存回DynamoDB然後再回給Messaging API。

申請Line@帳號

首先必須要先有一個Line@帳號才能使用BOT的Messaging API,一般使用者是無法使用BOT的。
直接到Line@的Business Center申請就可以了,是免費的。

* Developer trial和Messaging API有什麼不同?

如果是只要回覆訊息,不用主動發訊息,那用Messaging API就可以了。但是如果想要主動發訊息,就只能選擇進階版或是先用Developer trial試試看。Developer trial有好友50人的上限,免費版則是只能有一千則訊息。

申請好帳號後就可到Line@Manager 後台管理帳號。
要讓BOT帳號可以透過自訂的Web API來回應使用者,必須在Line@Manager後台中的BOT設定裡將Webhook傳訊設定為「允許」,且建議取消自動回應。

接著到Line Developer (在Line@Manager中的Bot設定裡按下“Line Developers”也會引導到Line Developer)

在Line Developer的主頁最下面可以找到Channel Access Token,第一次進來應該是還沒有產生,按下issue就可以產生Token。

申請NTT Docomo開發者帳號

接下來就是主菜,到Docomo Developer Support Center 申請一個帳號,按下右上方的ログイン/新規登錄就可以申請帳號。Docomo接受用FB/Google的sns account,非常方便。
申請帳號的時候建議要申請為法人帳號(公司帳號),限制比較少。只要填寫公司名稱/部門名稱/電話就可以,它實際上不會去追查是否是公司,所以放心的登錄吧。
登錄完之後會先收到(docomo Developer support)開発者本登録完了のお知らせ的mail,這時候就可以登錄進マイページ來申請API。

申請NTT Docomo API Developer Key

  1. 進入NTT Docomo Developer Support Center的マイページ,按下左邊的API利用申請・管理

  2. 按下“新規API利用申請"

  3. 進入到申請頁面後,會要你填寫你的application名稱,說明和開始使用日期。這邊全部都可以亂填,開發key是不會審核的。但是開始使用日期可能就要考慮一下自己程式開發的進度,因為開發key只有三個月而已。
    有一個必填項目コールバックURL,是如果你的Application需要讓user登錄NTT Docomo做OAuth回call用的,這邊我們因為沒用NTT帳號的功能,因此填https://dummy就可以了。アプリケーションタイプ(Application type)我是選ネイティブアプリケーション(Native application),感覺用Line BOT的話選ウェブアプリケーション(Web application)應該也可以。

  4. 選擇API
  5. 基本上如果一開始就登記為法人那所有的服務都可以申請,另外如果申請トレンド記事抽出、文字認識API,必須要填寫姓名/居住地/電話。
  6. 取得Key
  7. 按下申請按鈕之後開發者Key就已經產生好了,不需審核

Code

'use strict'
var https = require('https');
var AWS = require("aws-sdk");
AWS.config.update({
  region: "us-west-2"
});
var docClient = new AWS.DynamoDB.DocumentClient();
var dbparams = {};
    dbparams.TableName = "LineChat";
//Line access Token
const accessToken=[Line Access Token]
// Docomo雑談API
const DocomoAPIKEY = [DocomoAPIKEY];

exports.handler = function (event, context) {
    console.log('EVENT:', JSON.stringify(event, null, 2));
    var event_data=JSON.parse(event.body);;
    var reply_token = event_data.events[0].replyToken;
    console.log('reply_token:', reply_token);
    var source=event_data.events[0].source;
    console.log('source_Type',source.type);
    var receive_id = source.groupId?source.groupId:source.userId;
    console.log('receive_id:',receive_id);
    var events=event_data.events[0];
    var postData;
    console.log('type:', events.type);
    getUserName(source).then(function(userName){
        var message = events.message;
        if(message.type=="text"){
            console.log(userName);
            dbparams.Key={"userID":receive_id};
            GetFromDynamo(dbparams).then(data => {
                var context=data.Item?data.Item.context:""
                var mode=data.Item?data.Item.mode:"dialog";
                var docomo_body = {
                   "utt": message.text,
                   "nickname":userName,
                   "context":context,
                   "mode":mode
                 };
                 return queryFromDocomo(JSON.stringify(docomo_body));
            }).then(ret=>{
                console.log('docomo reply',ret);
                resetdbparams("LineChat");
                dbparams.Item={"userID": receive_id,
                            "context": ret.context,
                            "mode": ret.mode
                            };
                console.log("InsertToDynamo",dbparams);
                InsertToDynamo(dbparams);
                postData=JSON.stringify({
                            replyToken: reply_token,
                            messages: [{type: "text", text: ret.utt}]
                        });
                callLine(postData);
            });
        }
        else
        {
            console.log(message.type);
            dbparams.Key={"userID":receive_id};
            docClient.delete(dbparams, function(err, data) {
                if (err) {
                    console.error("Unable to delete item. Error JSON:", JSON.stringify(err, null, 2));
                } else {
                    console.log("DeleteItem succeeded:", JSON.stringify(data, null, 2));
                }
            });
            postData=JSON.stringify({
                replyToken: reply_token,
                messages: [
                    {
                      "type": "sticker",
                      "packageId": "2",
                      "stickerId": "154"
                    },
                    {type: "text", text: "ごめなさい。意味がわからない。。。"}]
            });
            callLine(postData);
        }
    });
};



function resetdbparams(TableName){
    dbparams = {};
    dbparams.TableName = TableName;
}
function GetPostDataByMessage(reply_token,message){
    return JSON.stringify({
            replyToken: reply_token,
            messages: [{type: "text", text: message}]
        });
}
function queryFromDocomo(docomo_body){
    return new Promise(function(resolve,reject){
        var contentLen = Buffer.byteLength(docomo_body, 'utf8');
        var rp = require('minimal-request-promise'),
            options = {
                headers: {
                    "Content-type": "application/json; charset=UTF-8",
                    "Content-Length":contentLen+''
                },
            body: docomo_body
        };
        rp.post('https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=' + DocomoAPIKEY, options).then(
            function (response) {
                    //console.log('got response', response.body);
                    var docomoreply=JSON.parse(response.body);
                    resolve(docomoreply);
                },
            function (response) {
                console.log('got error', response.body, response.headers, response.statusCode, response.statusMessage);
                reject(response.statusMessage);
            }
        );
    });
}
function getUserName(source){
    if (source.type=='user') 
    {
        return new Promise(function(resolve, reject){
            var rp = require('minimal-request-promise'),
                options = {
                    headers: {
                        "Content-type": "application/json; charset=UTF-8",
                        "Authorization": "Bearer " +accessToken
                    }
                }
        rp.get('https://api.line.me/v2/bot/profile/'+source.userId, options).then(
            function (response) {
                //console.log('got response', response.body, response.headers);

                var userData=JSON.parse(response.body);
                resolve(userData.displayName);
            },
            function (response) {
                console.log('got error', response.body, response.headers, response.statusCode, response.statusMessage);
            }
        );
        })
    }
}
function GetFromDynamo(params)
{
    return new Promise(function(resolve, reject){
        docClient.get(params, function(err, data) {
                if (err) {
                    console.log(err, err.stack);
                    reject(Error(err));
                } else {
                    console.log('get item from DynamoDB.');
                    console.log(data);
                    resolve(data);
                }
        });
    });
}
function InsertToDynamo(params)
{
    docClient.put(params, function(err, data) {
        if (err)
            console.log(JSON.stringify(err, null, 2));
        else
            console.log(JSON.stringify(data, null, 2));
    });
}
function callLine(postData){
    
    console.log('callLine,postData:',postData);
    var contentLen = Buffer.byteLength(postData, 'utf8');
    var rp = require('minimal-request-promise'),
        options = {
            headers: {
                "Content-type": "application/json; charset=UTF-8",
                "Content-Length":contentLen+'',
                "Authorization": "Bearer " +accessToken
            },
        body: postData
    };
    //console.log('Step:','rp has completed');
    rp.post('https://api.line.me/v2/bot/message/reply', options).then(
        function (response) {
            console.log('got response', response.body, response.headers);
        },
        function (response) {
            console.log('got error', response.body, response.headers, response.statusCode, response.statusMessage);
        }
    );
    console.log('Step:','rp posted');
}

這段Code是只處理文字訊息,會把文字訊息丟給雑談対話API並紀錄context,如果收到的不是文字訊息,就把DynamoDB裡的cotext刪掉,然後跟你說他不知道你說什麼並回你一個貼圖。
把Code部署到AWS Lambda之後設定API Gateway,將API Gateway填回Line Developer中的Webhook URL即可。

有兩個小tips
  1.  API Gateway的建立:
    我是使用Claudia來部署我的code,因此才發現Claudia api builder也不支援,所以單純的只用Claudia來做Deploy。連API Gateway都是手動設定的。但不知道為什麼如果我是從Lambda的trigger來建立API Gateway,從line app就會打不過去(但是如果是從Line Developer的verify button的話則可以)。所以只好反過來從API Gateway著手,建立一個新的API Gateway之後將API Gateway的內容綁定到Lambda function。
  2. Line Developer的verify:Line Developer中webhook URL的設定旁邊有個verify button可以讓你確定webhook URL是否有綁對。他會打一個replyToken是00000000000000000000000000000000的payload到webhook URL,如果沒有特別對這個測試payload設定的話會一直看到verify result是502。這樣是正常的,代表Line確實有去打這個webhook URL,只是code寫得沒那麼好沒判斷reply token是假的的狀況。

2017年1月7日 星期六

[C#面試問題]Singleton instance相關問題

主要翻譯自http://www.csharpstar.com/csharp-interview-questions-part-1/,再加上我自己的一點想法。

What is the difference between Static class and Singleton instance?

– In c# a static class cannot implement an interface. When a single instance class needs to implement an interface for some business reason or IoC purposes, you can use the Singleton pattern without a static class.
– You can clone the object of Singleton but, you can not clone the static class object
– Singleton object stores in Heap but, static object stores in stack
– A singleton can be initialized lazily or asynchronously while a static class is generally initialized when it is first loaded

Static class和Singleton instance有什麼不同?
Singleton的意思就是程式中只能有一個實體。
  1. 在C#當中,static class無法實作interface。如果一個single instance class需要實做一個interface,或是需要控制反轉(IoC)的時候,你可以使用Singleton instance而不需要static class。
    *控制反轉我覺得這篇文章(用超人的故事讲解 IoC(控制反转) 和 DI(依赖注入))講得蠻清楚的。
  2. 在Singleton instance中物件是可以被複製的,但是static class則不行。
  3. Singleton instance裡的物件是存在heap(堆積),而static class則是存在stack(堆疊)。
    在Memory的管理中,物件內容會存放於Heap,而物件參考(指標)是存放於Stack。部分程式語言(像是比較早期的objective-C 或是C++)Heap是需要自己釋放記憶體空間的,而C#則是用了CLR來接管heap,加上GC原則上是不用自己去釋放,但是還是會有memory leak的可能,最明顯的例子就是office interop這種COM+ object,或是SharePoint的SPSite。(不知道SharePoint2016有沒有改善)
    比方說我們可以從以下的code來看物件在heap中的生命週期:
    Cat cat = new Cat();
    
    Cat.Name = "Sumomo";
    
    public void ChangeName(Cat c)
    {
        c.Name = "Sora";
    }
    
    在這邊ChangeName function傳入的變數c並不是Cat的值,而是point,所以當進入ChangeName的時候在heap中會有兩個point,都指向Cat。而修改值只是修改heap中的參考,當離開function之後就進入ChangeName function的物件就會被回收。

    另外在C#當中Stack中存放參數(value),Heap存放物件(reference type)。比方說,int x = 42;這段語法會在stack中存放一個42的值。
    如果我們再建立一個object指向x
    object o = x;
    這時候x轉換成o時是將x變數的值複製一份到o在Heap 裡的空間,這個作業稱為boxing。如果我們再把object o再轉一次放進另一個變數y
    int y = (int)o;
    這時就會再把o的值複製一份到stack中,這個動作稱之為unboxing
    *String是例外,所有的string都是reference type。
  4. Singleton instance可以initialize lazily或是非同步執行,而static class則是會在第一次執行的時候被初始化。

Why singleton pattern is considered an Anti-pattern ?

– Singletons aren’t easy to handle with unit tests. You can’t control their instantiation and they may retain state across invocations.
– Memory allocated to an Singleton can’t be freed.
– In multithreaded environment, access to the singleton object may have to be guarded (e.g. via synchronization).
– Singletons promote tight coupling between classes, so it is hard to test
 為什麼Singleton instance被認為是一種Anti-pattern?
  1.  Singleton instance不容易做unit test,因為你無法控制它的實作,又很容易殘留。
  2. 在 Singleton instance中allocate的記憶體無法被回收
  3.  在多執行緒的環境當中,如果用到Singleton物件可能會被鎖定,造成資源競爭。是可以加上同步的機制,但會造成效能低落。雖然可以透過Lazy Instantization,但這並不是thread-safe的寫法。
  4.  Singleton instance會造成class之間的高度耦合,所以不容易測試。

結語
Singleton instance不是不能用,只是設計的時候要更小心,在對的地方使用。

2016年12月20日 星期二

使用Claudia.js製作查詢台銀匯率API & 自動回覆匯率BOT(Skype 版本)

前一篇文章分享了如何製作簡單的Facebook BOT,但是應該不會有人希望自己的BOT只會找理由吧。而且Lambda的最大應用其實是Web API,而Claudia其實也是可以幫忙建立API的,一樣是很輕鬆愉快喔~
這篇文章會介紹如何用Claudia寫一隻自動抓取台銀最新的現金賣出匯率,並轉成JSON格式的API,再建立一個機器人並讓機器人自動回覆匯率。

建立API

初始化npm專案

  1. 建立一個資料夾並命名為currencyrate
  2. 打開Mac Terminal/Windows command line tool並移動到這個資料夾(cd currencyrate)
  3. 初始化npm專案:輸入
    npm init  
    

安裝套件

如果之前選擇不將Claudia安裝到Global,要在這個步驟打入
npm install claudia -D
來安裝Claudia到這個專案。

另外還必須要安裝幾個npm套件:
  • claudia-api-builder:Claudia自動部署API的套件
  • minimal-request-promise:很簡單就可以取得網頁上資料的套件
  • csv-parse:因為台銀提供csv檔格式的API,這個套件可以解析csv檔
在Terminal或是Cmd輸入
npm install claudia-api-builder minimal-request-promise csv-parse -S 
即可安裝這兩個套件。

API的Code

打開你的sublime或是其他的記事本工具,輸入以下的code:
'use strict'
const parse = require('csv-parse/lib/sync');
const rp = require('minimal-request-promise')
const ApiBuilder = require("claudia-api-builder");

var api = new ApiBuilder();
var rootUri = 'http://rate.bot.com.tw/';
var currencyMethod = 'xrt/flcsv/0/day';
var sellcashcol=12;
var currencyCode=0;

api.get("/currency/{code}", function (message) {
 var code = message.pathParams.code;
 var errorMessage,sellcash;
 var returnData={errorMessage:errorMessage,sellcash:sellcash};
 return rp.get(rootUri+currencyMethod)
            .then(response => {
               var body='#'+response.body;
               var records = parse(body, {comment: '#'});
               var arrayFound = records.filter(function(item) {
     return item[currencyCode].toLowerCase()==code.toLowerCase();
    });
    if (arrayFound.length>0)
    {
     returnData.sellcash=arrayFound[0][12];
    }else{
     returnData.errorMessage='Cannot find any currency from code '+code
    }
    return returnData;
              })

});

module.exports = api;
然後儲存成index.js。 這段Code會到台銀網站提供的API取得匯率,透過csv-parser解析之後取得目標匯率並回傳。

透過Claudia部署

透過Claudia部署,一樣只要一個指令 claudia create --name currency --region us-west-2 --api-module index
部署完可以透過URL https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/currency/USD測試是否有成功,也可以進入AWS Console,選擇"API Gateway"服務後找到currency API,在resource中按下"Test“按鈕測試。

建立BOT

參考上篇文章建立機器人,然後在機器人的folder中安裝minimal-request-promise套件,並將BOT的Code修改如下:
'use strict'
const botBuilder = require('claudia-bot-builder');
const excuse = require('huh');
const rp = require('minimal-request-promise')
module.exports = botBuilder(
function(message) {
 if (message.text.lastIndexOf('Currency/', 0) === 0)
      {
          var msgArr=message.text.split('/');
          return rp.get('https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/currency/'+msgArr[1])
            .then(response => {
                var rate = JSON.parse(response.body);
                if(rate.sellcash!=null || typeof(rate.sellcash)!='undefined'){
                  return('Currency '+msgArr[1]+' exchange rate today is '+rate.sellcash) 
                }
                else
                {
                  return('Error occured, reason is'+rate.errorMessage);
                }
              })
      }
      else
      {
        return 'Thanks for sending '+ message.text +'.Your message is very important to us, but '+excuse.get();
      }
});
*注意https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/currency/ 這邊是剛剛建立好的API的URL,要記得改不要全部照抄,不然BOT找不到API不會理人的~
存檔之後,透過Claudia create或Claudia update將BOT部署到AWS。

Skype BOT設定

在這裡介紹怎麼設定Skype的BOT。Skype BOT已經整合到Microsoft的BOT Framework,目前還在Preview階段,根據Microsoft BOT Framework的Q&A將會在2017年推出正式版,這裏的設定教學都是針對Preview版本。
  1. 首先,用你的microsoft/Azure帳號登入Microsoft BOT Framework,並按下"Register a BOT"
  2. 在Bot profile區段,給你的BOT一個名字,並且在bot handle給予這個BOT一個識別值(identity)。bot handle區段一旦設定了,就不能再改了。

  3. 在Configuration區段中設定:
    1. Messaging endpoint:Claudia deploy回應結果中,deploy->skype區段中的內容,通常是https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/skype
    2. 按下"Create Microsoft App ID and password"按鈕
    3. 給予這個應用程式一個名稱(可以和bot的名字不同),然後按一下"產生應用程式密碼"
    4. 系統產生一組密碼,一定要複製起來,待會透過Claudia設定的時候要用。密碼只有這個時候可以看得到,沒有複製到就只好再產生一次了。

  4. Microsoft BOT framework的設定告一段落,回到我們的command/terminal console,打入
    Claudia update --configure-skype-bot
  5. Claudia會要求你輸入Skype App ID和Skype Private key,其中Skype App ID就是應用程式識別碼,Skype Private key則是按下“產生應用程式密碼以繼續”按鈕時,系統自動產生的那組密碼,也就是剛剛特別要你存起來的那組密碼
  6. 至此,Skype BOT已經全部設定完成。回到Microsoft BOT Framework,找到"Test connection to your bot"區段,按下Test按鈕,如果看到下方出現OK "ok",就表示兩方的configuration沒有問題。

Test in skype

在Microsoft BOT Framework中,我們可以看到系統已經很貼心地將我們的Skype Channels建立好了。

有注意到除了Skype Channel,還有一個Web Chat嗎?如果我們是自己寫API沒有透過claudia-bot-builder,這個BOT可以同時支援Skype和Web Chat。但很可惜的是claudia-bot-builder並沒有支援Web Chat,所以雖然BOT Framework有幫我們建立Web Chat的Channel,也已經啟用,在頁面最底下的Chat區段測試,BOT是不會理我們的....
請在Skype Channel旁邊有一個"Add to skype"按鈕,按下去之後會開啟另一個視窗:
按下"Add to Contacts"之後會將Skype開起來,並把BOT加入Skype聯絡人中。這時候就可以和BOT對話了。

BOT收到Currency/[幣別代碼]的指令的時候,會自動呼叫我們寫好的查詢台銀匯率API並回應。如果收到其他的訊息則是一樣隨便找個理由搪塞你。

About Microsoft BOT Framework

設定好BOT Framework之後其實我蠻好奇的,因為可以看到Channels能夠設定的並不只有Skype和Web Chat而已,還有很多種Channel可以設定,包括Facebook Messenger/slack等。
這邊可以看到Microsoft一貫的包山包海風格,其實Microsoft BOT Framework就是一個BOT Connector,可以通吃各家的BOT。.NET的Developer也可以透過Visual Studio的BOT Framework template寫好BOT後發佈到雲端或網站上,設定好應用程式的ID和密碼來串接,只要有在BOT Framework設定,一隻API可以通吃所有的BOT。當然Claudia也是可以通吃,只不過因為要跟skype串, 而在跟skype串接的時候,看到Microsoft BOT又這麼大堆頭, 可是我只是想跟你家skype連線而已,有種「Microsoft真是一以貫之的熱(ㄐㄧ)心(ㄆㄛˊ)啊~」的感覺。

想透過Visual Studio的BOT Template及Microsoft BOT Framework來開發BOT的話,可以參考董大偉老師的文章:關於bot framework (3) - 建立一個最基本的bot (v3新版)

2016年12月15日 星期四

使用Claudia.js製作Facebook機器人

以前我們常常會需要一些Web Service幫我們處理一些簡單的GET/POST,這些Web Service可能很簡單並不複雜,甚至不需要UI,不過它總是需要一個host來立足,於是你可能就必須要找一台主機,如果是Windows的話還要有IIS(然後忍受IIS偶爾的衝康跟無止盡的windows update) 

所以當AWS推出了Lambda,身為一個Infra暴弱的Dev,真的會要給AWS一個大大的讚!這才是真正的SASS精神啊。畢竟我們不需要為了喝牛奶而去養一頭牛你說是吧。Lamdba讓程式開發人員可以專心的在程式的邏輯上面,將每個function獨立出來計費可以讓阮囊羞澀的獨立開發者與startup不需要負擔主機的維運費用,並且有精美的dashboard可以統計各個function的狀態,真的是非~常~的美好。

BUT,這世界上總是有個BUT,Lambda要寫python/node.js方便是方便,不過在部署的時候還是得設定IAM的Role,function才能正常運行。如果是AWS的菜鳥(尤其是Infra跟我一樣爆爛的)看到IAM的設定應該會很茫然。於是現在就有很多Lambda的部署工具,比方說Python用的Kappa,以及現在我想介紹的Claudia.js。

Claudia.js其實就是用AWS的Node.js framework將Lamdba的部署自動化,IAM以及Lambda的設定都在Claudia.js當中完成。之後如果要更新code,也只要一個指令就能完成,而且不需要登入AWS Console,是不是很方便啊。

以下就來介紹怎麼透過Claudia.js自動部署microservice並在Facebook上面做一個機器人。

首先你必須要申請一個AWS帳號(不然你是要部署到哪裡去),在此就不贅述如何申請了。

設定環境

因為Claudia.js是用來Deploy Node.js的microservice,所以你的電腦也要裝Node.js(這不是廢話嗎)
Node.js預設就有npm(Node.js的package管理工具)因此以下會提到npm指令,但不需要另外安裝。

安裝Node.js

Windows版

Command:
sudo curl http://nvm-latest.herokuapp.com | bash

手動安裝:到官網安裝最新版的Node.js
安裝完後在Command line工具中執行
node -v
就可以檢查Node.js是否正確安裝。

Mac版

雖然Mac的Homebrew好用到令人流淚,但因為node.js版本更新非常快速,因此推薦使用nvm安裝。透過nvm安裝Node可以很簡單的輕鬆切換Node.js版本。

  1. 如果你有安裝XCode,可以透過XCode來安裝nvm
    $ xcode-select --install
  2. 如果沒有安裝,可以透過curl來安裝
    sudo curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash
  3.  在Terminal中輸入
    nvm ls-remote
    ,找出最新的版本,我寫這篇文章時最新的版本是7.2.1
  4. 執行
    $ nvm install 7.2.1
    $ nvm alias default 7.2.1
    安裝node.js

設定IAM Policy

因為AWS的IAM Role Policy預設並沒有啟用AWS Gateway API,所以要先將User Policy建立起來,Claudia在部署的時候才不會因為沒有權限而報錯。
  1. 登入AWS Console,選擇IAM service,然後按一下"Policies"。
  2. 按下"Create Policy"按鈕
  3. 選擇“Create Your Own Policy”
  4. 在Policy名稱上面填上“API_Gateway_Resources_Full”,並在Policy Document中填入以下資料:
  5. {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "Stmt1467321765000",
                "Effect": "Allow",
                "Action": [
                    "apigateway:*"
                ],
                "Resource": [
                    "*"
                ]
            }
        ]
    }

  6. 按下"Create Policy"就完成了。


新增一個IAM使用者

  1. 到AWS主控台,從上方的下拉選單選擇IAM服務,進入IAM服務後選擇左邊的"Users" ,並按下藍色的"Add User"
  2. 輸入使用者名稱,並勾選“Programmatic access”,然後按下“Next:Permissions"
  3. 選擇”Attach existing policies directly“ 並在底下的policy勾選IAM Full permission / Lambda Full permission,然後按下"Next: Review"
  4. 確定這個使用者有IAM Full Access 和Lambda Full Access之後,按下”Create User“
  5. 按下"Download .csv"按鈕可以下載這個使用者的Credential, 強烈建議要下載下來因為當按下close之後你就再也沒有機會看到Secret access key 了,只能重新建立一個使用者
    (都已經說了This is the last time these credentials will be available to download.了~)
 

建立登入資料

這個步驟是讓你在Deploy的時候不需要再輸入帳號密碼。
用Sublime或Notepad等純文字編輯器打開 Mac/Linux 的 ~/.aws/credentials 或 Windows 的 C:\Users\USERNAME\.aws\credentials,加入以下指令:
[default]
aws_access_key_id = YOUR_ACCESS_KEY_ID

aws_secret_access_key = YOUR_ACCESS_SECRET_KEY


YOUR_ACCESS_KEY_ID指的是剛剛建立好的帳號中,最後一步顯示的access_key_id,YOUR_ACCESS_SECRET_KEY則是被隱藏的access Secret key.
如果剛剛沒有把資料保存下來...再回去重新建立一個使用者吧

安裝Claudia.js(安裝至Global,可忽略這個步驟)

因為已經安裝好npm了,安裝Claudia就是一件非常輕鬆簡單的事情。在Mac的Terminal或是Windows的console打入以下指令:
npm install claudia -g
 
Claudia就裝好了。
[12/18]*補充說明:npm指令中-g是全域安裝(安裝到global)其實因為Claudia只是部署工具,比較建議隨著專案來安裝,如果不想安裝到Global可以在下面的安裝套件步驟中一起安裝Claudia.

開始寫機器人囉!

初始化npm專案

  1. 建立一個資料夾並命名為aLittleGreeter
  2. 打開Mac Terminal/Windows command line tool並移動到這個資料夾(cd aLittleGreeter)
  3. 初始化npm專案:輸入
    npm init
      

安裝套件

(如果選擇不將Claudia安裝到Global,可以在這個步驟打入
npm install claudia -D
來安裝Claudia到這個專案。)
在寫機器人之前,我們還需要安裝兩個npm套件
  • claudia-bot-builder:有了這個套件,Claudia才能幫你設定和其他通訊軟體(例如Facebook, skype, slack)
  •  huh:這是亂數取一個理由的npm套件,可以在系統掛掉的時候第一時間回信給PM解釋(誤)
在Terminal或是Cmd輸入
npm install claudia-bot-builder huh -S 
即可安裝這兩個套件。

 BOT的Code

(讀者表示:終於到寫Code了)
打開你的sublime或是其他的記事本工具,輸入以下的code:
const botBuilder = require('claudia-bot-builder');
const excuse = require('huh');
 
module.exports = botBuilder(
function(message) {
 return 'Thanks for sending '+ message.text +'Your message is very important to us, but '+excuse.get();
});
然後儲存成index.js。 這是一個很簡單的機器人,會把你說的話再重複一次,然後找個理由回(ㄈㄨ)答(ㄧㄢˇ)你。

透過Claudia把Code部署上AWS

Claudia會幫你把Lambda和Role該設定的全部都設定好,就交給Claudia部署就可以了。只需要一個指令 claudia create --name aLittleGreeter --region us-west-2 --api-module index
  • name 屬性:Claudia會使用name來建立IAM Role和Lamdba
  • region屬性:你要部署到哪個AWS的region,在這邊使用us-west-w2指的是美國的奧勒岡州。可以參考AWS Region列表選擇要部署到哪個region
  • api-module屬性:Lamdba程式的進入點。我們的檔名是index.js,所以將api-module設定為index,Claudia就會將index.js的內容部署到Lamdba。

如果沒有問題,就會看到Claudia一直在幫你跑AWS上面的流程了。
部署完成以後,應該會看到以下的訊息
{
  "lambda": {
    "role": "aLittleGreeter-executor",
    "name": "aLittleGreeter",
    "region": "us-west-2"
  },
  "api": {
    "id": "[api-gateway-id]",
    "module": "index",
    "url": "https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest",
    "deploy": {
      "facebook": "https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/facebook",
      "slackSlashCommand": "https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/slack/slash-command",
      "telegram": "https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/telegram",
      "skype": "https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/skype",
      "twilio": "https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/twilio",
      "kik": "https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/kik",
      "groupme": "https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/groupme",
      "viber": "https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/viber",
      "alexa": "https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/alexa"
    }
  }
}

然後回到AWS的IAM 管理介面,選擇Role,可以看到Claudia已經幫你建好一個Role了
 

再切到AWS的Lambda管理介面,發現Claudia也建好Lambda的函數了



這樣AWS的部署就已經完成了。有沒有很方便呀。

設定Facebook粉絲專頁機器人

OK,我們已經部署完AWS的部份了,接下來是Facebook的設定。
  1. 先建立一個粉絲專頁
  2. Facebook Developer應用程式開發頁面,如果之前沒有註冊成為Facebook Developer,應該會先看到"立即註冊"的按鈕,按下立即註冊成為Facebook Developer。
  3. 按下“新增應用程式”,類別選擇“專業應用程式”,取好名字以後選擇“建立應用程式編號"
  4. 建立好之後會到這個應用程式的主控台,在產品設定中按下Messanger區塊旁的"開始使用"
  5. 在"權杖產生"中,先選擇先前建立好的粉絲專頁,在旁邊的粉絲專頁存取權杖中會顯示一組權杖,先記錄下來。
  6. 這時我們要在Claudia設定與Facebook連動,回到電腦的Terminal或是Cmd Console,打入以下指令:
    claudia update --configure-fb-bot
    Claudia會將Code重新部署,部署完後會出現以下的回應:
    Facebook Messenger setup
    
    
    Following info is required for the setup, for more info check the documentation.
    
    
    Your webhook URL is: https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/facebook
    
    Your verify token is: [verify Token]
    
    Facebook page access token: 
    
  7. Claudia詢問的Facebook page access token就是剛剛Facebook應用程式中粉絲專頁的權杖,將剛剛複製好的權杖貼入Terminal
  8. 接著Claudia會詢問Facebook App Secret,到應用程式中選擇“主控台”,在主控台中找到"應用程式秘鑰",按下顯示後將內容貼到Terminal
  9. 到這邊AWS端的連動已經設定完成,接下來回到Facebook應用程式
  10. 到"Webhooks"區段中選擇“Setup Webhook”
  11. 填寫Webhook資訊:
    • 回呼網址:填上Claudia回應中的deploy->facebook區段中的內容,通常是https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/latest/facebook
    • 驗證權杖:在剛剛打入
      claudia update --configure-fb-bot
      的時候,Claudia的回應中有一段 Your verify token is: [verify Token],填入這個verify token
    • 訂閱欄位:至少要勾選messages
  12. 儲存之後,就可以看到Webhooks區段有綠色勾勾,代表已經設定完畢
  13. 然後就可以嘗試傳送訊息給你的粉絲專頁了

茶包射手

  • 部署的時候出現npm error

    檢查一下Code有沒有寫錯,或是需要的npm套件沒有安裝。
  • 部署的時候出現AccessDeniedException

    檢查IAM的使用者是否已經有IAM和Lambda的Full permission
  • 出現package.json does not exist in the source folder

    通常是因為沒有執行npm init,可以再執行一次讓npm產生package.json
  • 部署失敗,出現Make sure you have the latest version of node.js and npm installed.

    這是因為Node.js太舊了,如果Mac有安裝Homebrew只要透過Homebrew更新就可以了
    brew upgrade node
    如果沒有Homebrew,可以透過npm清除Node.js的Cache並下載最新版:
    sudo npm cache clean -f
    sudo npm install -g n
    sudo n stable
    
    Windows的使用者也可以透過npm來更新node.js
    npm cache clean
    npm update -g
    
  • 我想更新我的Code,重新執行Claudia出現這樣的訊息:

    Role with name aLittleGreeter-executor already exists.

    這是因為重複執行Claudia create。Claudia create會自動建立AWS的IAM Role和Lamdba,而名字是identity不能重複的。所以如果要更新Code或是設定其他的組態(像是Facebook連動)請用Claudia update.如果想要更新的不是只有Code本身,像是想要換AWS的region,那麼只能將建立好的IAM Role, Lamdba等等都先手動刪除再執行Claudia create。
  • Facebook機器人不會回應我的朋友

    這是正常的,這個機器人目前只會回應你而已,當全部開發完成之後會需要提交給Facebook審查,可以參考Facebook的審查說明。人工審查需要七天左右,審查通過其他人才可以用。

2013年11月24日 星期日

Clear umbraco recycle bin

delete from  cmsPreviewXml  where nodeId in (select id from umbracoNode where path like '%-20%' and id!=-20)
delete from cmsContentVersion where ContentId in (select id from umbracoNode where path like '%-20%' and id!=-20)
delete from cmsContent where nodeId in (select id from umbracoNode where path like '%-20%' and id!=-20)

delete from cmsContentXML where nodeId in (select id from umbracoNode where path like '%-20%' and id!=-20)

delete from cmsDocument where nodeId in (select id from umbracoNode where path like '%-20%' and id!=-20)

delete from cmsPropertyData where contentNodeId in (select id from umbracoNode where path like '%-20%' and id!=-20)

-- delete the XML nodes....
delete from umbracoNode where path like '%-20%' and id!=-20

2011年5月27日 星期五

Query list item in external list by object model

SPQuery query = new SPQuery();
var context = SPServiceContext.GetContext(SPSite);
using (var scope = new SPServiceContextScope(context))
{
SPListItemCollection itemcollec = olist2.GetItems(query);
}

2011年5月10日 星期二

Why cannot activate sandbox solution in SharePoint 2010

If you upload a sandbox solution to SharePoint 2010 solution gallery, and would like to activate it, however you find you cannot click the activate button. It's disabled.

Please go to your centeral administrator and click "Service on server" and make sure that your Microsoft SharePoint Fundation SandBox code service has started.