iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

3

今天是來補坑的。 (́◕◞౪◟◕‵)*

要介紹如何使用 .Net Core 3.0 建立 Line Bot 聊天機器人。

https://developers.line.biz/en/docs/messaging-api/line-bot-sdk/

看了官方的 SDK 後覺得有點傷心,怎麼沒有 C# 呢

不過還好有社群維護的,我使用 「LineMessagingApi」 這個,「MIT」 授權,可以用 Nuget 安裝。

https://ithelp.ithome.com.tw/upload/images/20191120/20106865R99ppeScGc.jpg

網址: https://github.com/pierre3/LineMessagingApi

接下來就直接進入程式的部分。

新增 .NET Core 專案

開啟 VS 新增空白的 .NET Core 網站,配置如下。

https://ithelp.ithome.com.tw/upload/images/20191120/20106865UBLHQiKzsj.jpg

需要安裝的套件(Nuget)

  • Newtonsoft.Json
  • Line.Messaging

接著開啟 appsettings.json 新增 line bot 的channelSecretaccessToken,因為這兩個為隱密資訊所以不寫死在程式碼內。

"LineBot": {
    "channelSecret": "xxx",
    "accessToken": "xxx"
  }

新增一個 LineBotConfig 類別。

public class LineBotConfig
{
    public string channelSecret { get; set; }
    public string accessToken { get; set; }
}

開啟 Startup 並將 LineBotConfig 註冊到 DI,在 Controller 內就可以透過建構式取得這兩個參數使用。

services.AddSingleton<LineBotConfig, LineBotConfig>((s) => new LineBotConfig
{
    channelSecret = Configuration["LineBot:channelSecret"],
    accessToken = Configuration["LineBot:accessToken"]
});

新增 LineBotController 這是 api 的接口,使用者傳訊息給機器人後,Line 會透過這個網址將訊息傳給我們進行後續處理,程式部屬完成後需要到 line developers 將 Webhook URL 改為這個網址。

[Route("api/linebot")]
public class LineBotController : Controller
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly HttpContext _httpContext;
    private readonly LineBotConfig _lineBotConfig;

    public LineBotController(IServiceProvider serviceProvider,
        LineBotConfig lineBotConfig)
    {
        _httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
        _httpContext = _httpContextAccessor.HttpContext;
        _lineBotConfig = lineBotConfig;
    }

    //完整的路由網址就是 https://xxx/api/linebot/run
    [HttpPost("run")]
    public async Task<IActionResult> Post()
    {
        return Ok();
    }
}

調整一下 Startup 內的路由

//ConfigureServices 新增
services.AddHttpContextAccessor();
services.AddRazorPages();

//Configure 新增
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "api",
        pattern: "api/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapRazorPages();
});

完整的 Startup

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<LineBotConfig, LineBotConfig>((s) => new LineBotConfig
        {
            channelSecret = Configuration["LineBot:channelSecret"],
            accessToken = Configuration["LineBot:accessToken"]
        });

        services.AddHttpContextAccessor();
        services.AddRazorPages();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "api",
                pattern: "api/{controller=Home}/{action=Index}/{id?}");
            endpoints.MapRazorPages();
        });
    }
}

程式的部分 LineMessagingApi 有範例,不過不是 .Net Core 版本的所以要做一些修改。

參考資料: https://github.com/pierre3/LineMessagingApi/blob/master/WebAppSample/Controllers/LineBotController.cs

[HttpPost("run")]
public async Task<IActionResult> Post()
{
    try
    {
        var events = await _httpContext.Request.GetWebhookEventsAsync(_lineBotConfig.channelSecret);
        var lineMessagingClient = new LineMessagingClient(_lineBotConfig.accessToken);
        var lineBotApp = new LineBotApp(lineMessagingClient);
        await lineBotApp.RunAsync(events);
    }
    catch (Exception ex)
    {
        //需要 Log 可自行加入
        //_logger.LogError(JsonConvert.SerializeObject(ex));
    }
    return Ok();
}

發現原範例中的 GetWebhookEventsAsync 已經不能使用,所以另外寫了一個。

