Step by step .Net Web API 2 撰寫 LineBot(Webhook) (使用套件)

先感謝董大偉(David)老師提供LineBot套件,在操作上得心應手,講解非常細心 : )

文章概要

1. 註冊LineBot
2. WebAPI設定
   2.1 前置作業
   2.2 Webhook服務空間設定(以Azure為例)
3. Webhook程式撰寫
   3.1 ReplyMessage
   3.2 PushMessage
   3.3 ButtonsTemplate
   3.4 CarouselTemplate
   3.5 ConfirmTemplate
   3.6 Channel Secret應用
4. 結論
5. 參考網址

1. 註冊LineBot

先到Line Business Center網站,登入您的Line帳號。

(Line Business Center 2017/09/21關閉服務,會用別的服務來代替,屆時在改變方法註冊)

我們在[帳號清單]底下建立一個Messaging API帳號,點選[開始使用Developer Trial],剩下資訊請自行設定。



在帳號清單底下可以看到剛剛建立的項目,點選[LINE@ MANAGER],剩下資料可自行設定,主要把[Messaging API設定]打開。


會警告說開啟Bot將會消失一對一的功能,本來就是要已開發Bot為主,設定上請跟以下圖片一樣,我們需要用Webhook傳訊。

在回到[帳號清單]可以發現有新的選項[Line Developers],點選,接著可以看到Webhook設定,那麼最主要[Channel Access Token],點選[ISSUE]將會產生新的一組,務必別外流,程式面需要用到該Token,當然更嚴謹需要用到[Channel Secret]之後一併說明。請先自行加入該Bot為好友。


2. WebAPI設定

2.1 前置作業

我是用VS2015示範,新增空白Web API,加入一個Controller(範例命名名稱為LineBotController),在專案屬性裡設定部分,把[ChannelAccessToken]與[ChannelSecret]值做設定,後面的值請自行更改。





在套件部分請在nuget安裝好。

2.2 Webhook服務空間設定(以Azure為例)

到這裡為止應該都是簡單的設定,很多人應該會卡在這項,Line為了安全性,Webhook需要HTTPS服務空間,在此範例我是用Azure免費的就夠測試了,等之後專案開發在去考慮服務空間問題。

至於哪裡還有提供HTTPS我給幾個選項:1. Ngrok 2. Letsencrypt

其中Ngrok比較容易實現,剩下可能再自行找找或自己有憑證先行架設好。

Azure範例:

為了測試Webhook回應成功,在LineBotController底下程式碼如下。

public class LineBotController : ApiController
{
    [HttpPost]
    public IHttpActionResult POST()
    {
        try
        {
            return Ok();
        }
        catch
        {
            return Ok();
        }
    }
}

Azure自行註冊好,先專案點選發行,剩下自行設定,不在描述,Azure已提供最簡單的方式來做發行 : ) 這邊如果講解不清楚可以去找找資料,Webhook網址應該會是https:{YourDomainName}/{api}/{ControllerName}。



接著,回到Line Developers後台,把Webhook服務網址貼上再點選[VERIFY],如果設定都無誤的話會是成功的狀態。

到目前為止都是設定,設定都好的話接下來就是Webhook程式撰寫 : )

3. Webhook程式撰寫

程式撰寫部分會先從簡單的開始,逐一做介紹,補充一下,您的LineId並非唯一,LineId可能在不同的Bot上所對應的ID不一定相同,這點需要注意,另外有些功能電腦版不支援,例如Template功能等。

例如:
UserA對於Bot A的LineId為A
UserA對於Bot B的LineId為B

3.1 ReplyMessage

這主要可以回覆使用者訊息,當使用者發出訊息時候利用接收到的ReplyToken來做回覆。

public class LineBotController : ApiController
{
    [HttpPost]
    public IHttpActionResult POST()
    {
        string ChannelAccessToken = Properties.Settings.Default.ChannelAccessToken;

        try
        {
            // 解析Json資料
            string postData = Request.Content.ReadAsStringAsync().Result;
            var ReceivedMessage = Utility.Parsing(postData);
            Bot bot = new Bot(ChannelAccessToken);

            string Lineid = ReceivedMessage.events.FirstOrDefault().source.userId;
            // 得到使用者資訊 (可以知道使用者狀態消息、暱稱等)
            var UserInfo = bot.GetUserInfo(Lineid);
            
            // 1.【ReplyMessage】
            string Message = "";
            Message = $"{UserInfo.displayName} 你好,你說了:{ReceivedMessage.events.FirstOrDefault().message.text}";
            Utility.ReplyMessage(ReceivedMessage.events.FirstOrDefault().replyToken, Message, ChannelAccessToken);

            return Ok();
        }
        catch
        {
            return Ok();
        }
    }
}

3.2 PushMessage

PushMessage非常簡單,只要知道對方LineId即可發出訊息,也可以發出貼圖訊息,如果要知道有那些貼圖可用,請參考此網站

