Line.Messaging 1.4.1 でQuick ReplyとFlex Messageに対応しました

Line.Messaging v1.4.1 をリリースしました

www.nuget.org

Quick Replyと、(長らくお待たせしておりましたがようやく)Flex Message に対応しました!

各機能の詳細は以下のドキュメントをご確認ください。

Quick Reply の使い方

Quick Replyの使い方は簡単で、各種送信メッセージに追加されたQuick ReplyプロパティにQuickReplyオブジェクトを設定してあげるだけです。

new TextMessage("text", new QuickReply()
{
    Items = new[] 
    {
        new QuickReplyButtonObject(new CameraTemplateAction("カメラ起動")),
        new QuickReplyButtonObject(new CameraRollTemplateAction("カメラロール")),
        new QuickReplyButtonObject(new LocationTemplateAction("位置を指定")),
    }
});

QuickReply ので表示するボタンは、QuickReplyButtonObjectItemsプロパティに配列で設定します。
QuickReplyButtonObject には、ボタンをタップした際の動作をActionプロパティで指定します。 Actionには、これまでテンプレートメッセージ等で使用していたITemplateActionインターフェースを実装するクラスに加え、今回追加されたカメラや、カメラロールを起動するアクションが使用できます。

  • MessageTemplateAction
  • UriTemplateAction
  • PostbackTemplateAction
  • LocationTemplateAction
  • DateTimePickerTemplateAction
  • CameraTemplateAction (New!)
  • CameraRollTemplateAction (New!)

Flex Message の使い方

Flex Messageの実装例を以下に書きましたので、こちらを参考に試してみてください。

LineMessagingApi/FlexMessageSampleApp.cs at master · pierre3/LineMessagingApi · GitHub

なお、ライブラリでは、以下の3種類の実装方法をサポートしています。

1. Flex Message のJSON文字列をそのまま利用する

LineMessagingClientに追加された以下のメソッドを 利用することでJSON文字列を直接指定してメッセージを送れるようになりました。

  • ReplyMessageWithJsonAsync
  • PushMessageWithJsonAsync
  • MulticastMessageWithJsonAsync

この方法は、LINE Developers で公開されている Flex Message Simulatorで生成したJSONをコピーして利用する場合に便利です。

private static readonly string FlexJson =
@"{
  ""type"": ""flex"",
  ""contents"": {
    ""type"": ""bubble"",
    ""direction"": ""ltr"",
    ""hero"": {
      ""type"": ""image"",
      ""url"": ""https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_1_cafe.png"",
      ""size"": ""full"",
      ""aspectRatio"": ""20:13"",
      ""aspectMode"": "
...(省略)
};
private static readonly string TextMessageJson = "{ \"type\" : \"text\", \"text\" : \"I Sent a flex message with json string.\" }";

private async Task ReplyFlexWithJson(MessageEvent ev)
{
    await MessagingClient.ReplyMessageWithJsonAsync(ev.ReplyToken, FlexJson, TextMessageJson);
}

2. オブジェクト初期化子を使ってFlex Messageのオブジェクトを組み立てる

以下の様に、Flexの各オブジェクトのプロパティ値をオブジェクト初期化子内で設定してオブジェクトを組み立てます。
この方法で実装した場合、コードの構造が生成されるJSONと一致するため、JSONを見慣れている人には結果がイメージしやすいかもしれません。

private async Task ReplyFlexWithObjectInitializer(MessageEvent ev)
{

    var restrant = CreateRestrantWithObjectInitializer();
    var news = CreateNewsWithExtensions();
    var receipt = CreateReceiptWithExtensions();

    //バブルメッセージ
    var bubble = new FlexMessage("Bubble Message")
    {
        Contents = restrant
    };

    //カルーセルメッセージ
    var carousel = new FlexMessage("Carousel Message")
    {
        Contents = new CarouselContainer()
        {
            Contents = new BubbleContainer[]
            {
                restrant,
                news,
                receipt
            }
        },
        QuickReply = new QuickReply(new[]
        {
            new QuickReplyButtonObject(new CameraRollTemplateAction("CameraRoll")),
            new QuickReplyButtonObject(new CameraTemplateAction("Camera")),
            new QuickReplyButtonObject(new LocationTemplateAction("Location"))
        })
    };

    await MessagingClient.ReplyMessageAsync(ev.ReplyToken, new FlexMessage[] { bubble, carousel });
}

//バブルコンテナの作成
private static BubbleContainer CreateRestrantWithObjectInitializer()
    {
        return new BubbleContainer()
        {
            Hero = new ImageComponent()
            {
                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_1_cafe.png",
                Size = ComponentSize.Full,
                AspectRatio = AspectRatio._20_13,
                AspectMode = AspectMode.Cover,
                Action = new UriTemplateAction(null, "http://linecorp.com/")
            },
            Body = new BoxComponent()
            {
                Layout = BoxLayout.Vertical,
                Contents = new IFlexComponent[]
                {
                    new TextComponent()
                    {
                        Text = "Broun Cafe",
                        Weight = Weight.Bold,
                        Size = ComponentSize.Xl
                    },
                    new BoxComponent()
                    {
                        Layout = BoxLayout.Baseline,
                        Margin = Spacing.Md,
                        Contents = new IFlexComponent[]
                        {
                            new IconComponent()
                            {
                                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                Size = ComponentSize.Sm
                            },
                            new IconComponent()
                            {
                                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                Size = ComponentSize.Sm
                            },
                            new IconComponent()
                            {
                                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                Size = ComponentSize.Sm
                            },
                            new IconComponent()
                            {
                                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                Size = ComponentSize.Sm
                            },
                            new IconComponent()
                            {
                                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png",
                                Size = ComponentSize.Sm
                            },
                            new TextComponent()
                            {
                                Text = "4.0",
                                Size = ComponentSize.Sm,
                                Margin = Spacing.Md,
                                Flex = 0,
                                Color = "#999999"
                            }
                        }
                    },
                    new BoxComponent()
                    {
                        Layout = BoxLayout.Vertical,
                        Margin = Spacing.Lg,
                        Spacing = Spacing.Sm,
                        Contents = new IFlexComponent[]
                        {
                            new BoxComponent()
                            {
                                Layout = BoxLayout.Baseline,
                                Spacing = Spacing.Sm,
                                Contents = new IFlexComponent[]
                                {
                                    new TextComponent()
                                    {
                                        Text = "Place",
                                        Size = ComponentSize.Sm,
                                        Color = "#aaaaaa",
                                        Flex = 1
                                    },
                                    new TextComponent()
                                    {
                                        Text = "Miraina Tower, 4-1-6 Shinjuku, Tokyo",
                                        Size = ComponentSize.Sm,
                                        Wrap = true,
                                        Color = "#666666",
                                        Flex = 5
                                    }
                                }
                            }
                        }
                    },
                    new BoxComponent(BoxLayout.Baseline)
                    {
                        Spacing = Spacing.Sm,
                        Contents = new IFlexComponent[]
                        {
                            new TextComponent()
                            {
                                Text = "Time",
                                Size = ComponentSize.Sm,
                                Color = "#aaaaaa",
                                Flex = 1
                            },
                            new TextComponent()
                            {
                                Text = "10:00 - 23:00",
                                Size = ComponentSize.Sm,
                                Wrap = true,
                                Color = "#666666",
                                Flex=5
                            }
                        }
                    }
                }
            },
            Footer = new BoxComponent()
            {
                Layout = BoxLayout.Vertical,
                Spacing = Spacing.Sm,
                Flex = 0,
                Contents = new IFlexComponent[]
                    {
                        new ButtonComponent()
                        {
                            Action = new UriTemplateAction("Call", "https://linecorp.com"),
                            Style = ButtonStyle.Link,
                            Height = ButtonHeight.Sm
                        },
                        new ButtonComponent()
                        {
                            Action = new UriTemplateAction("WEBSITE", "https://linecorp.com"),
                            Style = ButtonStyle.Link,
                            Height = ButtonHeight.Sm
                        },
                        new SpacerComponent()
                        {
                            Size = ComponentSize.Sm
                        }
                    }
            },
            Styles = new BubbleStyles()
            {
                Body = new BlockStyle()
                {
                    BackgroundColor = ColorCode.FromRgb(192, 200, 200),
                    Separator = true,
                    SeparatorColor = ColorCode.DarkViolet
                },
                Footer = new BlockStyle()
                {
                    BackgroundColor = ColorCode.Ivory
                }
            }
        };
    }

}

