第12 屆iT邦幫忙鐵人賽系列文章 (Day4)

本篇開始我們要來逐步實踐我們的婚禮 Chatbot 了!

我們預期在加入好友的時候 (OnFollow) 事件時回傳歡迎詞:Hi UserName, 感謝您加入 Kyle 的婚禮小助理!

取得使用者的資訊

記得上一篇講 Webhook Event 時的結構嗎? 裡面有個 userId

查看文件後,可以透過 access token 和 userId 來取得用戶資訊

建立一個 LineProfileUtility.cs 取得使用者資訊

我們建立一個 Class 來實作取得使用者相關程式,讓未來不同的情境可以調用,以下是文件取得使用者範例的Sample

    curl -v -X GET https://api.line.me/v2/bot/profile/{userId} \
    -H 'Authorization: Bearer {channel access token}'

有個小技巧可以把 curl 轉成 C# 的 HttpClinet ,將以上的資訊貼到 https://curl.olsh.me/ 即可轉換完成,再做一些微調就好了

轉換後如下:

using (var httpClient = new HttpClient())
{
    using (var request = new HttpRequestMessage(new HttpMethod("GET"), "[https://api.line.me/v2/bot/profile/{userId](https://api.line.me/v2/bot/profile/{userId)}"))
    {
        request.Headers.TryAddWithoutValidation("Authorization", "Bearer {channel access token}");

var response = await httpClient.SendAsync(request);
    }
}

access token 在幾天前的文章,我們已經定義在 appsetting.json 了,在建構的時候我們注入進來

新增一個 GetUserProfile function,實作如下

public async Task<UserProfile> GetUserProfile(string userId)
{
    using (var httpClient = new HttpClient())
    {
    using (var request = new HttpRequestMessage(new HttpMethod("GET"), $"{lineMessageApiBaseUrl}/{userId}"))
    {
    request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {accessToken}");
    var response = await httpClient.SendAsync(request);
    if(response.StatusCode != HttpStatusCode.OK)
    {
    // 這邊未來應該要 log 起來
    throw new Exception("get_profile_error");
    }
    var result = await response.Content.ReadAsStringAsync();
    return JsonConvert.DeserializeObject<UserProfile>(result);
    }
    }
}

UserProfile.cs 定義如下

public class UserProfile
{
    public string userId { get; set; }
    public string displayName { get; set; }
    public string pictureUrl { get; set; }
    public string statusMessage { get; set; }
}

JSON.NET 轉換

在 .NET Core 3.1 其實有內建 System.Text.Json 來做JSON的序列化跟反序列化處理,但個人還是習慣用 JSON.NET

JsonConvert.DeserializeObject<UserProfile>(result)

您可在專案以下打開 Nuget 套件管理

安裝 JSON.NET 回來

在 OnFollow 事件取得使用者資訊

建立一個 LineReplyMessageUtility.cs 來回覆訊息

我們建立一個 Class 來實作回覆使用者相關程式,以下是官方文件回覆使用者範例的Sample

curl -v -X POST https://api.line.me/v2/bot/message/reply \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {channel access token}' \
-d '{
    "replyToken":"nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
    "messages":[
        {
            "type":"text",
            "text":"Hello, user"
        },
        {
            "type":"text",
            "text":"May I help you?"
        }
    ]
}'

https://curl.olsh.me/ 轉成 C#

using (var httpClient = new HttpClient())
{
    using (var request = new HttpRequestMessage(new HttpMethod("POST"), "https://api.line.me/v2/bot/message/reply"))
    {
        request.Headers.TryAddWithoutValidation("Authorization", "Bearer {channel access token}"); 

        request.Content = new StringContent("{\n    \"replyToken\":\"nHuyWiB7yP5Zw52FIkcQobQuGDXCTA\",\n    \"messages\":[\n        {\n            \"type\":\"text\",\n            \"text\":\"Hello, user\"\n        },\n        {\n            \"type\":\"text\",\n            \"text\":\"May I help you?\"\n        }\n    ]\n}");
        request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); 

        var response = await httpClient.SendAsync(request);
    }
}

做點調整:將 StringContent 變成class,並用 JSON.NET序列化,完整實作如下:

private readonly string accessToken;
private static string lineMessageApiBaseUrl = "[https://api.line.me/v2/bot/message/reply](https://api.line.me/v2/bot/message/reply)";

public LineReplyMessageUtility(IOptions<LineSetting> lineSetting)
{
    accessToken = lineSetting.Value.ChannelAccessToken;
}

public async Task ReplyMessageAsync(string replyToken, params string[] messages)
{
    using (var httpClient = new HttpClient())
    {
    using (var request = new HttpRequestMessage(new HttpMethod("POST"), $"{lineMessageApiBaseUrl}"))
    {
    request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {accessToken}");

LineMessageReq req = new LineMessageReq();
    req.ReplyToken = replyToken;

foreach (var message in messages)
    {
    req.Messages.Add(new TextMessage()
    {
        Text = message
    });
    }

var postJson = JsonConvert.SerializeObject(req, new JsonSerializerSettings
    {
    ContractResolver = new DefaultContractResolver
    {
        NamingStrategy = new CamelCaseNamingStrategy() //轉小寫
    },
    Formatting = Formatting.Indented
    });

request.Content = new StringContent(postJson);
    request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
    var response = await httpClient.SendAsync(request);
    }
    }
}

LineMessageReq 這邊定義一個 IMessage 的介面,之後不同的訊息類型,將會以此來繼承

    public class LineMessageReq

    {

    public string ReplyToken { get; set; }

    public List<IMessage> Messages { get; set; } = new List<IMessage>();

    }

TextMessage Class

public class TextMessage : IMessage
{

    public string Text { get; set; }

    public LineMessageType Type => LineMessageType.text;

}

於 OnFollowAsync 加入 Reply

protected virtual async Task OnFollowAsync(Event ev)
{
    // 取得使用者的資訊
    var user = await lineProfileUtility.GetUserProfile(ev.source.userId);
    // 回傳歡迎詞
    await lineMessageUtility.ReplyMessageAsync(ev.replyToken, $@"Hi {user.displayName}, 感謝您加入婚禮小助理!")
}

如何測試加入好友事件呢? 其實只要封鎖再解除封鎖就好了

實作效果

實作效果

懶人包,本次學到了什麼?