public class LineBotController : ApiController
{
    [HttpPost]
    public IHttpActionResult POST()
    {
        string ChannelAccessToken = Properties.Settings.Default.ChannelAccessToken;

        try
        {
            // 解析Json資料
            string postData = Request.Content.ReadAsStringAsync().Result;
            var ReceivedMessage = Utility.Parsing(postData);
            Bot bot = new Bot(ChannelAccessToken);

            string Lineid = ReceivedMessage.events.FirstOrDefault().source.userId;
            // 得到使用者資訊 (可以知道使用者狀態消息、暱稱等)
            var UserInfo = bot.GetUserInfo(Lineid);
            
            // 2.【PushMessage】
            bot.PushMessage(Lineid, "PushMessage");
            bot.PushMessage(Lineid, 1, 113);

            return Ok();
        }
        catch
        {
            return Ok();
        }
    }
}

3.3 ButtonsTemplate

ButtonsTemplate呈現,主要讓使用者不用用輸入文字方式來進行動作,以點選Button方式進行動作。

注意:Template上選單的圖片網址需為HTTPS

這些動作會有URI、Postback、Message等,我先把這些方法寫成介面,程式碼可以更簡潔,等等看應用後會更清楚ButtonsTemplate再做什麼。

interface ITemplateAction
{
    void Message(string label, string text);
    void Uri(string label, string uri);
    void Postback(string label, string data, string text);
}
public class AddAction : ITemplateAction
{
    List<TemplateActionBase> templateactions = new List<TemplateActionBase>();

    public AddAction(List<TemplateActionBase> templateactions)
    {
        this.templateactions = templateactions;
    }

    /// <summary>
    /// 訊息
    /// </summary>
    /// <param name="label">按鈕文字</param>
    /// <param name="text">文字</param>
    public void Message(string label, string text)
    {
        templateactions.Add(new MessageActon()
        { label = label, text = text });
    }

    /// <summary>
    /// PostBack
    /// </summary>
    /// <param name="label">按鈕文字</param>
    /// <param name="data">PostBack Data</param>
    /// <param name="text">文字</param>
    public void Postback(string label, string data, string text)
    {
        templateactions.Add(new PostbackActon()
        { label = label, data = data, text = text == "" ? null : text });
    }

    /// <summary>
    /// URI
    /// </summary>
    /// <param name="label">標籤</param>
    /// <param name="uri">網址</param>
    public void Uri(string label, string uri)
    {
        templateactions.Add(new UriActon()
        { label = label, uri = new Uri(uri) });
    }
}

如此一來要增加Action更容易了,介紹一下Action所需用途。

Message:要使用者說出某些對話。
URI:可以連結該網址。
Postback:例如您想要在背後運行就要用此方式,當然跟Message也很像也可以發出一個對話,但通常我都拿來背後運行比較多,也就是說,使用者點選Postback的Action,後端會接送一串Postback文字,看這些文字要做什麼事情由您決定。

// 3.【ButtonsTemplate】
//接收PostBack資料
if (ReceivedMessage.events.FirstOrDefault().postback != null)
{
    string PostBack = ReceivedMessage.events.FirstOrDefault().postback.data;
    bot.PushMessage(Lineid, $"我收到你postback資料為{PostBack}");
}
else
{
    var Template = new List<TemplateActionBase>();
    AddAction action = new AddAction(Template);

    action.Message("文字標籤", "點選後發送的文字");
    action.Uri("連到Google", "https://www.google.com.tw");
    action.Postback("Postback1", "Postback1", null);
    action.Postback("Postback2", "Postback2", "使用者點選後所要呈現的文字");

    var ButtonsTemplate = new ButtonsTemplate()
    {
        thumbnailImageUrl = new Uri("https://pics.iclope.com/news/test-cigarette-electronique.jpg"),
        title = "標題",
        text = "內容文字",
        altText = "當電腦版看到的文字",
        actions = Template
    };

    // 發送ButtonsTemplate
    bot.PushMessage(Lineid, ButtonsTemplate);
}

3.4 CarouselTemplate

算是ButtonsTemplate延伸,其實就是多選單而已,Action都一樣。但值得注意的是,我在開發期間發現一個問題,也就是說選單規定是要同一列的,最多五個選單,但是列表的Action都要一致,否則會出錯。

例如:選單 * Action數量 其中Action數量要一致,選單不能大於五個。

// 4.【CarouselTemplate】
List<TemplateActionBase> ta1 = new List<TemplateActionBase>();
List<TemplateActionBase> ta2 = new List<TemplateActionBase>();
List<TemplateActionBase> ta3 = new List<TemplateActionBase>();

AddAction act1 = new AddAction(ta1);
AddAction act2 = new AddAction(ta2);
AddAction act3 = new AddAction(ta3);