3. ヘルパーメソッドを使ってメソッドチェーンでFlex Messageオブジェクトを生成す

BubbleContainerクラスやBoxComponentクラスに付加された拡張メソッドを利用することで、以下の様にメソッドチェーンで記述することも可能です。
こちらの方が多少すっきりと記述できると思います。

private async Task ReplyFlexWithExtensions(MessageEvent ev)
{
    var restrant = CreateRestrantWithObjectInitializer();
    var news = CreateNewsWithExtensions();
    var receipt = CreateReceiptWithExtensions();

    //バブルメッセージ
    var bubble = FlexMessage.CreateBubbleMessage("Bubble Message")
        .SetBubbleContainer(restrant);

    //カルーセルメッセージ
    var carousel = FlexMessage.CreateCarouselMessage("Carousel Message")
        .AddBubbleContainer(restrant)
        .AddBubbleContainer(news)
        .AddBubbleContainer(receipt)
        .SetQuickReply(new QuickReply(new[]
        {
            new QuickReplyButtonObject(new CameraTemplateAction("Camera")),
            new QuickReplyButtonObject(new LocationTemplateAction("Location"))
        }));

    await MessagingClient.ReplyMessageAsync(ev.ReplyToken, new FlexMessage[] { bubble, carousel });
}

//バブルコンテナの生成
private static BubbleContainer CreateNewsWithExtensions()
{
    return new BubbleContainer()
        .SetHeader(BoxLayout.Horizontal)
            .AddHeaderContents(new TextComponent("NEWS DIGEST") { Weight = Weight.Bold, Color = "#aaaaaa", Size = ComponentSize.Sm })
        .SetHero(imageUrl: "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_4_news.png",
                size: ComponentSize.Full, aspectRatio: AspectRatio._20_13, aspectMode: AspectMode.Cover)
            .SetHeroAction(new UriTemplateAction(null, "http://linecorp.com/"))
        .SetBody(boxLayout: BoxLayout.Horizontal, spacing: Spacing.Md)
            .AddBodyContents(new BoxComponent(BoxLayout.Vertical) { Flex = 1 }
                .AddContents(new ImageComponent("https://scdn.line-apps.com/n/channel_devcenter/img/fx/02_1_news_thumbnail_1.png")
                { AspectMode = AspectMode.Cover, AspectRatio = AspectRatio._4_3, Size = ComponentSize.Sm, Gravity = Gravity.Bottom })
                .AddContents(new ImageComponent("https://scdn.line-apps.com/n/channel_devcenter/img/fx/02_1_news_thumbnail_2.png")
                { AspectMode = AspectMode.Cover, AspectRatio = AspectRatio._4_3, Size = ComponentSize.Sm, Gravity = Gravity.Bottom }))
            .AddBodyContents(new BoxComponent(BoxLayout.Vertical) { Flex = 2 }
                .AddContents(new TextComponent("7 Things to Know for Today") { Gravity = Gravity.Top, Size = ComponentSize.Xs, Flex = 1 })
                .AddContents(new SeparatorComponent())
                .AddContents(new TextComponent("Hay fever goes wild") { Gravity = Gravity.Center, Size = ComponentSize.Xs, Flex = 2 })
                .AddContents(new SeparatorComponent())
                .AddContents(new TextComponent("LINE Pay Begins Barcode Payment Service") { Gravity = Gravity.Center, Size = ComponentSize.Xs, Flex = 2 })
                .AddContents(new SeparatorComponent())
                .AddContents(new TextComponent("LINE Adds LINE Wallet") { Gravity = Gravity.Bottom, Size = ComponentSize.Xs, Flex = 1 }))
        .SetFooter(BoxLayout.Horizontal)
            .AddFooterContents(new ButtonComponent() { Action = new UriTemplateAction("More", "https://linecorp.com") });
}

LINE BOT のリッチメニューをXAMLでデザインするツール

LINE でBotを作成する上で欠かせない機能にリッチメニューがあります。

非常に便利で強力な機能なのですが、メニューに使用する画像を用意する必要があります。
絵心の無い私にとっては、ここが一番のハードルだったりします。

ということで

XamlRichMenuMaker

LINE Bot のリッチメニューをXAMLでデザインして、そのままLINEサーバーへアップロードできるツールを作りました!

github.com

WPFプロジェクトをVisual Studioで開いて使用します。

  • リッチメニューのデザインをXAMLで行います。

f:id:pierre3:20180506105716p:plain

デザインが完成したら、プロジェクトをビルドし、アプリケーションを実行すると「リッチメニューの画像」と「アップロード用のJSON」が自動生成されます。
あとは「Create Rich Menu」ボタンをクリックしてLINEサーバーにアップロードするだけ!

f:id:pierre3:20180511232529p:plain:w600

使い方

ソリューションを取得する

まずはXamlRichMenuMaker のソリューション一式を取得します。
GithubからCloneしてくるのが手っ取り早いです。

> git clone https://github.com/pierre3/XamlRichMenuMaker.git

ソリューションを開き、デザイン用のXAMLファイルを作成する

  • Visual Studio 2017 で XamlRichMenuMaker.sln を開きます。
  • ソリューションエクスプローラを確認し、RichMenuDefsフォルダにある__template.xamlをコピーします。

f:id:pierre3:20180506105631p:plain

  • コピーしたファイルを任意の名前にリネームします。
  • ファイル名を右クリックしてプロパティを開き、ビルドアクションを「なし」出力ディレクトリにコピーを「常にコピーする」に設定します。

f:id:pierre3:20180512160437p:plain

  • このファイルを編集してリッチメニューをデザインします。

XAML編集の手順

__template.xamlファイルの中身は以下の通りです。
UserControlクラスを継承したRichMenuDefsControlの中身を編集していきます。

<local:RichMenuDefsControl 
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:system="clr-namespace:System;assembly=mscorlib"
             xmlns:local="clr-namespace:XamlRichMenuMaker;assembly=XamlRichMenuMaker"
             mc:Ignorable="d" 
             d:DesignHeight="1000" d:DesignWidth="2600">
    <UserControl.Resources>
        <system:Double x:Key="RichMenuWidth">2500</system:Double>
        <system:Double x:Key="RichMenuShortHeight">843</system:Double>
        <system:Double x:Key="RichMenuNormalHeight">1686</system:Double>
    </UserControl.Resources>

    <Grid Width="{StaticResource RichMenuWidth}" 
                  Height="{StaticResource RichMenuShortHeight}" >
        <Grid x:Name="menu_body" Background="LightGray">
            <local:RichMenuProperties.Settings>
                <local:RichMenuSettings Name="" ChatBarText="" Selected="True"/>
            </local:RichMenuProperties.Settings>

            <!-- Design your rich menu layout here. -->

        </Grid>
    </Grid>
</local:RichMenuDefsControl>

1. リッチメニューのサイズを指定する

リッチメニューには高さの異なる2種類のサイズがあります。

  • Normal :2500×1686
  • short:2500×843

これらは、テンプレートではリソースとして定義されています。
それを最上位のGridコントロールのWidth、Heightに設定します。

 <UserControl.Resources>
    <system:Double x:Key="RichMenuWidth">2500</system:Double>
    <system:Double x:Key="RichMenuShortHeight">843</system:Double>
    <system:Double x:Key="RichMenuNormalHeight">1686</system:Double>
</UserControl.Resources>

<Grid Width="{StaticResource RichMenuWidth}" 
                  Height="{StaticResource RichMenuShortHeight}" >

</Grid>    

2. リッチメニューの各種設置値を指定する

