おれの財宝か?欲しけりゃくれてやるぜ... さがしてみろお財布の全てをTwilioに置いてきた

# はじめに
このページはTwilioアドベントカレンダー企画です。
https://qiita.com/advent-calendar/2021/twilio-build

# Twilioを使ってなにか面白いことできないかな
今年はアドベントカレンダーをみてTwilioを触ってみることにした。
触っていくと**簡単に電話対応のコールセンター** が作れるということがわかった。

よろしい、ならば「謎解き」だ。

**ということで謎解きゲームを作ってみました。**
![Screen Shot 2021-12-27 at 2.09.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138863/388048b6-d53b-fc6d-add2-7e9eb17e7db4.png)


# 謎解きゲームルール
:::note info
スタート番号は +1(423)561-3869 です かけてみてください。
:::
- 謎解きなので、あまり多くは語りませんが、謎(クイズ)に答えていき、解いていく感じです。
- 2021/12/19 ~ の限定の予定です。
- twiloに課金した額がなくなりしだい終了する場合があります。w
- 謎解き完了者が十分に出た場合終了とします。

### 謎解き完走者、先着1位に5000円のアマゾンギフト2位に1000円のアマゾンギフト券をプレゼントします！！
![onepiece01_luffy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138863/f69c686b-efd7-eb1f-a859-aacae93f4749.png)


### ということでどうかどうかご興味あるかたは挑戦ください！
というか、せっかく作ったので遊んで欲しい。
実際問題そんなに激戦とかじゃないし、やれば楽しいと信じてます。


# 感想
- Twilio簡単にコールセンターとか作れて便利！！素敵！！Documentに多くの言語のサンプルがあるので最高。
- エラー時はログコンソールもあるが、結局動作確認に電話しまくることになる。お財布がwww
- カレンダー開発駆動、ひりついて、楽しかったです！
- Twilio Console内のFunctionsで簡単に開発、デプロイできる。便利すぎる。サーバレス。
![Screen Shot 2021-12-19 at 9.11.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138863/5de4b468-ae48-db41-1094-78e6432f3a90.png)

# 結果発表
- 謎解きに先着2名のクリア者が出ましたので、クリスマスプレゼントととして特典を付与しました！！
- ttya16様、kambarakun様、謎解きおめでとうございます。アンド解いて頂きありがとうございました！！！
- のべ100件程度のコール頂きました！！解いていただいた皆様、誠に誠にありがとうございました。
- **一応まだTwilio課金額は残っているので、謎自体はまだ少しの時間開けておこうと思います。良かったらやってみてください。**
![text_kansya.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138863/d17c3658-dfe1-7dca-f092-e981f9f79861.png)


# 中身ネタバレ
:::note alert
下記から「謎」の中身のネタバレに入ります。
実際に自分でときたい方は、下を見る前に、 +1(423)561-3869 にかけてみてください。
:::
実際どんなの作ったのと気になる人に対して、回答編載せておきます。

- 謎は全部で2問の構成にしていました。
- 1問目は簡単なクイズです。「ハルロックって何?」という問題。簡単ですよね?w
- 2問目は、期待値計算クイズ。ただ、最後にちょっと罠があります。
- 両方ともダイアルで回答する形です。不正解ならその旨が出てきてやり直し。正解なら次の問題へのSMSが送られてくる仕様です。

一応こんな感じで作ったというセリフのスクリプト載せておきます
超絶汚いですが、締切開発駆動で、突貫でやったので許してください。動いてはいます。w

### 1問目
```voice-ivr.js
/**
 * Returns TwiML that prompts the users to make a choice.
 * If the user enters something it will trigger the handle-user-input Function and otherwise go in a loop.
 */
exports.handler = function (context, event, callback) {
  const twiml = new Twilio.twiml.VoiceResponse();
  const gather = twiml.gather({
    numDigits: 1,
    action: 'handle-user-input',
    input: 'dtmf'
  });
  gather.say({language: 'ja-JP', voice: 'alice'}, 'ようこそ。これは、謎解きゲームです。賞金もあるかもです。楽しんでいってくださいね。');
  gather.pause({length: 1});
  gather.say({language: 'ja-JP', voice: 'alice'}, 'それでは第一問。');
  gather.say({language: 'ja-JP', voice: 'alice'}, '「ハルロック」ってなーんだ。');
  gather.say({language: 'ja-JP', voice: 'alice'}, '正解を数字で選んでね');
  gather.say({language: 'ja-JP', voice: 'alice'}, '1 珍しい宝石の名前');
  gather.say({language: 'ja-JP', voice: 'alice'}, '2 面白いマンガの名前');
  gather.say({language: 'ja-JP', voice: 'alice'}, '3 歴史的な発明しゃ');
  gather.say({language: 'ja-JP', voice: 'alice'}, '4 美味しいフルーツの名前');
  gather.say({language: 'ja-JP', voice: 'alice'},'さあ、どれでしょう。数字を押して選んでね。');
  gather.say({language: 'ja-JP', voice: 'alice'},'以上です。ご視聴ありがとうございました。もう一度聞きたいかたは、おてすうですが、かけ直してください。');

  gather.pause({length: 20});
  gather.say(
    {language: 'ja-JP', voice: 'alice'},
    'あ、ちょっとまってください。。。。'
  );
  gather.say(
    {language: 'ja-JP', voice: 'alice'},
    '最後のヒントをもう一つ。'
  );
  gather.say(
    {language: 'ja-JP', voice: 'alice'},
    '「もし困ったら、数字を倍にしてみてくださいね」'
  );
    gather.say(
    {language: 'ja-JP', voice: 'alice'},
    'では'
  );
  callback(null, twiml);
};

```