//需要一致Action數量
act1.Message("Test1", "Test1");
act1.Message("Test2", "Test2");

act2.Message("Test3", "Test3");
act2.Message("Test4", "Test4");

act3.Message("Test5", "Test5");
act3.Message("Test6", "Test6");

var ct1 = new Column()
{
    title = "選單一",
    text = "選單一說明",
    thumbnailImageUrl = new Uri("https://pics.iclope.com/news/test-cigarette-electronique.jpg"),
    actions = ta1
};

var ct2 = new Column()
{
    title = "選單二",
    text = "選單二說明",
    thumbnailImageUrl = new Uri("https://pics.iclope.com/news/test-cigarette-electronique.jpg"),
    actions = ta2
};

var ct3 = new Column()
{
    title = "選單三",
    text = "選單三說明",
    thumbnailImageUrl = new Uri("https://pics.iclope.com/news/test-cigarette-electronique.jpg"),
    actions = ta3
};

List<Column> CarouselTemplateSub = new List<Column>();

CarouselTemplateSub.Add(ct1);
CarouselTemplateSub.Add(ct2);
CarouselTemplateSub.Add(ct3);

var CarouselTemplate = new CarouselTemplate()
{
    altText = "當電腦版看到的文字",
    columns = CarouselTemplateSub
};

bot.PushMessage(Lineid, CarouselTemplate);

3.5 ConfirmTemplate

像我們網頁上的確認視窗,有同意跟不同意選項,當然Action可以自己自定義,應用面很廣,很常用的Template。

// 5.【ConfirmTemplate】
if (ReceivedMessage.events.FirstOrDefault().postback != null)
{
    string PostBack = ReceivedMessage.events.FirstOrDefault().postback.data;
    bot.PushMessage(Lineid, $"{PostBack}");
}
else
{
    var ConfirmActon = new List<TemplateActionBase>();

    AddAction action = new AddAction(ConfirmActon);
    action.Postback("是", "您選擇是", null);
    action.Postback("不是", "您選擇不是", null);

    ConfirmTemplate ConfirmTemplate = new ConfirmTemplate();
    ConfirmTemplate.text = "內容文字";
    ConfirmTemplate.altText = "電腦版所看到文字";
    ConfirmTemplate.actions = ConfirmActon;

    bot.PushMessage(Lineid, ConfirmTemplate);
}

3.6 Channel Secret應用

我在這篇文章看到後才知道Channel Secret應用,雖然Line是HTTPS資料傳輸,也要防範CSRF攻擊,Line傳給Webhook資料時會在Header夾帶一個X-Line-Signature簽名,這個Signature是由Channel Secret作為私鑰,與Request Body進行HMAC-SHA256進行加密計算。只有開發人員能夠驗證這組X-Line-Signature是否有效。詳細可看Line Developer文件。

public class Signature : AuthorizeAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        HttpRequestMessage Request = actionContext.Request;

        IEnumerable<string> headerValues;
        if (Request.Headers.TryGetValues("X-Line-Signature", out headerValues))
        {
            string lineSignature = headerValues.FirstOrDefault(),
                   reqBody = Request.Content.ReadAsStringAsync().Result;

            byte[] screct = Encoding.UTF8.GetBytes(Properties.Settings.Default.ChannelSecret),
                   body = Encoding.UTF8.GetBytes(reqBody),
                   hash = new HMACSHA256(screct).ComputeHash(body);
            string mySignature = Convert.ToBase64String(hash);

            if (mySignature == lineSignature) return;
        }
        HttpResponseMessage Response = Request.CreateResponse(HttpStatusCode.ExpectationFailed);
        Response.StatusCode = HttpStatusCode.InternalServerError;
        actionContext.Response = Response;
    }
}

自己定義好後Signature AuthorizeAttribute,只要在進入這Controller之前先檢查X-Line-Signature是否符合,如果不對會回應錯誤,反之,進入該Controller執行。

[HttpPost]
[Signature]
public IHttpActionResult POST()
{

}

4. 結論

此篇介紹是比較常見到的功能,當然還有更多功能還沒介紹,套件上董大偉老師一只有做更新,在開發上可以更多的應用。不過在開發上會有很多問題存在,例如最常見的如何知道使用者現在狀態是什麼? 例如處理問卷就會非常瑣碎,一來一往增加許多複雜度,必須記住使用者選項、狀態等,主要沒有網頁上的Session等幫你記住任何值,所有狀態、命令等都先存在資料庫,所以越多層的問答會非常麻煩,必須一層一層的拆,盡量依簡單方式來進行。ChatBot非常熱門,應用面很廣,甚至可以請假、點餐、消費等方式,算是一個溝通介接的窗口,相信往後ChatBot發展會更深入。

當然最重要的是Source Code可以下載。

5. 參考網址

LINE Developer 官方文件
.NET Walker
一步一步用 .NET Web API 撰寫 LINE Webhook (LINEBot)