リッチメニューの画像として保存されるのは、”x:Name=menu_body”とタグ付けされたGridコントロールです。
このGridに対して以下の値を設定します。

  • Grid の Background プロパティにGridの背景色を指定します。
  • メニュー登録時の各種設定を添付プロパティRichMenuProperties.Settings で指定します。

指定する項目には以下があります。

  • Name: リッチメニュー名
  • ChatBarText: LINEのチャット画面下のメニューバーに表示されるテキスト
  • Selected: デフォルトでリッチメニューを表示状態にする場合にTrueを指定
<Grid x:Name="menu_body" Background="LightGray">
        <local:RichMenuProperties.Settings>
            <local:RichMenuSettings 
                Name="My Rich Menu" 
                ChatBarText="MY メニュー" 
                Selected="True"/>
        </local:RichMenuProperties.Settings>

        <!-- Design your rich menu layout here. -->

</Grid>

3.リッチメニューの見た目を自由にデザインする

リッチメニューの外観は Grid ”menu_body" の中にコントロールを配置して行います。
今回の例では、Gridで3つあるボタンの配置を決めて、そこにImageコントロールでアイコン画像を、TextBlockでボタンのキャプションを設定しています。

<!-- ボタンの幅を2:1:1の割合で配置 -->
<Grid.ColumnDefinitions>
    <ColumnDefinition Width="2*"/>
    <ColumnDefinition/>
    <ColumnDefinition/>
</Grid.ColumnDefinitions>

<!-- 1つ目のボタン -->
<Image Grid.Column="0"  Source="../Resources/userinfo.png"
       VerticalAlignment="Top"
       Margin="0,150,0,0" Width="420" Height="420"/>
<TextBlock Grid.Column="0"  Margin="0,0,0,80" VerticalAlignment="Bottom" 
        HorizontalAlignment="Center" 
        FontSize="80">ユーザー情報確認
</TextBlock>

イコン画像は、以下のサイトで公開されているものを使用させていただきました。
icon-rainbow.com

シンプルで使い勝手の良いアイコンを無償で提供してくださっています。アイコンの種類も豊富で、お勧めのサイトです。

4. アクションエリアを定義する

次に、リッチメニューの中でボタンとして機能する領域(=アクションエリア)を定義します。 定義する項目は以下の2つです。

  • Bounds: アクションエリアの座標
  • Action: エリアをタップした際に動作するアクション

Boundsには、x:Name="area_{n}" (nには1~20の番号を指定)でタグ付けしたコントロールの座標をそのまま使用します。 今回は、ボタンの境界の表示も兼ねてRectangle コントロールを使用しています。

<!-- Action Area 1 -->
<Rectangle Grid.Column="0" x:Name="area_1"
                    Stroke="DarkGray" StrokeThickness="4">

</Rectangle>

Actionは 上記コントロールに添付プロパティRichMenuProperties.Actionを指定することで定義します。

<!-- Action Area 1 -->
<Rectangle Grid.Column="0" x:Name="area_1"
                    Stroke="DarkGray" StrokeThickness="4">
    <local:RichMenuProperties.Action>
           <local:RichMenuAction Type="Postback" Label="get user info" Data="API(UserInfo)"  Text="ユーザー情報確認"/>
     </local:RichMenuProperties.Action>
</Rectangle>

Actionには次の4タイプが指定できます。

  • Postback
  • Message
  • URI
  • Datetimepicker

各アクションに必要な設定項目については以下をご参照ください。

これで、XAMLの編集方法の説明は終わりです。 サンプルのXAMLファイルの全体像は以下で確認できます。

リッチメニューを作成・アップロードする

アプリケーションを実行する

実行前にまず、Visual Studioでプロジェクトをビルドします。(これは最初の1回のみで大丈夫です)
次に、メニューで[プロジェクト]-[XamlRichMenuMakerのプロパティ]を選択してプロジェクトのプロパティを開きます。
[デバッグ]-[開始オプション]-[コマンドライン引数]に以下の情報を設定します。

  • 関連付けるBOTのChannel Access Token
  • 作成したリッチメニューを関連付けるLINEアカウントのUser ID(デバッグ用)

f:id:pierre3:20180512162503p:plain:w600

[Ctrl]+[F5]キーを押してアプリケーションを実行します。

リッチメニューを作成する

アプリケーションが起動すると、以下の様にXAMLででサインしたリッチメニューの画像と、Messaging APIでLINE側に送信するJSONの内容が表示されます。

f:id:pierre3:20180511232529p:plain:w600

内容を確認してOKなら「Create Rich Menu」ボタンをクリックします。
これでリッチメニューの作成とアップロードの完了です!

作成・登録済みのリッチメニューを確認する

[Create Rich Menu]ボタンの右側にあるコンボボックスを開くと、XAMLファイルで定義したリッチメニュー名(XAMLファイル名)の下に既に登録済みのリッチメニューID(”richmenu-”で始まる文字列)が表示されます。

f:id:pierre3:20180512164224p:plain

これらを選択することで、リッチメニューの内容(画像、JSON)を確認することができます。

また、選択したリッチメニューに対して以下操作が可能です。

  • [Delete Rich Menu] : リッチメニューを削除します
  • [Link to User] : (コマンドライン引数で指定した)デバッグ用のLINEユーザーに関連付けします
  • [Unlink from User]: デバッグ用のLINEユーザーから関連付けを外します

f:id:pierre3:20180512164934p:plain:w600

まとめ

Visual StudioXAMLを使うので、使用環境は限られてしまいますが、非常に簡単にリッチメニューを作成できるので是非試してみてください!