參考資料: https://github.com/pierre3/LineMessagingApi/blob/c2461f89be28d2718730b919cf9a3b78153a9933/Line.Messaging/Webhooks/WebhookRequestMessageHelper.cs

新增 WebhookRequestMessageHelper.cs

/*
MIT License

Copyright (c) 2017 pierre3

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

public static class WebhookRequestMessageHelper
{
    public static async Task<IEnumerable<WebhookEvent>> GetWebhookEventsAsync(this HttpRequest request, string channelSecret, string botUserId = null)
    {
        if (request == null) { throw new ArgumentNullException(nameof(request)); }
        if (channelSecret == null) { throw new ArgumentNullException(nameof(channelSecret)); }

        var content = "";
        using (var reader = new StreamReader(request.Body))
        {
            content = await reader.ReadToEndAsync();
        }

        var xLineSignature = request.Headers["X-Line-Signature"].ToString();
        if (string.IsNullOrEmpty(xLineSignature) || !VerifySignature(channelSecret, xLineSignature, content))
        {
            throw new InvalidSignatureException("Signature validation faild.");
        }

        dynamic json = JsonConvert.DeserializeObject(content);

        if (!string.IsNullOrEmpty(botUserId))
        {
            if (botUserId != (string)json.destination)
            {
                throw new UserIdMismatchException("Bot user ID does not match.");
            }
        }
        return WebhookEventParser.ParseEvents(json.events);
    }

    internal static bool VerifySignature(string channelSecret, string xLineSignature, string requestBody)
    {
        try
        {
            var key = Encoding.UTF8.GetBytes(channelSecret);
            var body = Encoding.UTF8.GetBytes(requestBody);

            using (HMACSHA256 hmac = new HMACSHA256(key))
            {
                var hash = hmac.ComputeHash(body, 0, body.Length);
                var xLineBytes = Convert.FromBase64String(xLineSignature);
                return SlowEquals(xLineBytes, hash);
            }
        }
        catch
        {
            return false;
        }
    }

    private static bool SlowEquals(byte[] a, byte[] b)
    {
        uint diff = (uint)a.Length ^ (uint)b.Length;
        for (int i = 0; i < a.Length && i < b.Length; i++)
            diff |= (uint)(a[i] ^ b[i]);
        return diff == 0;
    }
}

接著看到 LineBotApp 這是主要撰寫程式邏輯的地方,範例很長我精簡一下,只取文字訊息相關的部分,主要會用到下列這兩個函數。

  • OnMessageAsync: 接收使用者訊息。
  • ReplyMessageAsync: 傳訊息給使用者。

channelId 和 userId 的差異

假設機器人被拉入某群組內,channelId 為群組的 Id,userId 為使用者 Id,如果使用者直接傳訊息給機器人,則 channelId 等於 userId。

public class LineBotApp : WebhookApplication
{
    private readonly LineMessagingClient _messagingClient;
    public LineBotApp(LineMessagingClient lineMessagingClient)
    {
        _messagingClient = lineMessagingClient;
    }

    protected override async Task OnMessageAsync(MessageEvent ev)
    {
        var result = null as List<ISendMessage>;

        switch (ev.Message)
        {
            //文字訊息
            case TextEventMessage textMessage:
                {
                    //頻道Id
                    var channelId = ev.Source.Id;
                    //使用者Id
                    var userId = ev.Source.UserId;
                    
                    //回傳 hellow
                    result = new List<ISendMessage>
                    {
                        new TextMessage("hellow")
                    };
                }
                break;
        }

        if (result != null)
            await _messagingClient.ReplyMessageAsync(ev.ReplyToken, result);
    }
}

到這裡就完成了基本的程式架構,機器人收到訊息後會回傳 hellow 給使用者。

結語

下一篇會介紹如何將程式部屬到 Azure 上,部屬後才能測試,今天就到這裡,感謝大家觀看。

慢慢補坑中~


上一篇
[Day01] LINE Bot 帳號申請
下一篇
[Day03] 將 Line Bot 部屬到 Azure 上 - 使用 Azure DevOps Pipelines
系列文
Line Bot 心得分享 LineMessagingApi + LUIS + BotFramework27

尚未有邦友留言

立即登入留言