```handle-user-input.js
function sendMessage(context, event) {
  const client = context.getTwilioClient();
  return client.messages
    .create({
      from: event.To,
      to: event.From,
      body: '第一問突破おめでとう！！第二の謎はこちらに、 +1(XXX)XXX-XXXX',
    })
    .then(
      (resp) => resp,
      (err) => {
        console.log(err);
        return Promise.resolve();
      }
    );
}

/**
 * Handles the user input gathered in the voice-ivr Function
 */
// eslint-disable-next-line consistent-return
exports.handler = function (context, event, callback) {
  let UserInput = event.Digits || event.SpeechResult;
  const twiml = new Twilio.twiml.VoiceResponse();

  if (!UserInput) {
    twiml.say('Sorry something went wrong. Please call again');
    return callback(null, twiml);
  }

  if (UserInput.length > 1) {
    if (UserInput.toLowerCase().includes('sales')) {
      UserInput = '1';
    } else if (UserInput.toLowerCase().includes('opening hours')) {
      UserInput = '2';
    } else if (UserInput.toLowerCase().includes('address')) {
      UserInput = '3';
    }
  }

  switch (UserInput) {
    case '1':
    case '3':
    case '4':
      twiml.say({language: 'ja-JP', voice: 'alice'}, '残念！不正解です。また、掛け直してチャレンジしてね！');
      return callback(null, twiml);
    case '2':
      twiml.say(
        {language: 'ja-JP', voice: 'alice'}, 
        '正解です！おめでとう！！エスエムエスをおくるよ！'
        );
      break; 
    default:
      twiml.say(
        'あれっつ、うまく認識できません。再度やり直してください。'
      );
      twiml.redirect('voice-ivr');
  }

  let request = Promise.resolve();
  if (UserInput === '2') {
    request = sendMessage(context, event);
  }

  request
    .then(() => {
      return callback(null, twiml);
    })
    .catch((err) => {
      return callback(err);
    });
};
```

### 2問目
```voice-ivr.js

/**
 * Returns TwiML that prompts the users to make a choice.
 * If the user enters something it will trigger the handle-user-input Function and otherwise go in a loop.
 */
exports.handler = function (context, event, callback) {
  const twiml = new Twilio.twiml.VoiceResponse();
  const gather = twiml.gather({
    numDigits: 2,
    action: 'handle-user-input',
    input: 'dtmf'
  });
  gather.say({language: 'ja-JP', voice: 'alice'}, 'こんにちは、ここはツイリオ謎解きゲーム第二問です。ゴールまでもうちょっとです。頑張ってね！');
  gather.pause({
    length: 1
});
  gather.say({language: 'ja-JP', voice: 'alice'}, 'それでは第ニもん。');
  
  gather.say({language: 'ja-JP', voice: 'alice'}, '20種類のキャラクターがめちゃめちゃいっぱい入ったガチャガチャがあります。');
  gather.say({language: 'ja-JP', voice: 'alice'}, 'それぞれのキャラクターは等しい確率で出るとします。');
  gather.say({language: 'ja-JP', voice: 'alice'}, 'あなたがすべてのキャラクターを引き当てるために、必要なガチャ回数の期待値は何回でしょう。');
  gather.say({language: 'ja-JP', voice: 'alice'},'以下の数字を押して選んでね。');
  gather.say({language: 'ja-JP', voice: 'alice'},'21回以上、30回以下なら、1を');
  gather.say({language: 'ja-JP', voice: 'alice'},'31回以上、40回以下なら、2を');
  gather.say({language: 'ja-JP', voice: 'alice'},'41回以上、50回以下なら、3を');
  gather.say({language: 'ja-JP', voice: 'alice'},'51回以上、60回以下なら、4を');
  gather.say({language: 'ja-JP', voice: 'alice'},'61回以上、70回以下なら、5を');
  gather.say({language: 'ja-JP', voice: 'alice'},'71回以上、80回以下なら、6を');
  gather.say({language: 'ja-JP', voice: 'alice'},'81回以上、90回以下なら、7を');
  gather.say({language: 'ja-JP', voice: 'alice'},'91回以上、100回以下なら、8を');
  gather.say({language: 'ja-JP', voice: 'alice'},'101回以上、110回以下なら、9を');
  gather.say({language: 'ja-JP', voice: 'alice'},'111回以上なら、10を');
  
  gather.say({language: 'ja-JP', voice: 'alice'},'さあ、どれでしょう。数字を押して選んでね。');
  gather.pause({
    length: 30
   });  
  twiml.say({language: 'ja-JP', voice: 'alice'},'最後まで聞いてくれてありがとう！最初に戻ります。');
  twiml.redirect();
  callback(null, twiml);
};


```


