やさしいチャットボットのつくりかた -4.Twitterからつぶやきをとってきてみよう！

#どういう記事？
Azure素人の自分がMicrosoft AzureのBot FrameworkとCognitive Serviceと触れ合いながら、難易度も人への接し方も**“やさしい”**チャットボットを作ってみた際の備忘録の第4部です。どんなチャットボットを作るのかを含め、これまでの手順は[第1部](https://qiita.com/miokawa/private/6f1a3e480580e1e29abc)、[第2部](https://qiita.com/miokawa/private/c71f9f4b62ce2b1c2864)、[第3部](https://qiita.com/miokawa/private/cdb243401fe3c8d9d583)にございます。
PowerPointで記録していたものをQiita化してみたものになるので、そんな感じの画像が頻出します。
Qiita初心者なので、書き方が下手でも広い心で許してほしいです。そして全体的にゆるいです。

# アジェンダ
以下の5部構成になります。この記事は第4部にあたります。

1. [Bot Frameworkでチャットボットを作ってみよう！](https://qiita.com/miokawa/private/6f1a3e480580e1e29abc)
2. [ボットをAzure（クラウド）にデプロイしてみよう！](https://qiita.com/miokawa/private/c71f9f4b62ce2b1c2864)
3. [Cognitive Serviceでテキスト分析をしてみよう！](https://qiita.com/miokawa/private/cdb243401fe3c8d9d583)
4. [Twitterからつぶやきをとってきてみよう！](https://qiita.com/miokawa/private/eb9f16220a36f65d7098)**←イマココ**
5. [LINE経由でボットと話してみよう！](https://qiita.com/miokawa/private/c6f0b550f8fa41a23228)

なお、公式ドキュメントに沿って作りましたので、それらのリンクを張り付けつつ、辿った道筋通りに書いていきます。
その筋の方からすると遠回りだったり野暮ったかったりすると思いますが、ご容赦ください。

# 2. Twitterからつぶやきをとってきてみよう！
これまで、[第1部](https://qiita.com/miokawa/private/6f1a3e480580e1e29abc)で作ったチャットボットのソースコードを、[第2部](https://qiita.com/miokawa/private/c71f9f4b62ce2b1c2864)でAzure（クラウド）上へデプロイし、ユーザーから受け取ったコメントを[Azure Cognitive Service](![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/d3b1948f-f50d-6e3a-9f15-8a4a21856cc1.png)
)の[Text Analytics](https://azure.microsoft.com/ja-jp/services/cognitive-services/text-analytics/)を使って、ネガティブorポジティブの判定をしてみました。今回は、図中の赤枠部分、Text Analyticsの判定結果がネガティブだった場合、Twitterで偉人さんのありがたいお言葉をつぶやく[励ましBotさん](https://twitter.com/hagemasi1_bot)のつぶやきをランダムに選択し、ユーザーに返してあげる部分を作っていこうと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/34f880f9-d8de-b5d0-4143-790478c0bc1c.png)


## 2.1 事前準備
#### Twitter Developer登録
Twitterの各種APIを使うためには、Developer登録が必要となります。審査に数日かかるようです。
[こちら](https://maipyon.net/twitter-developer-account/)にDeveloper登録について、詳しく書かれているので参考にしてください。
<font color="red">手順中で、Access tokenとaccess token secretが生成されますので、忘れずにメモをしておいてください。後々使います。</font>



#### CoreTweetインストール
C#からTwitterAPI を扱うためのライブラリである、CoreTweetのインストールをします。[第3部のTextAnalyticsの設定](https://qiita.com/miokawa/private/cdb243401fe3c8d9d583#23-text-analytics%E3%81%AE%E8%A8%AD%E5%AE%9A)でも実施したパッケージのインストールをします。検索ボックスで`CoreTweet`と検索して、インストールを実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/e5f34e2d-2d2b-4052-649a-4b03be81d77d.png)

## 2.2 sentimentAnalysisExampleメソッドから分析スコアを返す
[第3部](https://qiita.com/miokawa/private/cdb243401fe3c8d9d583)で作成したソースコードを前提に、必要な変更を加えていきます。まずは、ユーザーのメッセージをText Analyticsリソースに投げて分析しているsentimentAnalysisExampleメソッドが、呼び出し元に分析結果のスコアを返すようにコードを変更します。

```cs:EchoBot.cs
static double sentimentAnalysisExample(ITextAnalyticsClient client, string message)//戻り値をvoidからdouble型に変更
{
     var result = client.Sentiment(message, "ja");//引数messageを分析対象にし、言語は日本語に設定
     Debug.WriteLine($"User Message: {message}");//引数messageの内容を出力
     Debug.WriteLine($"Sentiment Score1: {result.Score:0.00}");//出力確認1
     return (double)result.Score;//分析スコアを返す
}
```

呼び出し側でも戻り値を格納する変数を用意する必要がありますので、OnMessageActivityAsync内でsentimentAnalysisExampleの部分に以下の通り変更を加えます。Debug.WriteLineは値の受け渡しが正常にできているかの確認のための出力です。

```cs:EchoBot.cs
var score = sentimentAnalysisExample(client,turnContext.Activity.Text);//戻り値を格納するためscoreを用意Debug.WriteLine($"Sentiment Score2: {score:0.00}");//出力確認2
```

## 2.3 スコアを元に条件分岐をする
Text Analyticsでは0~1の間の値を返します。1に近いほどポジティブで、0に近いほどネガティブな判定という意味となります。
今回は、スコアが0.5以上の場合はポジティブとみなして塩対応し、0.5未満の場合はTwitterからありがたいお言葉をとってきたいと思いますので、OnMessageActivityAsync内で条件分岐をし、それぞれの場合でDebug.WriteLineで出力確認をしたいと思います。
今までのOnMessageActivityAsyncに対する変更をまとめると以下の通りとなります。

```cs:EchoBot.cs
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    var replyText = $"Echo: {turnContext.Activity.Text}";
    await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken);

    //for TextAnalytics
    var client = authenticateClient();
    var score = sentimentAnalysisExample(client,turnContext.Activity.Text);//戻り値を格納するためscoreを用意
    Debug.WriteLine($"Sentiment Score2: {score:0.00}");//出力確認2

    if(0.5 <= score)
    {
        Debug.WriteLine($"塩対応");//出力確認3
    }
    else
    {
        Debug.WriteLine($"励まし対応");//出力確認4
    }
}
```

一度実行して試してみましょう。ボットに話しかけたメッセージと、そのスコア（2回）、スコアに応じた対応内容が出力されていればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/87b304fe-9a61-d39f-6d26-53ccf91a322c.png)

## 2.4 励まし対応（Twitter連携）部分を作る
ちゃんとスコアを受け取り、条件分岐できていることが確認できたので、いよいよTwitterとの連携部分を作っていきます。[事前準備](#事前準備)でインストールしたCoreTweetを使うので、以下の通りソースコードの上の方（usingほげほげがあるところ）に追加します。

```cs:EchoBot.cs
using CoreTweet;
```

そしてpublic class EchoBotの中のsentimentAnalysisExampleと同じレベル（深さ）に、Twitterからツイートを取ってくるためのgetTweetメソッドを用意します。

```cs:EchoBot.cs
 static string getTweet(double score)
 {
     Random cRandom = new System.Random(); //取得したツイートからランダムに1つ選択するための乱数
     var tokens = Tokens.Create("<API KEY>", "<API SECRET>", "<ACCESS TOKEN>", "<ACCESS TOKEN SECRET>");  //接続用トークン発行
     var tweet = "";//取得したツイートを格納する変数

     var parm = new Dictionary<string, object>();  //条件指定用Dictionary
     parm["count"] = 60;  //取得するツイート数
     parm["screen_name"] = "hagemasi1_bot";  //取得したいユーザーID


     Task task = Task.Factory.StartNew(async () =>
     {
          var tweets = await tokens.Statuses.UserTimelineAsync(parm); //parmの内容に従ってツイートを取得

          var random = cRandom.Next(61); //0～60の間の乱数を生成
          tweet = tweets[random].Text; //取得した60ツイートからrandom番目のツイートを格納

     }).Unwrap();

     task.Wait();

     return tweet;　//選んだツイートを戻り値として返す
}
```

それぞれ簡単に説明していきます。

+ **Random cRandom = new System.Random();**
一括でとってきたツイートたちから、どれか1つを選択する際に使う乱数です。

+ **var tokens = Tokens.Create()**
Twitterに接続する際のトークンです。それぞれ以下の手順で確認して、正しいものを入れていきます。

[Twitter Developerサイト](https://developer.twitter.com/)にアクセスし、画面の右上で、自分のTwitterアカウントより、**Apps**を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/9422b852-d565-84e8-a659-18233f52556e.png)

該当アプリの**Details**ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/554f5b33-6071-8cc2-b673-af9497a71f8c.png)

**Keys and tokens**タブをクリックします。API keyとAPI secret keyが表示されます。Access tokenとaccess token secretについては、事前準備の[Twitter Developer登録](#Twitter Developer登録)でメモしておいたものを使います。もしメモし損ねてしまっていたら、**Regenerate**ボタンで再作成してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/33af94d4-02b7-7f40-342b-41551c7d39e9.png)

+ **var tweet = "";**
取得してきたツイート群から、実際にユーザーに返すものを格納する変数です。

+ **var parm = new Dictionary<string, object>();**
取得するツイートの条件を指定するためのDictionaryクラスです。今回は取得してくる全ツイート数（60ツイート）と、取得対象のTwitterアカウントのID（hagemasi1_bot）を指定しています。Dictionaryクラスの公式リファレンスは[こちら](https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.generic.dictionary-2?redirectedfrom=MSDN&view=netframework-4.8)

+ **var tweets = await tokens.Statuses.UserTimelineAsync(parm);**
直前のDictionaryクラスで指定した内容を引数にして、hagemasi1_botのツイートを60ツイート取得してきています。戻り値は取得したツイートたちです。CoreTweetの公式リファレンスは[こちら](http://coretweet.github.io/docs/classCoreTweet_1_1Rest_1_1Statuses.html#a355f15e598c63dfb2b218f75a8a4097f)

+ **var random = cRandom.Next(61);**
取得した60個のツイートから、何番目のツイートをユーザーに返すか、乱数で決めています。randomには0~60の値のいずれかが入ります。

+ **tweet = tweets[random].Text;**
取得した60個のツイートのうち、random番目のものを選んで、tweet変数に格納しています。これをユーザーに返します。

+ **return tweet;**
tweet変数を呼び出し元に返します。


Twitterからツイートを取得するメソッドができたので、OnMessageActivityAsync内の条件分岐で**Debug.WriteLine($"励まし対応")**としていた部分からgetTweetを呼び出して、結果を出力するようにします。

```cs:EchoBot.cs
if(0.5 <= score)
{
    Debug.WriteLine($"塩対応");//出力確認3
}
else
{
    var res = getTweet(score);
    Debug.WriteLine($"励まし対応:{res}");//出力確認4
}
```

ではここでまた、実行してみましょう。できるだけネガティブな発言をボットに投げかけてくださいね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/92efc810-c4d0-86cc-8e87-23f99c1a4363.png)

ネガティブ判定され、Twitterから偉人さんのありがたいお言葉を取得してくることができました！

## 2.5 塩対応部分を作る
TextAnalyticsの分析スコアが0.5以上だった場合の、塩対応用メソッドgetShioCommentを、OnMessageActivityAsync内、getTweetと同じレベル（深さ）に作っていきます。こちらは特に何かと連携はせずに、コード内に書いたいくつかの塩対応フレーズからランダムに選んで返すだけのものにします。励まし対応の部分と被る要素が多いので、説明は割愛します。

```cs:EchoBot.cs
static string getShioComment()
{
   Random cRandom = new System.Random(); //乱数
   string res = "";
   var shio = new string[] { "へー・・・。", "・・・だから？", "知らんわー。", "興味ないね。", "いや、聞いてないし。", "ふーん・・・。で？", "そういうのいいから。", "あーちょっと今忙しいからまた今度。", "・・・けっ！", "リア充乙。" };

   var random = cRandom.Next(11);
   res = shio[random];
   return res;
}
```

呼び出し側も以下の通り変更します。条件分岐の塩対応部分でgetShioCommentメソッドを呼び出し、結果を出力することと、条件分岐外でres変数を定義し、塩対応、励まし対応それぞれの戻り値をres変数に格納する形にしました。

```cs:EchoBot.cs
var client = authenticateClient();
var score = sentimentAnalysisExample(client,turnContext.Activity.Text);//戻り値を格納するためscoreを用意
var res = "";//返事格納用
Debug.WriteLine($"Sentiment Score2: {score:0.00}");//出力確認2

if(0.5 <= score)
{
   res = getShioComment();
   Debug.WriteLine($"塩対応:{res}");//出力確認3
}
else
{
   res = getTweet();
   Debug.WriteLine($"励まし対応:{res}");//出力確認4
}
```

それではまたここで実行してみましょう。今度はできるだけポジティブな言葉を投げかけてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/284c4b4a-cb01-7159-4db5-ab1611f60130.png)

0.5以上のスコアで塩対応されていれば成功です！

## 2.6 ボットに発言させる
ここまできたら、後はもう少しです！今までデバッグとして出力させていたコメントを、ボットからユーザーに発言させるようにします。
まず、せっかくですので、返事に分析スコアもつけるため、score変数（小数点以下2桁まで）とres変数を連結します。`Environment.NewLine`は間で改行をするために入れています。
いよいよユーザーへの発言ですが、OnMessageActivityAsync内の`await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken);`部分で、ボットはオウム返しをしていますので、この部分を以下の通り変更して、塩対応/励まし対応の条件分岐後（返事をres変数に格納した後）に持ってきます。

```cs:EchoBot.cs
 res = $"Score:{score:0.00}" + Environment.NewLine + res;
 await turnContext.SendActivityAsync(MessageFactory.Text(res), cancellationToken);
```

また、オウム返しのテキストを作っている` var replyText = $"Echo: {turnContext.Activity.Text}";`は不要になるので、削除します。

ここまでの全ソースコードはこちらです。

```cs:EchoBot.cs
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.6.2

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;

//for TextAnalytics
using System;
using System.Net.Http;
using Microsoft.Azure.CognitiveServices.Language.TextAnalytics;
using Microsoft.Azure.CognitiveServices.Language.TextAnalytics.Models;
using Microsoft.Rest;
using System.Diagnostics;

//for Twitter
using CoreTweet;

namespace YasashiiBot.Bots
{
    public class EchoBot : ActivityHandler
    {
        //for TextAnalytics
        private static readonly string key = "<replace-with-your-text-analytics-key-here>;
        private static readonly string endpoint = "<replace-with-your-text-analytics-endpoint-here>";

        protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            
            //for TextAnalytics
            var client = authenticateClient();
            var score = sentimentAnalysisExample(client,turnContext.Activity.Text);//戻り値を格納するためscoreを用意
            var res = "";//返事格納用
            Debug.WriteLine($"Sentiment Score2: {score:0.00}");//出力確認2

            if(0.5 <= score)
            {
                res = getShioComment();
                Debug.WriteLine($"塩対応:{res}");//出力確認3
            }
            else
            {
                res = getTweet();
                Debug.WriteLine($"励まし対応:{res}");//出力確認4
            }

            res = $"Score:{score:0.00}" + Environment.NewLine + res;
            await turnContext.SendActivityAsync(MessageFactory.Text(res), cancellationToken);
        }

        protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
        {
            var welcomeText = "Hello and welcome!";
            foreach (var member in membersAdded)
            {
                if (member.Id != turnContext.Activity.Recipient.Id)
                {
                    await turnContext.SendActivityAsync(MessageFactory.Text(welcomeText, welcomeText), cancellationToken);
                }
            }
        }
        
        //for TextAnalytics
        static TextAnalyticsClient authenticateClient()
        {
            ApiKeyServiceClientCredentials credentials = new ApiKeyServiceClientCredentials(key);
            TextAnalyticsClient client = new TextAnalyticsClient(credentials)
            {
                Endpoint = endpoint
            };
            return client;
        }

        //for TextAnalytics
        static double sentimentAnalysisExample(ITextAnalyticsClient client, string message)//戻り値をdouble型に変更
        {
            var result = client.Sentiment(message, "ja");//引数messageを分析対象にし、言語は日本語に設定
            Debug.WriteLine($"User Message: {message}");//引数messageの内容を出力
            Debug.WriteLine($"Sentiment Score1: {result.Score:0.00}");//出力確認1

            return (double)result.Score;//分析スコアを返す
        }
        //for Twitter
        static string getTweet()
        {
            Random cRandom = new System.Random(); //取得したツイートからランダムに1つ選択するための乱数
            var tokens = Tokens.Create("<API KEY>", "<API SECRET>", "<ACCESS TOKEN>", "<ACCESS TOKEN SECRET>");  //接続用トークン発行
            var tweet = "";//取得したツイートを格納する変数

            var parm = new Dictionary<string, object>();  //条件指定用Dictionary
            parm["count"] = 60;  //取得数
            parm["screen_name"] = "hagemasi1_bot";  //取得したいユーザーID


            Task task = Task.Factory.StartNew(async () =>
            {
                var tweets = await tokens.Statuses.UserTimelineAsync(parm); //parmの内容に従ってツイートを取得

                var random = cRandom.Next(61); //0～60の間の乱数を生成
                tweet = tweets[random].Text; //取得した60ツイートからrandom番目のツイートを格納

            }).Unwrap();

            task.Wait();

            return tweet; //選んだツイートを戻り値として返す

        }

        static string getShioComment()
        {
            Random cRandom = new System.Random(); //乱数
            string res = "";
            var shio = new string[] { "へー・・・。", "・・・だから？", "知らんわー。", "興味ないね。", "いや、聞いてないし。", "ふーん・・・。で？", "そういうのいいから。", "あーちょっと今忙しいからまた今度。", "・・・けっ！", "リア充乙。" };

            var random = cRandom.Next(11);
            res = shio[random];
            return res;
        }
    }

    //for TextAnalytics
    class ApiKeyServiceClientCredentials : ServiceClientCredentials
    {
        private readonly string apiKey;

        public ApiKeyServiceClientCredentials(string apiKey)
        {
            this.apiKey = apiKey;
        }

        public override Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }
            request.Headers.Add("Ocp-Apim-Subscription-Key", this.apiKey);
            return base.ProcessHttpRequestAsync(request, cancellationToken);
        }
    }
}
```

# 2.7 Azureへ再デプロイする
PC上（ローカル）でBot Framework Emulatorを使って十分にテストができたら、Azure上に再度更新版のアプリをデプロイしましょう。
[第2部](https://qiita.com/miokawa/private/c71f9f4b62ce2b1c2864)に公式ドキュメントに沿ったデプロイ方法の記載がありますが、ちょっと面倒な手順なので、今回はVisual Studioから直接デプロイしてみます。途中でAzureへのサインインを要求されたら、適宜サインインをお願いします。

まず、ソリューションエクスプローラーから、ソリューション名を右クリックし、**発行**を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/94deeba1-8651-5c79-80f8-e5ad6947ccda.png)

次にデプロイ先のAzureリソースを選んでいきます。今回は[第2部](https://qiita.com/miokawa/private/c71f9f4b62ce2b1c2864)で作成したAzureリソースに再デプロイするので、**App Servive**、**既存のものを選択**を選んで、**プロファイルの作成**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/4c3bb24d-fa58-b7a1-5fbc-8a3bf50649fb.png)

続いて、App Serviceの詳細を指定します。**サブスクリプション**の部分で、デプロイ先のAzureサブスクリプションを選びます。**表示**部分は**リソースグループ**を選択し、下のボックスでデプロイ対象のリソースグループ、WebAppリソースを選択して、OKをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/757a6d4a-a405-6f40-2748-c8621c0ebae3.png)

最後に**発行**をクリックして、デプロイが終わるのを待ちます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/ee7610ee-419e-8395-722c-e7351e98f7e6.png)

デプロイされたら、[第2部 Webチャットでのテスト](https://qiita.com/miokawa/private/c71f9f4b62ce2b1c2864#25-web%E3%83%81%E3%83%A3%E3%83%83%E3%83%88%E3%81%A7%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88)の手順を参考に、テストをします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596292/70051f99-ab86-4cf6-75d2-fa17fa62e1c2.png)

ポジティブな発言とネガティブな発言をして、適切な返事が返ってきていれば成功です！


# 以降の工程について
以降の工程はこのようになっています。
次回はいよいよ、Azure上にあるボットアプリと、LINE経由でおしゃべりができるようにしてみたいと思います。

1. [Bot Frameworkでチャットボットを作ってみよう！](https://qiita.com/miokawa/private/6f1a3e480580e1e29abc)
2. [ボットをAzure（クラウド）にデプロイしてみよう！](https://qiita.com/miokawa/private/c71f9f4b62ce2b1c2864)
3. [Cognitive Serviceでテキスト分析をしてみよう！](https://qiita.com/miokawa/private/cdb243401fe3c8d9d583)
4. [Twitterからつぶやきをとってきてみよう！](https://qiita.com/miokawa/private/eb9f16220a36f65d7098)**←イマココ**
5. [LINE経由でボットと話してみよう！](https://qiita.com/miokawa/private/c6f0b550f8fa41a23228)