iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

2

今天是來補坑的,要介紹的是 .Net Core 3.0 串接 Line Bot Api。

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

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 + BotFramework6

尚未有邦友留言

立即登入留言