LINE Messaging APIの新機能「ユーザーアカウント連携」を使ってみるサンプル(C#)

Line.Messaging v1.2.5 をリリースしました!

www.nuget.org

今回のアップデートでは、ユーザーアカウントの連携機能に対応しています。

ユーザーアカウント連携については以下の公式ドキュメントをご確認ください。
https://developers.line.me/ja/docs/messaging-api/linking-accounts

Messaging APIに追加された機能は以下の2つです。

  • アカウント連携イベントの追加

    • WebhookApplicationクラスにOnAccountLinkAsync 仮想メソッドを追加しました
  • アカウント連携用Link Tokenの取得関数

    • LineMessagingClientクラスにIssueLinkTokenAsyncメソッドを追加しました

ユーザーアカウント連携のサンプル

上記公式ドキュメントに記載の連携シーケンスに従って、サンプルアプリを作ってみました。

github.com

サンプルアプリは、以下の2つのアプリケーションで構成されています。

  • Botサーバー(Provider's Bot Server)

    • Azure FunctionでLINE BotのWebhookを処理するボットアプリ
  • 連携するWebサービス(Provider's Web Server)

    • ASP.NET Core MVC のテンプレートを用いたWebアプリ
    • ログインにメールアドレスとパスワードを使用する、テンプレートに組み込みの認証を使用します。 (プロジェクト作成時に「認証の変更」-「個人のユーザアカウント」-「アプリ内のストア ユーザーアカウント」を選択)

f:id:pierre3:20180417221708p:plain:w600

連携手順

サンプルでは、ユーザーからメッセージを受け取った際にアカウント連携の処理を行うようにします。
BOTサーバーのWebhook(BotFunction/LineBotApp.cs)のメッセージイベント(OnMessageAsync)で以下のような実装にしています。

  • アカウント連携済みでない場合
    • 任意のメッセージで「アカウント連携を開始」
    • テキストメッセージ"解除"で「アカウント連携を解除」
  • アカウント連携済みの場合
/// <summary>
/// メッセージイベント
/// </summary>
protected override async Task OnMessageAsync(MessageEvent ev)
{
    switch (ev.Message)
    {
        case TextEventMessage textMessage:

            //Azureストレージに保存しているユーザー情報を取得
            var status = await Status.FindAsync(
                partitionKey: BotStatus.DefaultPartitionKey,
                rowKey: ev.Source.Id);

            //アカウント連携していない
            if (string.IsNullOrEmpty(status?.AccountLinkNonce))
            {
                //連携開始
                await StartAccountLinkAsync(ev);
            }
            //アカウント連携済みの場合
            else
            {
             if (textMessage.Text == "解除")
                {
                    //連係解除
                    await UnlinkAsync(ev, status);
                    return;
                }
                //連携済みのWebサービスのAPIを利用
                await InvokeWebApiAsync(ev, status);
            }
            break;
    }
}

アカウント連携を開始する(BOT サーバー側)

連携開始処理は、BotFunction/LineBotApp.csで以下の様に実装しています。

/// <summary>
/// アカウント連携を開始する
/// </summary>
private async Task StartAccountLinkAsync(MessageEvent ev)
{

    //LINEサーバーからLink Tokenを取得
    var linkToken = Uri.EscapeDataString(await Line.IssueLinkTokenAsync(ev.Source.Id));
    //連携用のリンクをユーザーに返信
    await Line.ReplyMessageAsync(ev.ReplyToken, new[]
    {
        new TemplateMessage("account link",
            new ButtonsTemplate("アカウント連携をします。", null, "LINE Account Link", new[]
            {
                new UriTemplateAction("OK", $"https://lineaccountlinkapp.azurewebsites.net/Account/Link?linkToken={linkToken}")
            }))
        });
}

以降、公式ドキュメントの「アカウント連携を実装する」の記載内容に合わせて実装の流れを追っていきます。

1. 連携トークンを発行する

HTTP POSTリクエストを/bot/user/{userId}/linkTokenエンドポイントに送信して、連携しようとしているユーザー用の連携トークンを発行します。

//LINEサーバーからLink Tokenを取得
var linkToken = Uri.EscapeDataString(await Line.IssueLinkTokenAsync(ev.Source.Id));

2. ユーザーを連携URLにリダイレクトする

ボットからユーザーに、連携URLを開くメッセージを送信します。たとえば、URIアクションを設定したテンプレートメッセージに連携URLを指定します。このとき、ステップ1で取得した連携トークンをURLのクエリパラメータとして指定します。

//連携用のリンクをユーザーに返信
await Line.ReplyMessageAsync(ev.ReplyToken, new[]
{
    new TemplateMessage("account link",
        new ButtonsTemplate("アカウント連携をします。", null, "LINE Account Link", new[]
        {
            new UriTemplateAction("OK", $"https://lineaccountlinkapp.azurewebsites.net/Account/Link?linkToken={linkToken}")
        }))
    });

ユーザーから見ると、以下のようなテンプレートメッセージがリプライされてきます。

f:id:pierre3:20180416232818p:plain:w400

アカウント連携処理(Webサービス側)

ユーザーがテンプレートメッセージの「OK」をタップすると、"(WebサービスのBase URL)/Account/Link”にリダイレクトされます。

Webサービス側の実装は、LineAccountLinkApp/Controllers/AccountController.csLinkLinkEx メソッドになります。

3. 自社サービスのユーザーIDを取得する

ユーザーが連携URLにアクセスしたら、自社サービスへのログイン画面を表示します。ユーザーがサービスにログインすると、自社サービスのユーザーIDを取得できます。

Account/Link アクションでは、ログインページへのリダイレクトのみを行います。その際、ログイン成功後にアカウント連携を実行するアクション(/Account/LinkEx)へリダイレクトするようにreturnUrlパラメータを設定しておきます。
また今回は、ログイン済みのユーザーでも必ず再ログインを求める様にしています。

/// <summary>
/// アカウント連携
/// </summary>
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Link([FromQuery]string linkToken)
{
    //ログイン済みでも再ログインを求める
    await _signInManager.SignOutAsync();
    //ログイン後、Account/LinkEx に遷移するようにreturnUrlを設定
    var returnUrl = Uri.EscapeDataString($"/Account/LinkEx?linkToken={linkToken}");
    //ログイン画面へリダイレクト
    return LocalRedirect($"/Account/Login?returnUrl={returnUrl}");
}

ユーザー側に表示されるログインページ

f:id:pierre3:20180416232837p:plain:w400

ログインに成功すると、returnUrlパラメータに設定していたAccount/LinkExのアクションへ遷移します。

4. ノンスを生成してユーザーをLINEプラットフォームにリダイレクトする

ステップ3で取得したユーザーIDからノンスを生成します。ノンスの要件は以下のとおりです。 - 予測が難しく一度しか使用できない文字列であること。セキュリティ上問題があるため、自社サービスのユーザーIDなどの予測可能な値は使わないでください。 - 長さは10文字以上255文字以下であること

ノンスとしてランダムな値を生成する際の推奨事項は以下のとおりです。 - 128ビット(16バイト)以上のデータで、セキュアなランダム生成関数(SecureRandomなど)を使う。 - Base64エンコードする。

今回は、セキュアな乱数の生成にRNGCryptoServiceProvider を使用しました。

//Nonceを生成
var rngcp = new RNGCryptoServiceProvider();
var bytes = new byte[32];
rngcp.GetBytes(bytes);
var nonce = Convert.ToBase64String(bytes);

ノンスは取得したユーザーIDと紐づけて保存します。たとえば、Redisのようなキーバリューストアを使う場合は、ノンスがキーで、ユーザーIDがバリューのペアを保存するとよいでしょう。

サンプルでは、組み込みのユーザー管理で使用しているSQLサーバーのDBにユーザーIDとNonceを保存するエンティティを追加しています。
(LineAccountLinkApp/Models/LineLink.cs, LineAccountLinkApp/Data/AccountLinkAppDbContext.cs)

 //ユーザーIDとNonceをDBに保存
var userId = User.FindFirst(ClaimTypes.NameIdentifier).Value;
var link = await _appDbContext.FindAsync<LineLink>(userId);

if (link == null)
{
    await _appDbContext.AddAsync(new LineLink()
    {
        Nonce = nonce,
        UserId = userId
    });
}
else
{
    link.Nonce = nonce;
    _appDbContext.Update(link);
}
await _appDbContext.SaveChangesAsync();

ノンスとユーザーIDを保存したら、ユーザーを以下のエンドポイントにリダイレクトします。

クエリパラメータにLink Token と Nonce を付けて、LINEサーバーへリダイレクトします。
(生成したNonce に/+ の文字が入っている場合があるため、System.Uri.EscapeDataStringメソッドでエスケープしています)

//生成したNonceを付けてLINEサーバーにリダイレクト
return Redirect($"https://access.line.me/dialog/bot/accountLink?linkToken={linkToken}&nonce={Uri.EscapeDataString(nonce)}");

5. アカウントを連携する

ボットサーバーでアカウント連携イベントを受信したら、連携処理を実行します。 Webhookイベントには、連携対象のLINEのユーザーIDとステップ4で生成したノンスが含まれています。このノンスを使って、ノンスと紐づけて保存しておいた自社サービスのユーザーIDを特定します。このIDとLINEのユーザーIDを連携すれば、アカウントの連携が完了します。

連携に成功していると、ユーザー側には以下のページが表示されます。

f:id:pierre3:20180416232937p:plain:w400

BOTサーバー側には、アカウント連携の結果はWebhookイベントOnAccountLinkAsyncに送られます。(BotFunction/LineBotApp.cs)

連携に成功した場合、イベントパラメータのLink.Resultに「OK」が、Link.NonceWebサービス側で発行したNonceが設定されてきます。
Botサーバー側でも、ユーザーID(こちらはLINEアカウントに紐づくID)とNonceを保存しておきます。

/// <summary>
/// アカウント連携イベント
/// </summary>
protected override async Task OnAccountLinkAsync(AccountLinkEvent ev)
{
    if (ev.Link.Result == LinkResult.Failed)
    {
        await Line.ReplyMessageAsync(ev.ReplyToken, $"アカウント連携に失敗しました....orz");
        return;
    }

    await Line.ReplyMessageAsync(ev.ReplyToken, $"アカウント連携に成功しました!");

    //連携に成功したらNonceを保存しておく
    await Status.UpdateAsync(
        new BotStatus(
            userId: ev.Source.Id,
            accountLinkNonce: ev.Link.Nonce));

これで、アカウント連携処理は完了です。

f:id:pierre3:20180416232948p:plain:w400

ユーザーアカウント連携の利用例

連携後の処理方法については、Webサービスの特性によっていろいろと考えられますが、今回は単純に「BOTからWebサービス側のAPIを実行」できるようにしてみたいと思います。
その際、連携時に取得したNonceをそのままAPIキーとして使用することとします。

以下は、Webサービス側に登録しているユーザーアカウントの情報を取得するAPIの実行例です。

Webサービス側では以下のようなAPIを実装しておきます。(LineAccountLinkApp/Controllers/ApiController.cs)

/// <summary>
/// ユーザー情報を取得する
/// </summary>
[HttpGet("user/info")]
public async Task<IActionResult> GetUserInfo([FromQuery]string nonce)
{
    //指定したNonceを持つユーザーを検索
    var link = _dbContext.Set<LineLink>().FirstOrDefault(o => o.Nonce == nonce);
    if (link == null)
    {
        return Forbid("Invalid account link nonce.");
    }
    //ユーザーIDからユーザー情報を取得して返す
    var user = await _userManager.FindByIdAsync(link.UserId);
    if (user == null)
    {
        return NotFound("User not found.");
    }
    return new JsonResult(new { user.Id, user.UserName, user.Email });
}

BOT側では、以下の様にパラメータにNonceを指定してAPIを呼び出します。(BotFunction/LineBotApp.cs)

/// <summary>
/// アカウント連携時に取得したNonceを利用してWebAppのAPIを実行
/// </summary>
private async Task InvokeWebApiAsync(MessageEvent ev, BotStatus status)
{
    //「Webサービスに登録しているユーザーアカウント情報」を取得するAPIを使用する
    //リクエストURLのパラメータにNonceを渡して実行する
    var nonce = Uri.EscapeDataString(status.AccountLinkNonce);
    var userInfo = await _httpClient.GetStringAsync($"https://lineaccountlinkapp.azurewebsites.net/api/user/info?nonce={nonce}");
    
    await Line.ReplyMessageAsync(ev.ReplyToken, userInfo);
}

上手くいけばユーザー情報がJSONで帰ってきます。今回は、結果をそのままユーザーに返信しています。

f:id:pierre3:20180416233022p:plain:w400

アカウント連携を解除する

連携解除について

アカウント連携機能を使う場合は、以下の2点を遵守してください。 - ユーザーに連携解除機能を必ず提供すること - ユーザーがアカウントを連携するときに、連携解除機能があることを通知すること

と、公式ドキュメントにも記載されている通り、連携解除機能を実装しておく必要があります。

Webサービス側のアカウント連携解除用APIを用意する

AccountControllerUnlinkアクションを追加します。
パラメータにNonceを受け取り、該当するレコードを検索してDBから削除します。

/// <summary>
/// アカウント連携解除
/// </summary>
[HttpDelete]
[AllowAnonymous]
public async Task<IActionResult> Unlink(string nonce)
{
    //指定されたNonceを持つユーザーを削除する
    var link = _appDbContext.Set<LineLink>().FirstOrDefault(o => o.Nonce == nonce);
    if (link == null)
    {
        return NotFound("User not found.");
    }
    _appDbContext.Remove(link);
    await _appDbContext.SaveChangesAsync();

    return Ok();
}

BotからWebサービス側の連携解除APIを呼び出す

BOT側では、ユーザーから"解除"のメッセージを受け取った場合に以下の処理を行います。

/// <summary>
/// アカウント連携を解除する
/// </summary>
private async Task UnlinkAsync(MessageEvent ev, BotStatus status)
{
    var ret = await _httpClient.DeleteAsync($"https://lineaccountlinkapp.azurewebsites.net/Account/Unlink?nonce={Uri.EscapeDataString(status.AccountLinkNonce)}");
    if (!ret.IsSuccessStatusCode)
    {
        await Line.ReplyMessageAsync(ev.ReplyToken, "アカウントリンクの解除に失敗しました。");
    }
    else
    {
        await Status.DeleteAsync(ev.Source.Type.ToString(), ev.Source.Id);
        await Line.ReplyMessageAsync(ev.ReplyToken, "アカウントリンクを解除しました。");
    }
}

まとめ

ここまでで、ユーザーアカウント連携に必要な手順の説明は終わりです。
今回の利用例では、連携することでWebサービス側のAPIを簡単に利用することができるようになりました。

次は、公式ドキュメントで推奨しているように、リッチメニューを利用して「連携」・「解除」を簡単にできるようにしてみたいと思います。

たとえば、Messaging APIを使えば、表示するリッチメニューをユーザーごとに変更できます。アカウントを連携していないユーザーにはアカウントを連携するメニューを表示し、アカウントを連携済みのユーザーには連携を解除するメニューを表示すれば、ユーザーにとって使いやすい形でアカウント連携機能を提供できるでしょう。

LINE Loginを利用したWebアプリを ASP.NET Core + OpenID Connectで実装する(その2:メールアドレスを取得する)

この記事は、以下のエントリの続きとなります。

pierre3.hatenablog.com

今回は、LINEでログイン時にユーザーのメールアドレスを取得する方法について述べます。

メールアドレスの取得権限を取得する

LINE Loginでメールアドレスを取得するには、LINE Loginアカウントの設定画面から申請して取得権限を付与してもらう必要があります。

f:id:pierre3:20180331212033p:plain:w600

LINE Developers コンソール でLINE Loginアカウントを選択して、Channel基本設定画面を開きます。
OpenID Connect - メールアドレス」の「申請」をクリックすると以下のような申請画面が表示されます。

f:id:pierre3:20180331212045p:plain:w600

この画面から申請を行うのですが、

本サービスで、メールアドレスの取得/利用目的について、ユーザに通知、明示、または同意取得している部分のスクリーンショットをアップロードしてください。

と記載されている通り、申請するには上記のようなスクリーンショットをアップロードする必要があります。

メールの取得/利用目的を明示したログイン画面を作る

前回までのアプリケーションでは、未ログイン時にトップページへアクセスするとLINEのログイン画面が直接表示されるようになっていましたが、以下のようなログイン画面を挟むように修正します。

f:id:pierre3:20180331223950p:plain:w600

この画面のスクリーンショットを取って、申請に使用することとします。

View とControllerを修正する

・Views/AccountフォルダにLogin.cshtmlを作成します。

<-- Login.cshtml -->

@{
    ViewData["Title"] = "Login";
}

<div class="jumbotron">
    <div class="panel panel-info center-block">
        <div class="panel-heading">ログインしてください</div>
        <div class="panel-body">
            LINEアカウントを利用してログインを行います。<br />
            <hr />
            本Webサービスでは、ログイン時の認証画面にて許可を頂いた場合のみ、あなたのLINEアカウントに登録されているメールアドレスを取得します。<br />
            取得したメールアドレスは、以下の目的以外では使用いたしません。また、法令に定められた場合を除き、第三者への提供はいたしません。
            <ul>
                <li>ユーザーIDとしてアカウントの管理に利用</li>
                <li>パスワード再発行時の本人確認に利用</li>
            </ul>
            <hr/>
        </div>
        <p class="text-center"><a asp-area="" asp-controller="Account" asp-action="LineLogin">
            <img src="~/images/btn_login_base.png"/>
        </a>
        </p>
    </div>
</div>

・Viewに対応するメソッドをAccountControllerに追加します。

public class AccountController : Controller
{
    //ログイン画面(/Account/Login)
    public IActionResult Login()
    {
        return View();
    }

    //ログイン画面の「LINE Log in」ボタンのアクション
    public IActionResult LineLogin()
    {
        return Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme);
    }

    public IActionResult Logout()
    {
        return SignOut(new AuthenticationProperties { RedirectUri = "/" }, CookieAuthenticationDefaults.AuthenticationScheme);
    }
}

これでパーツは揃いました。あとはControllerで[Authorize]属性を付けたページにアクセスした際に、ログインページ(/Acount/Login)にリダイレクトするように設定するだけです。

現在LIENのログイン画面にリダイレクトされているのは、StartupクラスのConfigurationService() に記述した以下の設定があるためです。

options.DefaultChallengeScheme = OpenIdConnectDefault.AuthenticationScheme;

この1行を削除するだけで、既定のログインページ「/Account/Login」へリダイレクトされるようになります。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.Configure<LineSettings>(Configuration.GetSection("LineSettings"));

    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        
        //この設定により、未ログイン時はLINEのログイン画面へリダイレクトされる。
        //options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(o =>
    {
        //...
    });
}

ログイン画面のスクリーンショットを取ったら、先ほどの申請画面にアップロードします。

f:id:pierre3:20180401081819p:plain:w600

同意する項目のチェックボックスにチェックを入れて「申請」をクリックします。
申請が受理されると以下の様に「申請済み」と表示され、メールアドレスの取得ができるようになります。

( 私が試したところ、「申請」クリック後即座に「申請済み」となりメールアドレスの取得も可能となりました。(画像認識で自動判定とかしているのかしら?))

f:id:pierre3:20180401082656p:plain:w600

OpenID Connectでメールアドレスを取得するように設定する

メールアドレスの取得権限が付与されたら、あとは認証時のスコープに"email"を追加するだけです。

//Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.Configure<LineSettings>(Configuration.GetSection("LineSettings"));

    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        //options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(o =>
    {
        o.ClientId = Configuration["LineSettings:LoginClientId"];
        o.ClientSecret = Configuration["LineSettings:LoginClientSecret"];
        o.ResponseType = OpenIdConnectResponseType.Code;
        o.UseTokenLifetime = true;
        o.SaveTokens = true;
        //スコープにemailを追加
        o.Scope.Add("email");
                
        o.Configuration = new OpenIdConnectConfiguration
        {
            Issuer = "https://access.line.me",
            AuthorizationEndpoint = "https://access.line.me/oauth2/v2.1/authorize?bot_prompt=aggressive",
            TokenEndpoint = "https://api.line.me/oauth2/v2.1/token"
        };
        o.TokenValidationParameters = new TokenValidationParameters
        {
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(o.ClientSecret)),
            NameClaimType = "name",
            ValidAudience = o.ClientId
        };
    });
}