```handle-user-input.js
function sendMessage(context, event) {
  const client = context.getTwilioClient();
  return client.messages
    .create({
      from: event.To,
      to: event.From,
      body: '謎解きゲームクリアおめでとう！！そして、お付き合い頂きありがとうございました！最後に次のフォームにいれてください！ https://forms.gle/XXXXXXXXXXXXXXXXXXXX',
    })
    .then(
      (resp) => resp,
      (err) => {
        console.log(err);
        return Promise.resolve();
      }
    );
}

/**
 * Handles the user input gathered in the voice-ivr Function
 */
// eslint-disable-next-line consistent-return
exports.handler = function (context, event, callback) {
  let UserInput = event.Digits || event.SpeechResult;
  const twiml = new Twilio.twiml.VoiceResponse();

  if (!UserInput) {
    twiml.say('Sorry something went wrong. Please call again');
    return callback(null, twiml);
  }

  if (UserInput.length > 1) {
    if (UserInput.toLowerCase().includes('sales')) {
      UserInput = '1';
    } else if (UserInput.toLowerCase().includes('opening hours')) {
      UserInput = '2';
    } else if (UserInput.toLowerCase().includes('address')) {
      UserInput = '3';
    }
  }

  switch (UserInput) {
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '7':
    case '8':
    case '9':
    case '10':
      twiml.say({language: 'ja-JP', voice: 'alice'}, '残念！不正解です。また、掛け直してチャレンジしてね！');
      return callback(null, twiml);
    case '12':
      twiml.say(
        {language: 'ja-JP', voice: 'alice'}, 
        '正解です！おめでとう！！エスエムエスをおくるよ！これで謎解きゲームはクリアです！ありがとうございました！'
        );
      break;
    case '6':
      twiml.say({language: 'ja-JP', voice: 'alice'}, '残念！不正解です。また、掛け直してチャレンジしてね！あれ〜、変だな〜。ヒント欲しいな〜って かた は、最初の電話をじっくり、くまなく聞くと良いかもしれません。');
      break;
    default:
      twiml.say({language: 'ja-JP', voice: 'alice'},
        'あれっつ、うまく認識できません。再度やり直してください。'
      );
      twiml.redirect('voice-ivr');
  }

  let request = Promise.resolve();
  if (UserInput === '12') {
    request = sendMessage(context, event);
  }

  request
    .then(() => {
      return callback(null, twiml);
    })
    .catch((err) => {
      return callback(err);
    });
};

```

### 技術的Tips
- 日本語読み上げで、感じだと時々間違えるところある。ひらがな表記とか適宜必要そう。
- Twilioで日本語番号とれるが、SMS使えるのは海外版のみ
- サンプルコードとかもドキュメントには沢山あり
- 有料アカウント化すると、無料でもらったTrialクレジットは消えちゃうので注意

# ふりかえり
感想にも書きましたが
#### Twilio Console内のFunctionsで簡単に開発、デプロイできる。便利すぎる。サーバレス。
の一言に付きます。
また、今回100件程度の電話でしたが、かかったコストは数百円でした。
簡単に実装できるので、結構お問い合わせ逼迫してるベンチャーとかは導入検討しても良さそうだなと思いました。

あと個人的な反省は、今回SMSもサクッと使いたかったのでTwilio番号をアメリカの番号にしてしまいましたが、
そこを国内番号にしとけばもっと敷居が下がったかなというのは反省ポイントです。
(そこで参加できなかったお友達も多いのではと思っています。)

あと謎解き(?)はじめて作りましたが面白かったです。


<a href="https://amzn.to/3Fu2oYr" target="_blank"><img border="0" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138863/c14fe7ff-011e-e9bd-c091-80120bb0bf8d.png" width="250"></a>
<a href="https://amzn.to/3Fu2oYr">ハルロック</a>
おもろいです！