Alexa Reminder skillですべてのリマインダーの取得と削除

Alexa Reminder skillで「すべてのリマインダーの取得」と「リマインダーの削除」を試してみたので、その内容をまとめます。
「リマインダーの作成」についてはいろいろネットを検索するとでてきましたが、「すべてのリマインダーの取得」や「リマインダーの削除」などについての記事は見つけることができませんでしたので、ここにメモを残します。

#前提
まず、「すべてのリマインダーの取得」にあたり、「リマインダーの作成」はできているものとします。
以下の記事で作成済みです。
[Alexa Reminder skillのリマインダー作成時のtriggerについて](https://qiita.com/Mount/items/4b3323a2190380c413f2)
[Alexa Reminder skillでリマインダー作成時にInvalid Token、DEVICE_NOT_SUPPORTED、UNAUTHORIZEDとなった場合の対策](https://qiita.com/Mount/items/ae60d3dbb20e238a2dcb)

#注意事項
「すべてのリマインダーを取得」する場合、Alexaのリマインダーにスキルで作成したリマインダーが設定されていることが望ましいと思います。
今回、取得できるリマンダーは、自分で作成したスキル上で追加したものに限られます。
別のスキルやもともとAlexaに標準で搭載されているリマインダーは、取得できません。
そういったリマインダーはセッション外として別途アクセス許可が必要なようなため、試せていません。
そのやり方は、以下にあるようですが、私にはよくわかりませんでした。
[AlexaリマインダーAPIのセッション内およびセッション外の動作](https://developer.amazon.com/ja/docs/smapi/alexa-reminders-api-reference.html#in-session-and-out-of-session-behavior-for-alexa-reminders-api)

#すべてのリマインダーの取得
##すべてのリマインダーを取得する理由
- リマインダーを削除するためには、削除するリマインダーのid、すなわちalertTokenが必要になります。
- 削除可能なリマインダーは、アクティブなもの（statusがON）だけに限られます。完了済みのもの（statusがCOMPLETE）は削除できず、削除しようとするとエラー（404 ALERT_NOT_FOUND）が発生します。そのため、アクティブなアラートを対象にする必要があります。
- リマインダーを削除するためには、ユーザーがどんなリマインダーを削除するのか選択する必要があります。

##すべてのリマインダーの取得方法
基本的なリマインダーの作成とやり方は同じです。
違うのは、HTTPのメソッドがPOSTからGETになることくらいでしょうか。
###リクエスト

```
GET /v1/alerts/reminders
```

これを送ってあげると、このスキルで作成されたリマインダーをすべて取得することができます。ソースコードは、以下の通りです。

```
const https = require('https');
const urlParse = require('url').parse;
:
const GetReminderHandler = {
  canHandler(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return (request.type === 'IntentRequest')
      && (request.intent.name === 'GetReminderIntent);
  },
  async handle(handlerInput) {
    const system = handlerInput.requestEnvelope.context.System;
    const accessRequest = {
      hostname: urlParse(system.apiEndpoint).hostname,
      path: "/v1/alerts/reminders",
      auth: `Bearer ${system.apiAccessToken}`,
      method: "GET",
    };

    const res = await httpGET(accessRequest);
    let speechOutput;
    if (typeof res.totalCount === 'undefined') {
      speechOutput = "リマンダーを取得できませんでした";
    } else {
      speechOutput = res.totalCount;
    }
    return handlerInput.responseBuilder
      .speak(speechOutput)
      .withShouldEndSession(true)
      .withSimpleCard(SKILL_NAME, res.totalCount)
      .getResponse();
  },
};

/**
 * http GET用。
 * リマインダーの取得で利用する。
 * 
 * @request hostname, Authorizationを含むObject
 * @return responseの値、もしくはBodyの値。
 */
const httpGET = (request) => {
  console.log("httpGET");
  const Options = {
    hostname: request.hostname,
    path: request.path,
    method: request.method,
    headers: {
    'Content-Type': 'application/json',
    'Authorization': request.auth,
    },
  };
  return new Promise((resolve, reject) => {
    const clientRequest = https.request(Options, (response) => {
      const chunks = [];
      response.on('data', chunk => {
        chunks.push(chunk);
      }).on('end', () => {
        const hex = chunks.join('');
        let parseObject;
        if (hex != '') {  //bodyがある場合bodyを返す
          parseObject = JSON.parse(hex);
          resolve(parseObject);
        }
        resolve(response);  //bodyがない場合headerを返す
      }).on('error', (error) => {
        reject(error);
      });
    });
    clientRequest.end();
  });
};

:
const skillBuilder = Alexa.SkillBuilders.standard();
exports.handler = skillBuilder
  .addRequestHandlers(
    GetReminderHandler,  //handlerの追加を忘れずに。
    HelpHandler,
    ExitHandler,
    SessionEndedRequestHandler
  )
  .addErrorHandlers(ErrorHandler)
  .lambda();

```

##ソースコードの説明
ところどころ抽出して、説明します。
###accessRequestの説明

```
    const system = handlerInput.requestEnvelope.context.System;
    const accessRequest = {
      hostname: urlParse(system.apiEndpoint).hostname,
      path: "/v1/alerts/reminders",
      auth: `Bearer ${system.apiAccessToken}`,
      method: "GET",
    };
```

hostnameは、IntentのhandlerInputの値から取得します。Alexa Skills kitによると、APIエンドポイントは、「https://api.amazonalexa.com」となっていますが、私の環境では少し異なるようです。
authは、apiAccessTokenの値を設定します。
methodは、GETです。

###httpGETの呼び出しの説明

```
  async handle(handlerInput) {
  :
    const res = await httpGET(accessRequest);
    let speechOutput;
    if (typeof res.totalCount === 'undefined') {
      speechOutput = "リマンダーを取得できませんでした";
    } else {
      speechOutput = res.totalCount;
    }
```

handlerには、asyncをつけて、httpGETの呼び出しには、awaitを入れました。
httpGET関数は、ネットのサンプルを参照してPromiseを使いましたが、httpGET処理の完了を担保できないため、その外側にawaitを入れることにしました。
必要な理由はよくわかりませんが、こうしないと応答のheaderまでしかPromiseされないようです。イベントリスナーはPromiseされないのか？まだPromiseを理解しきれておりませんので、現状はこうしました。

###httpGET関数の中身

```
  return new Promise((resolve, reject) => {
    const clientRequest = https.request(Options, (response) => {
      const chunks = [];
      response.on('data', chunk => {
        chunks.push(chunk);
      }).on('end', () => {
        const hex = chunks.join('');
        let parseObject;
        if (hex != '') {  //bodyがある場合bodyを返す
          parseObject = JSON.parse(hex);
          resolve(parseObject);
        }
        resolve(response);  //bodyがない場合headerを返す
      }).on('error', (error) => {
        reject(error);
      });
    });
    clientRequest.end();
  });
};
```

ネットにあったサンプルをほぼそのまま使っており、どういう処理が適切なのか判断できていませんが、ちょっとだけアレンジしました。
https.requestでGETを送信した応答は、responseでいったんheaderだけが戻ります。
そこからonイベント('data')とonイベント('end')で、bodyをすべて取得します。
bodyを含む場合は、body（すなわち、chunkをJSON.parseしたもの）を返し、bodyを含まない場合は、header（すなわち、response）を返すようにしました。
GETの応答のheaderとbodyを両方取得したかったのですが、いまいちどうやって返せばいいのかわからなかったため苦肉の策です。

###res.totalCountの取得

```
    const res = await httpGET(accessRequest);
    let speechOutput;
    if (typeof res.totalCount === 'undefined') {
      speechOutput = "リマンダーを取得できませんでした";
    } else {
      speechOutput = res.totalCount;
    }
```

resに対してtotalCountが定義されているかを確認するif文を作りました。
res.totalCountの定義がない場合は、なんらかのエラーが発生することになります。
totalCountをspeechOutputに入れたのは、単に応答をAlexaに話させるためだけです。

#リマインダーの削除

##リマインダーの削除方法
基本的なリマインダーの取得とやり方は同じです。
違うのは、HTTPのメソッドがGETからDELETEになることと削除するidの設定が必要なことくらいでしょうか。
###リクエスト

```
DELETE /v1/alerts/reminders/{id}
```

削除するには、削除するリマインダーのidが必要です。このidは、alertTokenのことであり、リマインダーの取得で、値がわかります。
リマインダーを作成すると作成され、一意に識別されるものと思われますので、リマインダー作成時に取得することもありとは思いますが、リマインダーの削除では、完了済みのリマインダーは削除できないため、削除前に改めてstatusを確認しつつ、削除するのがよいと思います。
ソースコードは以下の通りです。

```
const DeleteReminderHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return (request.type === 'IntentRequest')
      && (request.intent.name === 'DeleteReminderIntent');
  },
  async handle(handlerInput) {
    const system = handlerInput.requestEnvelope.context.System;
    const getRequest = {
      hostname: urlParse(system.apiEndpoint).hostname,
      path: "/v1/alerts/reminders",
      auth: `Bearer ${system.apiAccessToken}`,
      method: "GET",
    };
    const reminders = await httpGET(getRequest);
    const totalCount = reminders.totalCount;
    let speechOutput;
    const alertToken = [];
    if (typeof totalCount === 'undefined') {  //bodyが正常でない場合
      speechOutput ="削除できませんでした。";      
    } else {  //bodyが正常な場合, alertTokenをすべて取得して削除。
      for (let i = 0 ; i < totalCount; i++) {
        if (reminders.alerts[i].status === 'ON') {
          alertToken.push(reminders.alerts[i].alertToken);
        }
      }
      if (alertToken.length == 0) {
        speechOutput = "有効なリマインダーはありませんでした。";
      } else {
        await deleteReminders(system, alertToken);
        speechOutput = "削除しました。";
      }
    }
    return handlerInput.responseBuilder
      .speak(speechOutput)
      .withShouldEndSession(true)
      .withSimpleCard(SKILL_NAME, speechOutput)
      .getResponse();
  },
};

const deleteReminders = async (system, alertToken) => {
  console.log("DELETE");
  for (let i = 0 ; i < alertToken.length; i++) {
    alertToken[i] = encodeURIComponent(alertToken[i]);
    const deleteRequest = {
      hostname: urlParse(system.apiEndpoint).hostname,
      path: `/v1/alerts/reminders/${alertToken[i]}`,
      auth: `Bearer ${system.apiAccessToken}`,
      method: "DELETE",
    };
    const res = await httpGET(deleteRequest);
  }
};
:
const skillBuilder = Alexa.SkillBuilders.standard();
exports.handler = skillBuilder
  .addRequestHandlers(
    GetReminderHandler,  //handlerの追加を忘れずに。
    DeleteReminderHandler,  //handlerの追加も忘れずに。
    HelpHandler,
    ExitHandler,
    SessionEndedRequestHandler
  )
  .addErrorHandlers(ErrorHandler)
  .lambda();
```

##ソースコードの説明
ところどころ抽出して、説明します。
###まずはリマインダーの取得の説明

```
  async handle(handlerInput) {
    const system = handlerInput.requestEnvelope.context.System;
    const getRequest = {
      hostname: urlParse(system.apiEndpoint).hostname,
      path: "/v1/alerts/reminders",
      auth: `Bearer ${system.apiAccessToken}`,
      method: "GET",
    };
    const reminders = await httpGET(getRequest);
    const totalCount = reminders.totalCount;
```

いったん、すべてのリマインダーを取得し、totalCountを取得します。

###alertToken取得と、削除

```
    const alertToken = [];
    if (typeof totalCount === 'undefined') {  //bodyが正常でない場合
      speechOutput ="削除できませんでした。";      
    } else {  //bodyが正常な場合, alertTokenをすべて取得して削除。
      for (let i = 0 ; i < totalCount; i++) {
        if (reminders.alerts[i].status === 'ON') {
          alertToken.push(reminders.alerts[i].alertToken);
        }
      }
      if (alertToken.length == 0) {
        speechOutput = "有効なリマインダーはありませんでした。";
      } else {
        await deleteReminders(system, alertToken);
        speechOutput = "削除しました。";
      }
    }
```

totalCountの型がundefinedになる場合、Bodyを取得できていないため処理を終了します。
取得できた場合は、alertTokenをすべて取得します。
そのなかでstatusがONとなっているアクティブなリマインダーを取得します。
そのアクティブなリマインダーに対して、deleteReminders関数で削除します。

###deleteReminers関数

```
const deleteReminders = async (system, alertToken) => {
  console.log("DELETE");
  for (let i = 0 ; i < alertToken.length; i++) {
    alertToken[i] = encodeURIComponent(alertToken[i]);
    console.log(`accessToken:${alertToken[i]}`);
    const deleteRequest = {
      hostname: urlParse(system.apiEndpoint).hostname,
      path: `/v1/alerts/reminders/${alertToken[i]}`,
      auth: `Bearer ${system.apiAccessToken}`,
      method: "DELETE",
    };

    const res = await httpGET(deleteRequest);
    if (!(typeof res.statusCode === "undefined")) console.log("delete res.statusCode:" + res.statusCode);
  }
};
```

単純にalertTokenの数だけ、リマインダー削除を実施します。
`encodeURIComponent(alertToken[i])'は、文字列が適切かどうか確認しているだけです。ここでは処理は何もしていません。
削除するのにhttpGET関数を再利用しています。削除のmethodはDELTEであるため、関数名と一致しないのはいまいちでした。別の関数名をつけたほうがよかったですね。


#参考情報
[Alexa Skill Kit すべてのリマインダーの取得](https://developer.amazon.com/ja/docs/smapi/alexa-reminders-api-reference.html#get-all-reminders)