取得したメールアドレスを確認する

トップページに取得したメールアドレスを表示するようにHome/IndexのViewを書き換えます。
メールアドレスの取得にはClaimTypes.Emailを使用します。

@{
    ViewData["Title"] = "Home Page";
}
@using System.Security.Claims;

<div class="jumbotron">
    <div class="panel panel-default center-block">
        <div class="panel panel-heading">
            <div class="row">
                <div class="col-md-10 col-sm-10 col-xs-8">
                    <p>
                        <img src="@User.FindFirst("picture")?.Value" width="32" height="32" alt="User Image" />
                        @User.Identity.Name (@User.FindFirst(ClaimTypes.Email)?.Value)
                    </p>
                </div>
                <div class="col-md-2 col-sm-2 col-xs-4">
                    <a class="btn btn-default" role="button" asp-area="" asp-controller="Account" asp-action="Logout">Logout</a>
                </div>
            </div>
        </div>
        <div class="panel panel-body">
            <h2>プッシュメッセージ送信テスト</h2>
            <form role="form" asp-area="" asp-controller="Home" asp-action="PushMessage" method="post">
                <input type="text" name="pushMessage" class="form-control" placeholder="プッシュするメッセージを入力してください">
                <button type="submit" class="btn btn-default">送信</button>
            </form>
        </div>
    </div>
