建立 LINE 商用帳號
這篇文章主要的重點會擺在使用 .NET Web API 建立 LINE 的 webhook 功能,建立帳號的部分這邊僅會簡單帶過。首先先確認你已經在 LINE Business Center 中已經將機器人帳號建立完成,請注意 Messaging API 要已經是 PUBLISHED 才算是 OK。
點選 LINE Developer 可以進入主控台。可以設定 Webhook URL 或取得 Channel Access Token、Channel Secret 等必要資訊。
建立 .NET Web API Project
.NET Web API 是微軟提供的一套建立 HTTP 服務的 Framework,它可以幫助我們快速建立擁有 RESTful 風格的 Web App。
選擇 ASP.NET Web Application 專案,再選擇使用 Web API Template,就可以建立完成。
建立 LINE Webhook Controller
為了讓 LINE 將使用者訊息傳給我們處理,根據 LINE API Reference 中 Webhook API 的說明,首先必須建立一個 HTTP POST 的 API 供 LINE 呼叫。在 Controllers 目錄中加入一個 Web API 2 Controller - Empty 命名為「LineController」。
接下來,加入 Webhook 入口並設定基本的 Router。
[RoutePrefix("line")] public class LineController : ApiController { [HttpPost] [Route] public IHttpActionResult webhook() { return Ok("OK"); } }
其中
而
[HttpPost] :規定只接受 HTTP POST 方法[Route] :因為沒有給任何參數,表示 URL 為 http://{hostname}/line 時,就會執行這個方法[Route("haha")] :以此類推,如果設定成這樣,就會在 URL 為 http://{hostname}/line/haha 才會執行。
完成後,就可以執行並使用 HTTP POST http://{hostname}/line 看到一個顯示 OK 的基本畫面。
綁定 Webhook URL
剛才建立好的 https://{hostname}/line 就可以設定到您帳戶主控台中的 Webhook URL,並點選「VERIFY」進行驗證,如果 URL 是有效的就會顯示 Success.
Webhook URL 是要告訴 LINE 如何將使用者的訊息傳送給你,而且必須要 SSL 加密 (https),SSL 必須是擁有第三方認證的,不能是 self signed certificate。這一點是我覺得比較麻煩的,在這邊假設您已經都準備好了。
建立 LINE Webhook Data Model
為了取得存放在 Request 中的 Body 資料,這邊採用一個比較嚴謹的作法,建立 LINE 的 Webhook data Model。這樣做主要原因有二,一是維護方便,資料格式與屬性已經都定義在 Model 中,不用每次都回到官網查文件,另外也可以借用 .NET Web API 強大的 mapping 機制,它會自動將資料對應到所指定的 Model 中,再交給我們存取。
根據官方文件的說明,從 LINE 傳遞過來的資料大概會長這樣:
{ "events": [ { "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA", "type": "message", "timestamp": 1462629479859, "source": { "type": "user", "userId": "U206d25c2ea6bd87c17655609a1c37cb8" }, "message": { "id": "325708", "type": "text", "text": "Hello, world" } }, { "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA", "type": "follow", "timestamp": 1462629479859, "source": { "type": "user", "userId": "U206d25c2ea6bd87c17655609a1c37cb8" } } ] }
觀察官方文件之後,大約可以訂出一個初步的 LINE Webhook data Model。首先,建立一個全新的 Model,命名為「LineWebhookModels.cs」
Event Object
根據官方說明,傳進來的資料由數個 event 組成,所以資料表示成 {events:[ ... ]} ,其中又分為 message, follow, unfollow, join, leave, postback, beacon 7種 Event。
namespace LINE_Webhook.Models { public enum EventType { message, follow, unfollow, join, leave, postback, beacon } public class LineWebhookModels { public List<Event> events { get; set; } } public class Event { public EventType type { get; set; } public string timestamp { get; set; } } }
其中 LineWebhookModels Model 唯一整包 LINE 傳入的資料,我們使用
Source Object
在每個 event 中一定會包含一個 Source Object,來表示該 event 是由誰發出,其中又分成 user, group, room 3種 Source 來源,將 Event Model 改成這樣。
public enum SourceType { user, group, room } public class Event { public EventType type { get; set; } public string timestamp { get; set; } public Source source { get; set; } } public class Source { public SourceType type { get; set; } public string userId { get; set; } public string groupId { get; set; } public string roomId { get; set; } }
Message Object
接下來就進入重頭戲,取得訊息內容。當 event 為 message type 時,就會多出 message 與 replyToken 兩個成員,其中 message 又分成 text, image, video, audio, location, sticker 6個種類。最後將 Event Model 改成這樣。
public enum MessageType { text, image, video, audio, location, sticker } public class Event { public EventType type { get; set; } public string timestamp { get; set; } public Source source { get; set; } public string replyToken { get; set; } public Message message { get; set; } } public class Message { public string id { get; set; } public MessageType type { get; set; } public string text { get; set; } public string title { get; set; } public string address { get; set; } public decimal latitude { get; set; } public decimal longitude { get; set; } public string packageId { get; set; } public string stickerId { get; set; } }
大功告成!這樣一來,無論是哪一種類型的訊息,透過
建立 Model 並非唯一的方式,您也可以使用 dynamic 的型態來接收從 LINE 傳遞過來的資料。
將用戶的訊息印出來
有了
public IHttpActionResult webhook([FromBody] LineWebhookModels data) { if (data == null) return BadRequest(); if (data.events == null) return BadRequest(); foreach (Event e in data.events) { if (e.type == EventType.message) { string senderID = ""; switch (e.source.type) { case SourceType.user: senderID = e.source.userId; break; case SourceType.room: senderID = e.source.roomId; break; case SourceType.group: senderID = e.source.groupId; break; } Trace.WriteLine("傳遞者ID " + senderID); Trace.WriteLine("內容 " + e.message.text); } } return Ok(); }
除了文字訊息之外,其他擁有照片、影片或檔案等媒體類型的 message 若要取得原始資源,則要透過 LINE Get content API 額外取得,細節可以參考官方文件。
回覆用戶
能夠接收用戶訊息後,最重要的就是回覆用戶訊息了!為了介紹,這邊會以「回音」的功能進行說明 (用戶說什麼就回什麼)。首先,您必須從帳戶主控台中取得
<appSettings> <add key="AccessToken" value="Jk0lRN...."/> </appSettings>
根據官方文件的 Reply message API 中所述,透過 HTTP POST 方法,並設定相對應 Header 與 Body 資訊後,就可以對用戶發送的訊息進行回覆。這裡採用 .NET 內建的 WebRequest 就可以輕鬆辦到:
WebRequest req = WebRequest.Create("https://api.line.me/v2/bot/message/reply"); req.Method = "POST"; req.ContentType = "application/json"; req.Headers["Authorization"] = "Bearer " + WebConfigurationManager.AppSettings["AccessToken"]; WebResponse response = req.GetResponse(); using (var streamReader = new StreamReader(response.GetResponseStream())) { string result = streamReader.ReadToEnd(); Trace.WriteLine(result); }
先等等,還沒加上要回覆的資料,依照官方說明,送出 Reply data大概長這樣,其中 messages 最多5則,而且跟剛才介紹的 Message Object 是一樣的,可以是不同類型的回覆:
{ "replyToken":"nHuyWiB7yP5Zw52FIkcQobQuGDXCTA", "messages":[ { "type":"text", "text":"Hello, user" } ] }
我們可以透過 Model 的機制來規範這個 Reply data,而且剛才規範的
分為 ReceiveMessage 與 SendMessage
透過繼承的方式,抽離 Message 相同的部分,留下 type 提供繼承時才實作類別,這樣就可以保持原先的
public enum MessageType { text, image, video, audio, location, sticker } public abstract class Message<T> { public string id { get; set; } public T type { get; set; } public string text { get; set; } public string title { get; set; } public string address { get; set; } public decimal latitude { get; set; } public decimal longitude { get; set; } public string packageId { get; set; } public string stickerId { get; set; } } public class ReceiveMessage : Message<MessageType> { } public class SendMessage : Message<string> { }
Event Model 也改成這樣:
public class Event { public EventType type { get; set; } public string timestamp { get; set; } public Source source { get; set; } public string replyToken { get; set; } public ReceiveMessage message { get; set; } }
ReplyBody Model
將 Message 分開後,Reply data 就可以獨立成一個 Model:
public class ReplyBody { public string replyToken { get; set; } public List<SendMessage> messages { get; set; } }
將 Reply 獨立成一個類別
有了ReplyBody Model 後,透過 Newtonsoft.Json 就可以將資料轉換成 JOSN 的格式。此外,回覆訊息是一個可以被重複使用 (reuse) 的功能,將它獨立成一個類別有助於讓架構更好。
public class Reply { public const string API_URL = "https://api.line.me/v2/bot/message/reply"; private WebRequest req; public Reply(ReplyBody body) { //--- set header and body required infos --- req = WebRequest.Create(API_URL); req.Method = "POST"; req.ContentType = "application/json"; req.Headers["Authorization"] = "Bearer " + WebConfigurationManager.AppSettings["AccessToken"]; // --- format to json and add to request body --- using (var streamWriter = new StreamWriter(req.GetRequestStream())) { string data = JsonConvert.SerializeObject(body); streamWriter.Write(data); streamWriter.Flush(); } } /* --- send message to LINE --- return response data */ public string send() { string result = null; try { WebResponse response = req.GetResponse(); using (var streamReader = new StreamReader(response.GetResponseStream())) { result = streamReader.ReadToEnd(); } } catch (WebException ex) { Trace.WriteLine(ex.ToString()); } return result; } }
這裡將Reply單獨抽離不是最佳的做法,您可以依照實際狀況將 WebRequest 等功能做更高階的抽象化,以利後續加入其他更多的 API。
實作「回音」功能
有了
public IHttpActionResult webhook([FromBody] LineWebhookModels data) { if (data == null) return BadRequest(); if (data.events == null) return BadRequest(); foreach (Event e in data.events) { if (e.type == EventType.message) { ReplyBody rb = new ReplyBody() { replyToken = e.replyToken, messages = procMessage(e.message) }; Reply reply = new Reply(rb); reply.send(); } } return Ok(data); }
透過
private List<SendMessage> procMessage(ReceiveMessage m) { List<SendMessage> msgs = new List<SendMessage>(); SendMessage sm = new SendMessage() { type = Enum.GetName(typeof(MessageType), m.type) }; switch (m.type) { case MessageType.sticker: sm.packageId = m.packageId; sm.stickerId = m.stickerId; break; case MessageType.text: sm.text = m.text; break; default: sm.type = Enum.GetName(typeof(MessageType), MessageType.text); sm.text = "很抱歉,我只是一隻回音機器人,目前只能回覆基本貼圖與文字訊息喔!"; break; } msgs.Add(sm); return msgs; }
雖然 API 看似可以回覆與用戶相同的貼圖,但實際上 API 僅能回覆內建的貼圖,您可以參考這個官方釋出的 Sticker List 來確認那些是內建貼圖。
現在大功告成,試驗一下是否能跟回音機器人講話了:
Security Issue
隨然 LINE 已經強迫要求使用 https 進行資料傳輸,但不代表 webhook server 不會被 CSRF 攻擊,因此 LINE 在傳遞給 webhook 資料時會在 Header 夾帶一個
取得 Channel Secret
為了進行加密計算,從應用程式主控台中將 Channel Secret 取出並暫時寫到
<appSettings> <add key="AccessToken" value="c..."/> <add key="ChannelSecret" value="3d..."/> </appSettings>
Verify the Signature
透過官方所示之演算法,進行 Signature 驗證實作。這邊借用 .NET Web API 的 Authentication and Authorization 機制來實作。保持原來的 Web API Lifecycle,複寫 AuthorizeAttribute 定義客製化驗證是最佳的方式:
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(WebConfigurationManager.AppSettings["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; } }
其中,複寫 OnAuthorization,這個方法會在進入 Controller 之前被觸發,藉由參數HttpActionContext可以從原始的 Request data 取出必要資料進行驗證。
由 Request.Headers 中取出X-Line-Signature(由LINE所發的簽名),再由 Request.Content 取出 Request body 資料,透過 .NET 內建的 HMACSHA256 物件進行加密運算。
如果加密運算後與LINE所發的簽名一致則表示該訊息為 LINE 官方所發出,否則回應 InternalServerError (500 Error)。
加上驗證屬性 AuthorizeAttribute
有了客製化的 Signature AuthorizeAttribute 後,就可以在 LineController 的 Action 中附加,表示該 Controller 必須先通過驗證,通過後才會進入該 Controller:
[HttpPost] [Route] [Signature] public IHttpActionResult webhook([FromBody] LineWebhookModels data) { .... return Ok(data); }
做成 AuthorizeAttribute 的好處是,未來若擴充更多 LINE 相關的 API 時,直接在 Controller 中加入該 Attribute 即可。
結論
以上就是使用 .NET Web API 建立 LINE webhook 程式的介紹,希望對需要使用 C# 語言開發的朋友會有幫助。畢竟無論是官方或是網路上對於 C# 怎麼建立 LINE webhook 的詳細說明實在太少,因此在此做小小的貢獻。
GitHub
這次整個介紹的詳細的程式碼可以在我的 GitHub 上找到,有任何問題也歡迎回饋給我。謝謝。
Refreence
- LINE API Reference
- C# Custom Attributes
- Microsoft ASP.NET Web API
- C#Enum
- ASP.NET WEB API Custom Authorize
- HMACSHA256
感謝分享這麼好的範例解說
回覆刪除您好,謝謝您的分享
回覆刪除我用你的例子發生(403) Forbidden
請問這是沒有權限使用line api嗎?
我的權限是REPLY_MESSAGE
PUSH_MESSAGE
是否可以給予指點,謝謝
感謝分享 : )
回覆刪除感謝分享
回覆刪除想問一下我在單機localhost 就收不到HTTP Post的消息了是..........?
回覆刪除非常感謝你的分享.
回覆刪除非常感謝您的分享,您的步驟講解非常的仔細。
回覆刪除雖然因為我個人對MVC的架構不熟悉,中間遇到一點問題。
但是解決了!現在可以成功的運行。
請問,要怎麼知道 line request 傳甚麼樣的 json 資料進來?line 文件沒有查到 @@
回覆刪除回覆的replyToken 要怎麼產生
回覆刪除