</div>

これで準備完了です。確認してみましょう。

・ログイン時の認可画面で取得項目にメールアドレスが追加されています。

f:id:pierre3:20180401085958p:plain:w500

・メールアドレスの取得を許可してログイン。取得したメールアドレスが表示されました!

f:id:pierre3:20180401090041p:plain:w600

LINE Loginを利用したWebアプリを ASP.NET Core + OpenID Connectで実装する


■ 目次


チュートリアル

今回は、LINE Loginを利用したWebアプリケーションをASP.NET Core +OpenID Connectで実装する方法について解説したいと思います。

はじめに

ここでは、Visual Studio 2017を使って「「ASP.NET Core Webアプリケーション」テンプレートからLINE Loginに対応したWebアプリを実装する手順」について解説します。

なお、ASP.NET CoreでOpenID Connectを使用する手順は、こちらの記事を参考にさせていただきました。

blog.shibayan.jp

また、この記事の解説に使用したサンプルアプリをGitHubに上げています。 とりあえず動かしてみたいという方はこちらをCloneして試してみてください。

github.com

※ このサンプルアプリを試す際は、以下の手順でLINE LoginのChannel情報等を設定してください。

  • appSettings.Base.json ファイルをappSettings.jsonにリネームする
  • リネームしたファイル内の"LineSettings"以下を、ご自身のLINE Loginアカウント情報に書き換える
    • LINE Login アカウントのChannel ID
    • LINE Login アカウントのChannel Secret
    • 連携するBotアカウント のAccess Token

LINE の設定

LINE Loginアカウントを作成する

まずは、以下の公式ドキュメントに従ってLINE Loginアカウントを作成します。

LINEログインを利用するには

連携するBotアカウントの用意

ログイン時に友達追加するBotのアカウントも作成しておきます。プッシュメッセージを使用するのでDeveloper Trialプランで作成します。 (もちろん、すでに作成済みのBotアカウントで試しても良いです)

Messaging APIを利用するには

なお、今回はログイン時に友達追加できることと、Webアプリからプッシュメッセージが送信できることが確認できれば良いのでWebhookの設定は不要です。

LINE Login アカウントの設定

LINE Loginアカウントを作成したら「Channel基本設定」を確認します。

  • Channel ID とChannel Secretは後で必要となるので控えておきます
  • 「このチャネルにリンクされたボット」欄で、作成済みのBotの一覧から先ほど用意したBotを選択します
  • 「アプリタイプ」にはWEBを選択します

f:id:pierre3:20180331072144p:plain:w400

左側のメニューで「アプリ設定」を選択し、ログイン後のリダイレクト先を設定します。

f:id:pierre3:20180331065551p:plain:w600

  • リダイレクト先のパスは「/signin-oidc」とします。
  • ローカルデバッグでの確認であれば、Visual Studio でのデバッグ実行時のURLでOKです。(デバッグ時のURLはPropertiesフォルダ内にある「launchSettings.json」で確認できます)

Webアプリケーションの作成

ASP.NET Coreのプロジェクトを作成する

メニュー[ファイル]-[新規作成]-[プロジェクト...]から「ASP.NET Core Webアプリケーション」を選択

f:id:pierre3:20180329215712p:plain:w600

テンプレートのタイプは「Webアプリケーション(モデル ビュー コントローラ)」を選択。「認証なし」のままで作成します。

f:id:pierre3:20180329220425p:plain:w600

NuGetパッケージを追加する

「ソリューションのNuGetパッケージの管理」画面または、パッケージマネージャコンソールで Microsoft.AspNetCore.Authentication.OpenIdConnect をインストールします。

PM> Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect

AppSettings.json に LINE Login のChannel情報を設定する

予め、appsettings.json に LINE Login のアカウント登録時に取得した ChannelIdとChannel Secretの値を追加しておきます。

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "LineSettings": {
    "LoginClientId": "156xxxxxxxxxx",
    "LoginClientSecret": "934a4xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  }
}

StartupクラスでOpenID Connectを使用するように設定する

あとは、Stertup.cs で以下の記述を追加するだけでOKです。 (詳細は冒頭でリンクしたしばやん先生の記事をご確認ください)

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

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        //OpenID Connectを使用するための記述 ここから==>
        services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddCookie()
        .AddOpenIdConnect(o =>
        {
            //ClientIdとClientSecretをConfiguration(appsettings.json)から取得。階層のあるデータはコロン(:)で区切って指定する
            o.ClientId = Configuration["LineSettings:LoginClientId"];
            o.ClientSecret = Configuration["LineSettings:LoginClientSecret"];
            o.ResponseType = OpenIdConnectResponseType.Code;
            o.UseTokenLifetime = true;
            o.SaveTokens = true;
            
            o.Configuration = new OpenIdConnectConfiguration
            {
                Issuer = "https://access.line.me",
                AuthorizationEndpoint = "https://access.line.me/oauth2/v2.1/authorize",
                TokenEndpoint = "https://api.line.me/oauth2/v2.1/token"
            };

            o.TokenValidationParameters = new TokenValidationParameters
            {
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(o.ClientSecret)),
                NameClaimType = "name",
                ValidAudience = o.ClientId
            };
        });
        //<==ここまで
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseBrowserLink();
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();
        
        //Authentication Middleware を使う
        app.UseAuthentication();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

これで、Controller の[Authorize]属性を付けたメソッドにアクセスすると、LINEアカウントのログイン画面にリダイレクトされるようになります。

・Controllers/HomeController.c

//<HomeController.cs>

public class HomeController:Controller
{
    [Authorize]
    public IActionResult Index()
    {
        return View();
    }
}

f:id:pierre3:20180329234308p:plain:w300

ログイン時にBotアカウントを友達追加できるようにする

ユーザーがLINEアカウントの認可画面で関連するBotを友達追加するか否かを選択できるようにするには、認可URLに、クエリパラメータbot_promptを追加します。

先ほどのコードのAddOpenIdConnect() に渡すラムダ式内、AuthorizationEndpoint に設定するURLを以下の様に修正します。

o.Configuration = new OpenIdConnectConfiguration
{
    Issuer = "https://access.line.me",
    AuthorizationEndpoint = "https://access.line.me/oauth2/v2.1/authorize?bot_prompt=normal",
    TokenEndpoint = "https://api.line.me/oauth2/v2.1/token"
};

bot_prompt の値は、normal、aggressiveの2種類があり、aggressiveを設定した場合は、認可画面を閉じた後にボット追加用の確認画面が表示されるようになります。

f:id:pierre3:20180330223612p:plain:w300

  • aggressive の場合(認可画面からログイン後、確認用のダイアログが表示される)

f:id:pierre3:20180330224231p:plain:w300

なお、「ブロック解除」と表示されているのは、動作確認用に一度友達追加したBOTを「ブロック」しているためです。初めて友達追加する場合は「友達追加」と表示されます。

取得したユーザー情報を確認する

デバッカで確認してみると、取得したユーザーの情報はUser.Identity.Claimesに格納されているのが分かります。

f:id:pierre3:20180331095929p:plain

ClaimsプロパティはKey-Valueのコレクションとなっていて、値はUser.FindFirst()メソッドにKeyを指定して取得することができます。
定義済みのClaimは"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"の様にURLで指定する必要がありますが、 System.Security.Claims.ClaimTypes に定数として定義されているのでこれを使用します。

//View(Razor)でもControllerでも同じ様に取得できます
var name =User.FindFirst("picture").Value;  //ユーザー名はUser.Identity.Nameでも取得可
var picture = User.FindFirst("picture").Value;  //画像のURLを取得
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier).Value;  //LINEアカウントに紐づくユーザーID

それでは、ログインしたユーザーの情報が確認できるように、トップページ(Home/Index)のViewを書き換えてみます。

<-- Index.cshtml -->

@{
    ViewData["Title"] = "Home Page";
}

<div class="jumbotron">
    <div class="panel panel-default center-block">
        <div class="panel panel-heading">
            <div class="row">
                <div class="col-md-10 col-sm-10 col-xs-8">
                    <p><img src="@User.FindFirst("picture").Value" width="32" height="32" alt="User Image"> @User.Identity.Name</p>
                </div>
                <div class="col-md-2 col-sm-2 col-xs-4">
                    <a class="btn btn-default" role="button" asp-area="" asp-controller="Account" asp-action="Logout">Logout</a>
                </div>
            </div>
        </div>
    </div>
</div>

ついでに「Logout」ボタンでログアウトできるように、AccountControllerを追加してLogout()メソッドを実装します。

//<AccountController.cs>

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Mvc;

namespace LineLoginSampleApp.Controllers
{
    public class AccountController : Controller
    {
        public IActionResult Logout()
        {
            return SignOut(new AuthenticationProperties { RedirectUri = "/" }, CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}

ここで、一度デバッグ実行してみましょう。 LINEのログイン画面からログイン後、以下の様にユーザー名と画像が表示されればOKです。
「Logout」ボタンも確認してみましょう。ボタン押下後、再びLINEのログイン画面にリダイレクトされることが確認できると思います。

f:id:pierre3:20180331093703p:plain:w400

Botからプッシュメッセージを送ってみる

次は、LINEログイン時にBotを友達追加したユーザーに対してBotからプッシュメッセージを送ってみましょう。

Messaging APIのライブラリを追加

LINE Messaging APISDKをNuGetからインストールします。

PM > Install-Package Line.Messaging
appSettings.jsonBotのChannel アクセストークンを追加する

LINE Developersコンソールを開き、今回用意したBotを選択します。
Channel基本設定で「アクセストークン(ロングターム)」を確認し、appSettings.jsonに追加します。

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "LineSettings": {
    "LoginClientId": "156xxxxxxxxxx",
    "LoginClientSecret": "934a4xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "MessagingAccessToken": "zd1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}
メッセージを送ってみる

画面から送信するメッセージの内容を設定して送れるようにViewとControllerを書き換えます。

・Home/Index.cshtml に、メッセージの入力ボックスと送信ボタンを追加します。

<-- Index.cshtml -->

@{
    ViewData["Title"] = "Home Page";
}
@using System.Security.Claims;

<div class="jumbotron">
    <div class="panel panel-default center-block">
        <div class="panel panel-heading">
            <div class="row">
                <div class="col-md-10 col-sm-10 col-xs-8">
                    <p>
                        <img src="@User.FindFirst("picture")?.Value" width="32" height="32" alt="User Image" />
                        @User.Identity.Name
                    </p>
                </div>
                <div class="col-md-2 col-sm-2 col-xs-4">
                    <a class="btn btn-default" role="button" asp-area="" asp-controller="Account" asp-action="Logout">Logout</a>
                </div>
            </div>
        </div>
        <div class="panel panel-body">
            <h2>プッシュメッセージ送信テスト</h2>
            <form role="form" asp-area="" asp-controller="Home" asp-action="PushMessage" method="post">
                <input type="text" name="pushMessage" class="form-control" placeholder="プッシュするメッセージを入力してください">
                <button type="submit" class="btn btn-default">送信</button>
            </form>
        </div>
    </div>
</div>

・Views/Home/ の配下にメッセージ送信後の遷移先となるView(PushMessage.cshtml)を作成しておきます。

<-- PushMessage.cshtml -->

@{
    ViewData["Title"] = "PushMessage";
}

<h2>送信しました</h2>
<a class="btn btn-default" role="button" asp-area="" asp-controller="Home" asp-action="Index">戻る</a>

・HomeControllerにPushMessageメソッドを追加してそこに送信処理を記述します。

//<HomeController.cs>

public class HomeController : Controller
{
    private LineSettings lineSettings;
    public HomeController(IOptions<LineSettings> options)
    {
        lineSettings = options.Value;
    }

    [Authorize]
    public IActionResult Index()
    {
        return View();
    }
    
    [HttpPost]
    [Authorize]
    public async Task<IActionResult> PushMessage(string pushMessage)
    {
        var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier).Value;
         using (var line = new Line.Messaging.LineMessagingClient(lineSettings.MessagingAccessToken))
        {
            await line.PushMessageAsync(userId, pushMessage);
        }
        return View();
    }
}

なお、appSettings.jsonに設定したアクセストークンを、HomeControllerのコンストラクタの引数LineSettingsオブジェクトから取得していますが、これを行うためにはStartupクラスでアプリケーション設定をDI(Dependency Injection)するよう設定しておく必要があります。

・ModelsディレクトリにLineSettingsクラスを追加します。

//<LineSettings.cs>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace LineLoginSampleApp.Models
{
    public class LineSettings
    {
        public string LoginClientId { get; set; }
        public string LoginClientSecret { get; set; }
        public string MessagingAccessToken { get; set; }
    }
}

・StartupクラスのConfigurationServices()メソッドに、アプリケーション設定をDIするコードを追加します。

//<Stertup.cs>

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    
    //ConfigurationからLineSettingsセクションを抽出してDependency Injectionする
    services.Configure<LineSettings>(Configuration.GetSection("LineSettings"));
    
   //...
}

これで準備完了です。実行してみましょう。

・メッセージを入力して「送信」

f:id:pierre3:20180331161942p:plain:w500

・LINEアプリを確認し、BOTから入力したメッセージが送られてきていれば成功です!

f:id:pierre3:20180331162032p:plain:w500

次は

次は、ユーザーのメールアドレスを取得する方法を見てみようと思いますが、長くなりすぎてしまったので次回にします。

pierre3.hatenablog.com

C#で開発するLINE Botアプリケーション (開発環境編)

(もうだいぶ前の話になってしまいましたが)C# 版LINE Messaging API が、コミュニティSDKとしてLINEの公式サイトに掲載されました!

C#でLINE BOTを開発する環境もいろいろと整ってきましたので、この辺で少し整理したいと思います。

■ 目次

開発環境

開発環境としては、大きく分けて以下の2つがあります。

MacLinuxで開発したい方は前者を選択することになります。
Windowsで開発する場合は、どちらでもお好きな方を。

クロスプラットフォームでの開発

日本マイクロソフト@kenakamu さんがyeomanのテンプレートや導入記事を公開してくださっています。

テンプレート

yeoman テンプレートは、C#以外にもgo、nodejs、pyhon、rubyなどにも対応しています。

github.com

チュートリアル

Azure Functions (v2.0) で実装する方法と、ASP.NET CoreのWeb Apps で実装する方法の2種類が用意されています。

特にWeb Apps 編の記事は、LINE Bot開発入門者向けのハンズオンセミナーのテキストとして使用しているものです。
入門者の方は、まずはこちらの手順に従って試してみるのが良いでしょう。

line.connpass.com

Visual Studio 2017での開発

テンプレート

Visual Studio Market Place にてプロジェクトテンプレートを公開しています。

marketplace.visualstudio.com

このテンプレートをインストールすると、以下の3種類のプロジェクトが作成できるようになります。

  • Azure Functions 用テンプレート(LINE Bot Function)
  • Web Apps 用テンプレート (LINE Bot Application)
  • Bot Frameworkに対応したWeb Appsのテンプレート(LINE with Bot Framework Application)

チュートリアル

各テンプレートの利用手順は、以下を参照してください

  • Aure Function

pierre3.hatenablog.com

開発を便利にするツールなど

LINESimulator

アプリケーションのデバッグに非常に便利なLINE サーバーのシミュレータです。
node.js 製ですので、C#に限らずあらゆる開発環境で利用可能です。

github.com

line-richmenus-manager

Botアプリケーションの使い勝手を格段に向上する「リッチメニュー」の作成をサポートするアプリケーションです。
リッチメニューの作成とLINE サーバーへのアップロードが可能です。

github.com

C#で開発するLINE Botアプリケーション(環境構築:Visual Studio 2017 + Azure Functions編)

この記事では、Visual Studio 2017とAzure Functionsを使用してLINE Bot アプリケーションを開発を行う手順を解説します。

1.Azure アカウント、LINE@ アカウントの準備

Microsoft Azure アカウント

Microsoft  Azureのアカウントを持っていない場合は、作成しておきます。

Azure Functions では無料のサービスプランを選択することはできないため、3000円/月のクレジットが1年間付いてくる「Visual Studio Dev Essentials」の利用をお勧めします。

LINE@アカウント

LINE Messaging API を利用するLINE@アカウントを作成します。

作成や設定の手順はこちらの記事の「LINE のセットアップ」を参照ください。 - qiita.com

2.プロジェクトテンプレートをインストールし、LINE Bot Function のプロジェクトを作成する

  1. Visual Studio Market Placeから「LINE Bot C# Template」をインストールします。
    以下のリンクで「Download」をクリックしてダウンロードしたVSIXファイルを実行するか、Visual Studioのメニュー[ツール]-[拡張機能と更新プログラム...]から「LINE Bot C# Template」を検索してインストールします。 marketplace.visualstudio.com

  2. インストール後、Visual Studio 2017 を起動し、メニューから[ファイル]-[新規作成]-[プロジェクト]を選択します。

  3. 「新しいプロジェクト」ダイアログで[Visual C#]-[Cloud]のカテゴリ内にある「LINE Bot Function」を選択します。
  4. プロジェクト名を入力し、「OK」をクリックします。(ここで入力したプロジェクト名が作成するAzure Functionの関数名の既定値となります。)

f:id:pierre3:20180318160255p:plain:w800

3.作成されたプロジェクトをビルドする

メニュー[ビルド]-[ソリューションのビルド]を選択し、ビルドエラーがないことを確認します。

4.ビルドしたFunctionをAzureに発行する

  • ソリューションエクスプローラで、作成したLINE Bot Functionのプロジェクトを右クリックし、ポップアップメニューで[発行...]を選択します

f:id:pierre3:20180318160435p:plain:w400

  • 次の画面で「Azure 関数アプリ」-「新規作成」を選択し、 「発行」をクリックします。

f:id:pierre3:20180318160708p:plain:w800

  • App Serviceの作成ダイアログで、作成するFuncsion App Serviceの設定を行います。

f:id:pierre3:20180318161128p:plain:w400

  • 「作成」をクリックするとAzure FunctionのApp Serviceが作成され、そこにビルドしたアプリケーションが発行されます。

5.LINE Developpersに、作成したFunctionのURLを登録する。

LINE Developers Console で、作成したBotアカウントを選択し、Channel基本設定画面を開きます。

f:id:pierre3:20180318162734p:plain:w800

  • メッセージ送受信設定の「Webhook送信」を「利用する」に設定します。
  • Botグループトーク参加」を「利用する」に設定します。
  • 「Webhook URL」に、作成したFunctionのURLを入力します。
    入力するAzure FunctonのURLは、既定では『https://{App ServiceのApp Name}.azurewebsites.net/api/{Function名}』となります。

f:id:pierre3:20180318164136p:plain:w600

  • LINE@機能の利用の「自動応答メッセージ」及び「友だち追加時あいさつ」を「利用しない」に設定します。

f:id:pierre3:20180318165111p:plain:w600

また、この画面に記載されているChannel Secret とアクセストークンの値は、この後の設定で必要となりますので確認しておきます。

6.LINE Messaging APIの 「Channel Secret」および「Channel Access Token」をアプリケーション設定に登録する

  1. Azureポータルで、作成したAzure Functionを選択して[Application Settings]画面を開きます。
  2. アプリケーション設定の一覧に、先ほどのChannel基本設定画面で確認した「Channel Secret」と「Channel Access Token(アクセストークン(ロングターム))」を追加します。

f:id:pierre3:20180318170305p:plain:w800

7.デバッグ情報通知用に自分のLINEアカウントを設定する

アプリケーション内のエラーの情報やデバッグ情報の通知先として、自分のLINEアカウントのUserIDを登録します。User IDは、Channel基本設定ページの一番下に記載されています。

f:id:pierre3:20180318171239p:plain:w600

これを、「DebugUser」という名前で、Azure Functionのアプリケーション設定に追加します。

f:id:pierre3:20180318171018p:plain:w800

8.動作確認

確認する端末でBOTアカウントを友達登録(※)し、メッセージを送ってみましょう。

送信したメッセージと同じ内容がBOTアカウントから返信されればOK。設定完了です。

(※)Channel基本設定(LINE Developpers)画面に表示されるQRコードを読み取ることで友達登録が可能です。